cortad 0.3.1-rc.8 → 0.3.1-rc.9

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.
@@ -579,7 +579,7 @@ def _install():
579
579
  # person's latest message on, and Anthropic document and search_result blocks. The app's own
580
580
  # instructions are not passages: only what sits under such a label is.
581
581
  passage_text, passages_max = 3000, 6
582
- material = re.compile(r"\b(?:retriev\w*|context|knowledge|documents?|sources?|search[ _-]?results?|references?|passages?|excerpts?|snippets?|chunks?|background|faq)\b|检索|知识|参考资料|资料|上下文|文档|背景", re.I)
582
+ material = re.compile(r"\b(?:retriev\w*|context|knowledge|documents?|sources?|search[ _-]?results?|references?|passages?|excerpts?|snippets?|chunks?|background|faq|relevant)\b|检索|知识|参考资料|资料|上下文|文档|背景|相关", re.I)
583
583
  heading = re.compile(r"^\s*(?:#{1,6}\s+[^\n]{1,80}|[^\n]{1,80}[::]\s*(?:\([^\n)]*\))?|\[[^\n\]]{1,80}\]|={2,}\s*[^\n]{1,80}?\s*={2,})\s*$")
584
584
  # An inline label is a noun phrase that ends in the material word ("context:", "background:",
585
585
  # "retrieved passages:"): "4. Knowledge:" in a numbered instruction and a JSON key are not.
@@ -587,6 +587,36 @@ def _install():
587
587
  named = re.compile(r"(?:" + material.pattern + r")\s*$", re.I)
588
588
  tagged = re.compile(r"<([A-Za-z][\w-]*)[^>]*>([\s\S]*?)</\1>")
589
589
 
590
+ # A label's block runs on to the next heading of its own kind: "资料:" over retrieved chunks
591
+ # that open on markdown headings holds every chunk, where stopping at its first paragraph kept
592
+ # one chunk of five and the reader called the knowledge base's own 1% fee made up. A markdown
593
+ # label holds deeper headings. A closing paragraph that opens on a label of its own ("问题:...",
594
+ # "Question: ...") is the prompt's ask, not material. A long block is several passages, cut
595
+ # between its paragraphs.
596
+ markdown = re.compile(r"^\s*(#{1,6})\s")
597
+
598
+ def ends_block(label, line):
599
+ if not heading.match(line):
600
+ return False
601
+ outer, inner = markdown.match(label), markdown.match(line)
602
+ return not inner or bool(outer and len(inner.group(1)) <= len(outer.group(1)))
603
+
604
+ def asks(para):
605
+ m = inline.match(para.split("\n")[0])
606
+ return bool(m and not named.search(m.group(1)))
607
+
608
+ # A heading inside a fenced block is the material's own text: a README chunk's "## Features".
609
+ fence = re.compile(r"^\s*(?:```|~~~)", re.M)
610
+
611
+ def add_block(add, name, paras):
612
+ text = ""
613
+ for p in paras:
614
+ if text and len(text) + len(p) + 2 > passage_text:
615
+ add(name, text)
616
+ text = ""
617
+ text += ("\n\n" if text else "") + p
618
+ add(name, text)
619
+
590
620
  def labelled(text, add):
591
621
  def lift(m):
592
622
  if material.search(m.group(1).replace("_", " ")):
@@ -599,13 +629,14 @@ def _install():
599
629
  lines = paras[i].split("\n")
600
630
  head = lines[0]
601
631
  if heading.match(head) and material.search(head):
602
- body = "\n".join(lines[1:])
603
- # A heading alone on its paragraph labels what follows, up to the next heading.
604
- if not body.strip():
605
- while i + 1 < len(paras) and len(body) < passage_text and not heading.match(paras[i + 1].split("\n")[0]):
606
- i += 1
607
- body += ("\n\n" if body else "") + paras[i]
608
- add(re.sub(r"[#::\[\]=]", "", head).strip(), body)
632
+ block = ["\n".join(lines[1:])]
633
+ fenced = len(fence.findall(block[0])) % 2 == 1
634
+ while i + 1 < len(paras) and (fenced or (not ends_block(head, paras[i + 1].split("\n")[0]) and not (i + 2 == len(paras) and asks(paras[i + 1])))):
635
+ i += 1
636
+ block.append(paras[i])
637
+ if len(fence.findall(paras[i])) % 2 == 1:
638
+ fenced = not fenced
639
+ add_block(add, re.sub(r"[#::\[\]=]", "", head).strip(), block)
609
640
  else:
610
641
  label = next((m for m in (inline.match(l) for l in lines) if m and named.search(m.group(1))), None)
611
642
  if label:
@@ -855,7 +886,76 @@ def _install():
855
886
  except Exception:
856
887
  return ""
857
888
 
858
- def meter(url, sent, status, kind, raw, turn=None):
889
+ # Which of the app's own lines made a model call, nearest first: the run names the code path that
890
+ # ran by it, where the read could only name the door. Its frames on this stack outside installed
891
+ # packages, then, when the call runs in a task another task awaits (LangChain runs each step of a
892
+ # chain as its own task), the awaiting task's. Relative to the folder the app was started in.
893
+ root = os.getcwd().replace(os.sep, "/").rstrip("/") + "/"
894
+ installed = re.compile(r"/(?:site-packages|dist-packages|\.venv|venv|__pypackages__)/|/lib/python\d")
895
+
896
+ def ours(frame, out):
897
+ f = frame.f_code.co_filename.replace(os.sep, "/")
898
+ if f.startswith(root) and not installed.search(f[len(root) - 1:]):
899
+ at = "%s:%d" % (f[len(root):], frame.f_lineno)
900
+ if at not in out:
901
+ out.append(at)
902
+ return len(out) >= 3
903
+
904
+ # The task that waits on this one: its wake-up is among this one's callbacks, directly, or behind
905
+ # the future an asyncio.gather callback closes over.
906
+ def awaiting(fut, depth=0):
907
+ for cb in getattr(fut, "_callbacks", None) or ():
908
+ fn = cb[0] if isinstance(cb, tuple) else cb
909
+ owner = getattr(fn, "__self__", None)
910
+ if owner is not None and owner is not fut and hasattr(owner, "get_coro"):
911
+ return owner
912
+ for cell in (getattr(fn, "__closure__", None) or ()) if depth < 2 else ():
913
+ try:
914
+ inner = cell.cell_contents
915
+ except ValueError:
916
+ continue
917
+ found = awaiting(inner, depth + 1) if inner is not fut and hasattr(inner, "_callbacks") else None
918
+ if found is not None:
919
+ return found
920
+ return None
921
+
922
+ # An embedding call answers nobody, so where it was made names no path a reply came down.
923
+ embedding = re.compile(r"/embeddings$", re.I)
924
+
925
+ def callers_now(url):
926
+ if embedding.search(urlsplit(str(url)).path or ""):
927
+ return None
928
+ out = []
929
+ try:
930
+ # Below the event loop's own frames is whatever started the loop, not this call's path.
931
+ frame = sys._getframe(1)
932
+ while frame is not None and not frame.f_code.co_filename.replace(os.sep, "/").endswith("/asyncio/events.py"):
933
+ if ours(frame, out):
934
+ return out
935
+ frame = frame.f_back
936
+ aio = sys.modules.get("asyncio")
937
+ try:
938
+ task = aio.current_task() if aio else None
939
+ except RuntimeError:
940
+ task = None
941
+ for _ in range(8):
942
+ task = awaiting(task) if task is not None else None
943
+ if task is None:
944
+ break
945
+ chain, coro = [], task.get_coro()
946
+ while coro is not None and len(chain) < 64:
947
+ f = getattr(coro, "cr_frame", None) or getattr(coro, "gi_frame", None) or getattr(coro, "ag_frame", None)
948
+ if f is not None:
949
+ chain.append(f)
950
+ coro = getattr(coro, "cr_await", None) or getattr(coro, "gi_yieldfrom", None) or getattr(coro, "ag_await", None)
951
+ for f in reversed(chain):
952
+ if ours(f, out):
953
+ return out
954
+ except Exception:
955
+ pass
956
+ return out or None
957
+
958
+ def meter(url, sent, status, kind, raw, turn=None, caller=None):
859
959
  try:
860
960
  tokens, model, events = read_reply(kind, (raw or "")[:reply_max])
861
961
  parts = urlsplit(str(url))
@@ -878,6 +978,8 @@ def _install():
878
978
  passages = passages_in(as_text)
879
979
  if passages:
880
980
  row["passages"] = passages
981
+ if caller:
982
+ row["caller"] = caller
881
983
  write({"call": row})
882
984
  except Exception:
883
985
  pass
@@ -1425,7 +1527,7 @@ def _install():
1425
1527
  finally:
1426
1528
  self.finish()
1427
1529
 
1428
- def watch(request, response, sent):
1530
+ def watch(request, response, sent, caller):
1429
1531
  model = is_model_call(request.url)
1430
1532
  if not model and not is_retrieval(request.url):
1431
1533
  dep(request.url, response.status_code)
@@ -1433,13 +1535,13 @@ def _install():
1433
1535
  kind = response.headers.get("content-type", "")
1434
1536
  if getattr(response, "_content", None) is not None:
1435
1537
  if model:
1436
- meter(request.url, sent, response.status_code, kind, response.content.decode("utf-8", "replace"))
1538
+ meter(request.url, sent, response.status_code, kind, response.content.decode("utf-8", "replace"), caller=caller)
1437
1539
  else:
1438
1540
  dep(request.url, response.status_code, passages=passages_from(response.content.decode("utf-8", "replace")))
1439
1541
  return response
1440
1542
  turn = turn_now()
1441
1543
  if model:
1442
- done = lambda raw: meter(request.url, sent, response.status_code, kind, unpacked(raw, response.headers.get("content-encoding")), turn)
1544
+ done = lambda raw: meter(request.url, sent, response.status_code, kind, unpacked(raw, response.headers.get("content-encoding")), turn, caller)
1443
1545
  else:
1444
1546
  done = lambda raw: dep(request.url, response.status_code, passages=passages_from(unpacked(raw, response.headers.get("content-encoding"))), turn=turn)
1445
1547
  stream = response.stream
@@ -1448,7 +1550,7 @@ def _install():
1448
1550
  elif hasattr(stream, "__iter__"):
1449
1551
  response.stream = SyncKept(stream, done)
1450
1552
  elif model:
1451
- meter(request.url, sent, response.status_code, kind, "")
1553
+ meter(request.url, sent, response.status_code, kind, "", caller=caller)
1452
1554
  else:
1453
1555
  dep(request.url, response.status_code)
1454
1556
  return response
@@ -1458,6 +1560,7 @@ def _install():
1458
1560
  if cls is module.Client:
1459
1561
  def sync_send(self, request, *a, _send=send, **k):
1460
1562
  sent = sent_of(request)
1563
+ caller = callers_now(request.url) if is_model_call(request.url) else None
1461
1564
  try:
1462
1565
  note(request.url, sent)
1463
1566
  except Exception:
@@ -1466,18 +1569,19 @@ def _install():
1466
1569
  response = _send(self, request, *a, **k)
1467
1570
  except Exception as err:
1468
1571
  if is_model_call(request.url):
1469
- meter(request.url, sent, 0, "", "")
1572
+ meter(request.url, sent, 0, "", "", caller=caller)
1470
1573
  else:
1471
1574
  dep(request.url, 0, type(err).__name__)
1472
1575
  raise
1473
1576
  try:
1474
- return watch(request, response, sent)
1577
+ return watch(request, response, sent, caller)
1475
1578
  except Exception:
1476
1579
  return response
1477
1580
  cls.send = sync_send
1478
1581
  else:
1479
1582
  async def async_send(self, request, *a, _send=send, **k):
1480
1583
  sent = sent_of(request)
1584
+ caller = callers_now(request.url) if is_model_call(request.url) else None
1481
1585
  try:
1482
1586
  note(request.url, sent)
1483
1587
  except Exception:
@@ -1486,12 +1590,12 @@ def _install():
1486
1590
  response = await _send(self, request, *a, **k)
1487
1591
  except Exception as err:
1488
1592
  if is_model_call(request.url):
1489
- meter(request.url, sent, 0, "", "")
1593
+ meter(request.url, sent, 0, "", "", caller=caller)
1490
1594
  else:
1491
1595
  dep(request.url, 0, type(err).__name__)
1492
1596
  raise
1493
1597
  try:
1494
- return watch(request, response, sent)
1598
+ return watch(request, response, sent, caller)
1495
1599
  except Exception:
1496
1600
  return response
1497
1601
  cls.send = async_send
@@ -1500,6 +1604,7 @@ def _install():
1500
1604
  send = module.Session.send
1501
1605
 
1502
1606
  def sent(self, request, *a, **k):
1607
+ caller = callers_now(request.url) if is_model_call(request.url) else None
1503
1608
  try:
1504
1609
  note(request.url, request.body)
1505
1610
  except Exception:
@@ -1517,7 +1622,7 @@ def _install():
1517
1622
  if is_model_call(request.url):
1518
1623
  # A streamed reply is counted as a call whose counts were not read.
1519
1624
  raw = response.content.decode("utf-8", "replace") if getattr(response, "_content_consumed", False) else ""
1520
- meter(request.url, text(request.body), response.status_code, response.headers.get("content-type", ""), raw)
1625
+ meter(request.url, text(request.body), response.status_code, response.headers.get("content-type", ""), raw, caller=caller)
1521
1626
  except Exception:
1522
1627
  pass
1523
1628
  return response
@@ -1540,7 +1645,7 @@ def _install():
1540
1645
  dep(str_or_url, 0, type(err).__name__)
1541
1646
  raise
1542
1647
  if is_model_call(str_or_url):
1543
- response._cortad = (str_or_url, text(body), turn_now())
1648
+ response._cortad = (str_or_url, text(body), turn_now(), callers_now(str_or_url))
1544
1649
  elif is_retrieval(str_or_url):
1545
1650
  response._cortad_dep = (str_or_url, turn_now())
1546
1651
  else:
@@ -1559,7 +1664,7 @@ def _install():
1559
1664
  call = getattr(self, "_cortad", None)
1560
1665
  if call:
1561
1666
  self._cortad = None
1562
- meter(call[0], call[1], self.status, self.headers.get("content-type", ""), (raw or b"").decode("utf-8", "replace"), call[2])
1667
+ meter(call[0], call[1], self.status, self.headers.get("content-type", ""), (raw or b"").decode("utf-8", "replace"), call[2], call[3])
1563
1668
  return raw
1564
1669
 
1565
1670
  def release_kept(self, *a, **k):
@@ -1570,7 +1675,7 @@ def _install():
1570
1675
  call = getattr(self, "_cortad", None)
1571
1676
  if call:
1572
1677
  self._cortad = None
1573
- meter(call[0], call[1], self.status, self.headers.get("content-type", ""), "", call[2])
1678
+ meter(call[0], call[1], self.status, self.headers.get("content-type", ""), "", call[2], call[3])
1574
1679
  return release(self, *a, **k)
1575
1680
 
1576
1681
  module.ClientSession._request = requested
package/lib/replay.mjs CHANGED
@@ -121,6 +121,9 @@ const calledOf = (v) => {
121
121
  return called.length ? { called } : {};
122
122
  };
123
123
  const passagesOf = (v) => (texts(v).length ? { passages: texts(v).slice(0, 6) } : {});
124
+ // Which of the app's own lines made the call, nearest first, as "path:line".
125
+ const AT = /^[^\n\r\t]{1,300}:\d{1,7}$/;
126
+ const callerOf = (v) => { const caller = Array.isArray(v) ? v.filter((c) => typeof c === "string" && AT.test(c)).slice(0, 3) : []; return caller.length ? { caller } : {}; };
124
127
  const meter = (() => {
125
128
  const rows = [];
126
129
  const deps = [];
@@ -131,7 +134,7 @@ const meter = (() => {
131
134
  rows.push({
132
135
  at: count(call.at), host: call.host.slice(0, 253), model: String(call.model ?? "").slice(0, 160), status: count(call.status),
133
136
  promptTokens: count(call.promptTokens), cachedTokens: count(call.cachedTokens), completionTokens: count(call.completionTokens),
134
- usage: call.usage === true, ...turnOf(call.turn), ...rulesOf(call.rules), ...toolsOf(call.tools), ...calledOf(call.called), ...passagesOf(call.passages),
137
+ usage: call.usage === true, ...turnOf(call.turn), ...rulesOf(call.rules), ...toolsOf(call.tools), ...calledOf(call.called), ...passagesOf(call.passages), ...callerOf(call.caller),
135
138
  });
136
139
  if (rows.length > ROWS) rows.splice(0, rows.length - ROWS);
137
140
  },
@@ -159,7 +162,7 @@ const meter = (() => {
159
162
  }
160
163
  return {
161
164
  totals: [...totals.values()].sort((a, b) => b.calls - a.calls),
162
- rows: rows.slice(-400).reverse().map(({ at, host, model, status, turn, rules, tools, called, passages }) => ({ at, host, model, status, ...(turn ? { turn } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages ? { passages } : {}) })),
165
+ rows: rows.slice(-400).reverse().map(({ at, host, model, status, turn, rules, tools, called, passages, caller }) => ({ at, host, model, status, ...(turn ? { turn } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages ? { passages } : {}), ...(caller ? { caller } : {}) })),
163
166
  deps: deps.slice(-400).reverse(),
164
167
  };
165
168
  },
package/lib/text.mjs CHANGED
@@ -159,13 +159,19 @@ const sideText = (m) => (m.rate ? rateText(m.rate) : m.unavailable ? null : move
159
159
  // Over zero pairs there is no move to print: the line says verification is unavailable and why,
160
160
  // never "no change, 0 points" measured on nothing. The visible side's reason is the said line.
161
161
  function verifyLines(v) {
162
- // An access verify is a re-knock, not a trial move: the routes side by side, then the count.
162
+ // An access verify is a re-knock, not a trial move: the routes side by side, then the count. Only
163
+ // a route knocked and answered both times is compared; one the app no longer has or did not answer
164
+ // is named as such, never read as a closed gate.
163
165
  if (v.access) {
164
166
  const lines = [`Verify of ${v.findingId}.`];
165
167
  for (const r of v.routes ?? []) {
166
- const then = r.wasOpen ? `answered ${r.before} with no sign-in` : `refused (${r.before || "no answer"})`;
167
- const now = r.nowOpen ? `answers ${r.after} with no sign-in` : r.after ? `refuses (${r.after})` : "did not answer";
168
- lines.push(` ${r.method} ${r.route}: ${then}, now ${now}.`);
168
+ const then = r.wasOpen ? `answered ${r.before} with no sign-in` : `refused (${r.before})`;
169
+ const now =
170
+ r.state === "answered" ? (r.nowOpen ? `now answers ${r.after} with no sign-in` : `now refuses (${r.after})`)
171
+ : r.state === "gone" ? "your app no longer has this route"
172
+ : r.state === "no-answer" ? "your app did not answer this time"
173
+ : "was not knocked again";
174
+ lines.push(` ${r.method} ${r.route}: ${then}, ${now}.`);
169
175
  }
170
176
  if (v.said) lines.push(v.said);
171
177
  return lines;
@@ -355,6 +361,9 @@ function findingBlock(f, placed) {
355
361
  // investigation itself.
356
362
  if (q.asked) lines.push(` Asked: "${clip(q.asked, 300)}"`);
357
363
  lines.push(` Reply ${q.reply}: "${clip(q.quote, 400)}"${about ? ` (${about})` : ""}`);
364
+ // What the reader checked the reply against: what their app retrieved for it, the start of each.
365
+ if (q.checked) lines.push(` ${q.checked}`);
366
+ for (const p of (q.passages ?? []).slice(0, 3)) lines.push(` Passage${p.source ? ` (${clip(p.source, 60)})` : ""}: "${clip(p.text, 240)}"`);
358
367
  // What a developer reproduces it from: the request as sent, the body back, the app's own log.
359
368
  if (q.request) lines.push(` Request: ${q.request.method} ${q.request.path}${q.request.body ? ` ${clip(q.request.body, 600)}` : ""}`);
360
369
  if (has(q.response)) lines.push(` Response: ${q.response ? clip(q.response, 600) : "(empty body)"}`);
package/lib/trace.cjs CHANGED
@@ -119,15 +119,33 @@ if (FILE) {
119
119
  .replace(/\*(\w+)/g, "{$1}");
120
120
  const joined = (a, b) => ("/" + [a, b].map((s) => String(s).replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/"));
121
121
  const nodePath = require("node:path");
122
+ // The frames of the stack it is called from, nearest first, outside this hook and node_modules,
123
+ // relative to the folder the app was started in. An awaited call keeps its awaiting frames in
124
+ // the stack, and an SDK is deeper than the default ten frames.
125
+ const stackFrames = () => {
126
+ const limit = Error.stackTraceLimit;
127
+ Error.stackTraceLimit = 60;
128
+ const stack = String(new Error().stack);
129
+ Error.stackTraceLimit = limit;
130
+ const out = [];
131
+ for (const line of stack.split("\n")) {
132
+ const m = /\(?(?:file:\/\/)?(\/[^()]+?):(\d+):\d+\)?$/.exec(line.trim());
133
+ if (m && m[1] !== __filename && !m[1].includes("/node_modules/")) out.push({ file: nodePath.relative(process.cwd(), decodeURI(m[1])), line: Number(m[2]) });
134
+ }
135
+ return out;
136
+ };
122
137
  // The file that registered a route: the first frame outside this hook and outside node_modules.
123
- const callerFile = () => {
124
- for (const line of String(new Error().stack).split("\n").slice(2)) {
125
- const m = /\(?(?:file:\/\/)?(\/[^()]+?):\d+:\d+\)?$/.exec(line.trim());
126
- if (!m || m[1] === __filename || m[1].includes("/node_modules/")) continue;
127
- const rel = nodePath.relative(process.cwd(), decodeURI(m[1]));
128
- return rel.startsWith("..") ? "" : rel;
138
+ const callerFile = () => { const f = stackFrames()[0]; return f && !f.file.startsWith("..") ? f.file : ""; };
139
+ // Which of the app's own lines made a model call, nearest first: the run names the code path that
140
+ // ran by it, where the read could only name the door.
141
+ const callers = () => {
142
+ const out = [];
143
+ for (const f of stackFrames()) {
144
+ const at = `${f.file}:${f.line}`;
145
+ if (!f.file.startsWith("..") && !out.includes(at)) out.push(at);
146
+ if (out.length === 3) break;
129
147
  }
130
- return "";
148
+ return out.length ? out : undefined;
131
149
  };
132
150
  const seen = new Set();
133
151
  let nest = false;
@@ -561,13 +579,36 @@ if (FILE) {
561
579
  // person's latest message on, and Anthropic document and search_result blocks. The app's own
562
580
  // instructions are not passages: only what sits under such a label is.
563
581
  const PASSAGE_TEXT = 3000, PASSAGES_MAX = 6;
564
- const MATERIAL = /\b(?:retriev\w*|context|knowledge|documents?|sources?|search[ _-]?results?|references?|passages?|excerpts?|snippets?|chunks?|background|faq)\b|检索|知识|参考资料|资料|上下文|文档|背景/i;
582
+ const MATERIAL = /\b(?:retriev\w*|context|knowledge|documents?|sources?|search[ _-]?results?|references?|passages?|excerpts?|snippets?|chunks?|background|faq|relevant)\b|检索|知识|参考资料|资料|上下文|文档|背景|相关/i;
565
583
  const HEADING = /^\s*(?:#{1,6}\s+[^\n]{1,80}|[^\n]{1,80}[::]\s*(?:\([^\n)]*\))?|\[[^\n\]]{1,80}\]|={2,}\s*[^\n]{1,80}?\s*={2,})\s*$/;
566
584
  // An inline label is a noun phrase that ends in the material word ("context:", "background:",
567
585
  // "retrieved passages:"): "4. Knowledge:" in a numbered instruction and a JSON key are not.
568
586
  const INLINE = /^\s*(?:[-*]\s+)?([A-Za-z\u4e00-\u9fff][A-Za-z\u4e00-\u9fff '’_-]{0,29})[::]\s*\S/;
569
587
  const NAMED = new RegExp(`(?:${MATERIAL.source})\\s*$`, "i");
570
588
  const TAGGED = /<([A-Za-z][\w-]*)[^>]*>([\s\S]*?)<\/\1>/g;
589
+ // A label's block runs on to the next heading of its own kind: "资料:" over retrieved chunks
590
+ // that open on markdown headings holds every chunk, where stopping at its first paragraph kept
591
+ // one chunk of five and the reader called the knowledge base's own 1% fee made up. A markdown
592
+ // label holds deeper headings. A closing paragraph that opens on a label of its own ("问题:...",
593
+ // "Question: ...") is the prompt's ask, not material. A long block is several passages, cut
594
+ // between its paragraphs.
595
+ const MARKDOWN = /^\s*(#{1,6})\s/;
596
+ const endsBlock = (label, line) => {
597
+ if (!HEADING.test(line)) return false;
598
+ const outer = MARKDOWN.exec(label), inner = MARKDOWN.exec(line);
599
+ return !inner || Boolean(outer && inner[1].length <= outer[1].length);
600
+ };
601
+ const asks = (para) => { const m = INLINE.exec(para.split("\n")[0]); return Boolean(m && !NAMED.test(m[1])); };
602
+ // A heading inside a fenced block is the material's own text: a README chunk's "## Features".
603
+ const fences = (para) => (para.match(/^\s*(?:```|~~~)/gm) || []).length;
604
+ const addBlock = (add, name, paras) => {
605
+ let text = "";
606
+ for (const p of paras) {
607
+ if (text && text.length + p.length + 2 > PASSAGE_TEXT) { add(name, text); text = ""; }
608
+ text += (text ? "\n\n" : "") + p;
609
+ }
610
+ add(name, text);
611
+ };
571
612
  const labelled = (text, add) => {
572
613
  const rest = String(text || "").replace(TAGGED, (all, tag, inner) => { if (MATERIAL.test(tag.replace(/_/g, " "))) { add(tag, inner); return ""; } return all; });
573
614
  const paras = rest.split(/\n\s*\n/);
@@ -575,10 +616,13 @@ if (FILE) {
575
616
  const lines = paras[i].split("\n");
576
617
  const head = lines[0];
577
618
  if (HEADING.test(head) && MATERIAL.test(head)) {
578
- let body = lines.slice(1).join("\n");
579
- // A heading alone on its paragraph labels what follows, up to the next heading.
580
- if (!body.trim()) while (i + 1 < paras.length && body.length < PASSAGE_TEXT && !HEADING.test(paras[i + 1].split("\n")[0])) body += (body ? "\n\n" : "") + paras[++i];
581
- add(head.replace(/[#::[\]=]/g, "").trim(), body);
619
+ const block = [lines.slice(1).join("\n")];
620
+ let fenced = fences(block[0]) % 2 === 1;
621
+ while (i + 1 < paras.length && (fenced || (!endsBlock(head, paras[i + 1].split("\n")[0]) && !(i + 2 === paras.length && asks(paras[i + 1]))))) {
622
+ block.push(paras[++i]);
623
+ if (fences(paras[i]) % 2 === 1) fenced = !fenced;
624
+ }
625
+ addBlock(add, head.replace(/[#::[\]=]/g, "").trim(), block);
582
626
  continue;
583
627
  }
584
628
  const inline = lines.map((l) => INLINE.exec(l)).find((m) => m && NAMED.test(m[1]));
@@ -636,6 +680,8 @@ if (FILE) {
636
680
  // Where models are served, by host, and the paths every OpenAI-shaped or vendor endpoint ends with.
637
681
  const MODEL_HOST = /(?:^|\.)(?:openai\.com|anthropic\.com|fireworks\.ai|openrouter\.ai|groq\.com|mistral\.ai|together\.xyz|together\.ai|deepseek\.com|cohere\.ai|cohere\.com|perplexity\.ai|x\.ai|googleapis\.com|openai\.azure\.com|cognitiveservices\.azure\.com|amazonaws\.com|replicate\.com|huggingface\.co|cerebras\.ai|deepinfra\.com|novita\.ai|moonshot\.cn|dashscope\.aliyuncs\.com|bigmodel\.cn|ai-gateway\.vercel\.sh|gateway\.ai\.cloudflare\.com|helicone\.ai|portkey\.ai)$/i;
638
682
  const MODEL_PATH = /\/(?:chat\/completions|completions|responses|messages|embeddings)$|:(?:generateContent|streamGenerateContent)|\/invoke(?:-with-response-stream)?$|\/api\/(?:chat|generate)$/i;
683
+ // An embedding call answers nobody, so where it was made names no path a reply came down.
684
+ const EMBEDDING = /\/embeddings$/i;
639
685
  const isModelCall = (host, path) => {
640
686
  const h = String(host || "").replace(/:\d+$/, "");
641
687
  const p = String(path || "").split("?")[0];
@@ -790,14 +836,14 @@ if (FILE) {
790
836
  const m = /\/models\/([^/:]+):|\/model\/([^/]+)\/(?:invoke|converse)/.exec(path || "");
791
837
  return m ? decodeURIComponent(m[1] || m[2]) : "";
792
838
  };
793
- const meter = ({ host, path, sent, status, type, body, turn }) => {
839
+ const meter = ({ host, path, sent, status, type, body, turn, caller }) => {
794
840
  try {
795
841
  const reply = readReply(type, String(body || "").slice(0, REPLY_MAX));
796
842
  const rules = rulesIn(sent);
797
843
  const tools = toolsIn(sent);
798
844
  const called = calledIn(sent, reply.events);
799
845
  const passages = passagesIn(sent);
800
- row({ call: { at: Date.now(), host: String(host).replace(/:443$/, ""), model: String(askedFor(path, sent) || reply.model || "").slice(0, 160), status, ...(reply.tokens || { promptTokens: 0, cachedTokens: 0, completionTokens: 0 }), usage: Boolean(reply.tokens), ...(turn ? { turn } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages ? { passages } : {}) } });
846
+ row({ call: { at: Date.now(), host: String(host).replace(/:443$/, ""), model: String(askedFor(path, sent) || reply.model || "").slice(0, 160), status, ...(reply.tokens || { promptTokens: 0, cachedTokens: 0, completionTokens: 0 }), usage: Boolean(reply.tokens), ...(turn ? { turn } : {}), ...(rules ? { rules } : {}), ...(tools ? { tools } : {}), ...(called ? { called } : {}), ...(passages ? { passages } : {}), ...(caller ? { caller } : {}) } });
801
847
  } catch { /* the command is gone */ }
802
848
  };
803
849
 
@@ -1014,7 +1060,7 @@ if (FILE) {
1014
1060
  }
1015
1061
  const sent = bodyText(init && init.body);
1016
1062
  note(als.getStore(), sent);
1017
- const call = { host: url.host, path: url.pathname, sent, turn: (als.getStore() || {}).turn };
1063
+ const call = { host: url.host, path: url.pathname, sent, turn: (als.getStore() || {}).turn, caller: EMBEDDING.test(url.pathname) ? undefined : callers() };
1018
1064
  return realFetch.apply(this, arguments).then((res) => {
1019
1065
  try {
1020
1066
  // A clone is a tee: your app's branch gets every byte as fast as it reads, this one is read to the end.
@@ -1043,7 +1089,8 @@ if (FILE) {
1043
1089
  const end = req.end;
1044
1090
  req.write = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk)); return write.call(this, chunk, ...r); };
1045
1091
  req.end = function (chunk, ...r) { if (chunk && typeof chunk !== "function") parts.push(Buffer.from(chunk)); sent = Buffer.concat(parts).toString("utf8"); note(ctx, sent); return end.call(this, chunk, ...r); };
1046
- const call = { host: String(host || ""), path: String(path || "").split("?")[0], turn: ctx && ctx.turn };
1092
+ const bare = String(path || "").split("?")[0];
1093
+ const call = { host: String(host || ""), path: bare, turn: ctx && ctx.turn, caller: EMBEDDING.test(bare) ? undefined : callers() };
1047
1094
  let done = false;
1048
1095
  const once = (row) => { if (!done) { done = true; meter({ ...call, sent, ...row }); } };
1049
1096
  req.once("error", () => once({ status: 0, type: "", body: "" }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortad",
3
- "version": "0.3.1-rc.8",
3
+ "version": "0.3.1-rc.9",
4
4
  "description": "Connects the AI app on your machine to Cortad for test conversations, and gives your coding agent the MCP and skill to run them. No dependencies.",
5
5
  "bin": {
6
6
  "cortad": "local.mjs"