cortad 0.3.1-rc.13 → 0.3.1-rc.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/proof.mjs CHANGED
@@ -547,8 +547,12 @@ const sentence = ({ model, site }) => (model ? " The model said it."
547
547
  // The tools the request's last model call asked for, when they ran inside this request and no model
548
548
  // call came after them: the app closed the request before the model could answer from their result
549
549
  // (an AI SDK route left at one step). A tool the page runs itself and sends back is not this.
550
+ // A last call that wrote words answered: its request carries the tool calls before it in its history
551
+ // (MIT Learn AI's answering call resent search_courses), which is not the request stopping there.
550
552
  function endedOnTool(ex) {
551
- const asked = [...new Set(arr(generation(ex).at(-1)?.called).map((c) => str(c?.name)).filter(Boolean))];
553
+ const last = generation(ex).at(-1);
554
+ if (str(last?.reply).trim()) return null;
555
+ const asked = [...new Set(arr(last?.called).map((c) => str(c?.name)).filter(Boolean))];
552
556
  if (!asked.length) return null;
553
557
  const inApp = new Set(ex.deps.flatMap((d) => arr(d.called).map((c) => str(c?.name))));
554
558
  const streamed = eventsOf(str(ex.reply)).events.some((e) => e && typeof e === "object" && /tool.*(?:output|result)/i.test(str(e.type)));
@@ -171,7 +171,14 @@ def _install():
171
171
  # What the app's tools answered, as the prompt of the next model call carries them (chat tool
172
172
  # messages, responses function outputs, Anthropic tool_result blocks, Gemini functionResponse
173
173
  # parts): the material a reply's facts rest on. Bounded per call.
174
- tool_text, tools_max = 3000, 12
174
+ # A tool's answer is kept to the cap the reader takes, and one longer says it was cut (wire.cjs toolText).
175
+ tool_text, tools_max = 12000, 12
176
+
177
+ def tool_text_row(name, full, **extra):
178
+ out = {"name": str(name or "")[:80], "text": full[:tool_text], **extra}
179
+ if len(full) > tool_text:
180
+ out["cut"] = True
181
+ return out
175
182
 
176
183
  def text_of(v):
177
184
  if isinstance(v, str):
@@ -214,9 +221,10 @@ def _install():
214
221
  out, names = [], {}
215
222
 
216
223
  def add(name, text):
217
- t = text_of(text)[:tool_text]
224
+ full = text_of(text)
225
+ t = full[:tool_text]
218
226
  if t.strip() and len(out) < tools_max and all(o["text"] != t for o in out):
219
- out.append({"name": str(name or "")[:80], "text": t})
227
+ out.append(tool_text_row(name, full))
220
228
 
221
229
  messages = items(body.get("messages"))
222
230
  for m in messages:
@@ -645,9 +653,10 @@ def _install():
645
653
  calls, names, answers = {}, {}, []
646
654
 
647
655
  def answer(name, said):
648
- t = text_of(said)[:tool_text]
656
+ full = text_of(said)
657
+ t = full[:tool_text]
649
658
  if name and t.strip() and len(answers) < tools_max and not any(a["name"] == name and a["text"] == t for a in answers):
650
- answers.append({"name": str(name)[:80], "text": t, "provider": True})
659
+ answers.append(tool_text_row(name, full, provider=True))
651
660
 
652
661
  for e in events:
653
662
  if not isinstance(e, dict):
@@ -1731,7 +1740,7 @@ def _install():
1731
1740
  text = "raised %s: %s" % (type(error).__name__, error) if error is not None else returned(value)
1732
1741
  row = {"at": int(time.time() * 1000), "host": "in-app", "status": 200,
1733
1742
  "called": [{"name": name, "arguments": args_text(plain_of(named))}],
1734
- "tools": [{"name": name, "text": str(text)[:tool_text]}]}
1743
+ "tools": [tool_text_row(name, str(text))]}
1735
1744
  row.update(inside_of(req))
1736
1745
  write({"dep": row})
1737
1746
  except Exception:
package/lib/stores.mjs CHANGED
@@ -126,6 +126,8 @@ function fromParts(values) {
126
126
  for (const [p, hostName] of hosts) {
127
127
  // A host setting left blank is a store the app is set up not to use (QDRANT_HOST= beside QDRANT_PATH).
128
128
  if (hostName && !String(values[hostName] ?? "").trim()) continue;
129
+ // A host setting that holds a whole address (REDIS_HOST=redis://...) is fromUrl's, not a host.
130
+ if (hostName && String(values[hostName]).includes("://")) continue;
129
131
  const parts = {};
130
132
  for (const name of Object.keys(values)) {
131
133
  if (!name.startsWith(p) || name === hostName) continue;
package/lib/trace.cjs CHANGED
@@ -17,7 +17,7 @@ if (FILE) {
17
17
  const http = require("node:http");
18
18
  const https = require("node:https");
19
19
  const als = new AsyncLocalStorage();
20
- const { MAX, REPLY_MAX, EMBEDDING, TOOL_TEXT, scrubbed, list, clipped, argsText, turnOf, isModelCall, isRetrieval, passagesFrom, decoded, cookieNames, makeRecorder } = require("./wire.cjs");
20
+ const { MAX, REPLY_MAX, EMBEDDING, TOOL_TEXT, toolText, scrubbed, list, clipped, argsText, turnOf, isModelCall, isRetrieval, passagesFrom, decoded, cookieNames, makeRecorder } = require("./wire.cjs");
21
21
  const row = (value) => { try { fs.appendFileSync(FILE, JSON.stringify(scrubbed(value)) + "\n", { mode: 0o600 }); } catch { /* the command is gone */ } };
22
22
  // What the app writes into its own folder while a run is on: each file copied once before its
23
23
  // first write, so the command puts every one back when the run ends (lib/writes.mjs). Trials
@@ -611,7 +611,7 @@ if (FILE) {
611
611
  args.forEach((a, i) => { named[IDENT.test(names[i] || "") ? names[i] : String(i)] = plainOf(a); });
612
612
  let text;
613
613
  try { text = error ? `raised ${(error && error.name) || "an error"}: ${(error && error.message) || String(error)}` : returned(value); } catch { text = "returned a value this hook cannot read"; }
614
- row({ dep: { at: Date.now(), host: "in-app", status: 200, ...(ctx ? { ex: ctx.id } : {}), ...(turn ? { turn } : {}), called: [{ name, arguments: argsText(named) }], tools: [{ name, text: String(text).slice(0, TOOL_TEXT) }] } });
614
+ row({ dep: { at: Date.now(), host: "in-app", status: 200, ...(ctx ? { ex: ctx.id } : {}), ...(turn ? { turn } : {}), called: [{ name, arguments: argsText(named) }], tools: [toolText(name, String(text))] } });
615
615
  };
616
616
  const TOOL = Symbol.for("cortad.wrapped");
617
617
  const wrappers = new WeakMap();
package/lib/wire.cjs CHANGED
@@ -55,7 +55,11 @@ const partsOf = (text) => text.split(/\{[^}]*\}|\$\{[^}]*\}|%[sd]|<[^>]{1,40}>/)
55
55
  // messages of a chat body, the function outputs of a responses body, the tool_result blocks of
56
56
  // an Anthropic one, the functionResponse parts of a Gemini one. They are the material a reply's
57
57
  // facts rest on, and without them a real ticket id read as invented. Bounded per call.
58
- const TOOL_TEXT = 3000, TOOLS_MAX = 12;
58
+ // A tool's answer is kept to the cap the reader takes (12,000 characters), and one longer says it was
59
+ // cut: MIT Learn AI's 8,000-character search results were kept at 3,000, and a podcast episode in
60
+ // the part cut off was graded as made up. A cut answer stands the passage questions down.
61
+ const TOOL_TEXT = 12000, TOOLS_MAX = 12;
62
+ const toolText = (name, full, extra = {}) => ({ name: String(name || "").slice(0, 80), text: full.slice(0, TOOL_TEXT), ...extra, ...(full.length > TOOL_TEXT ? { cut: true } : {}) });
59
63
  const textOf = (v) => (typeof v === "string" ? v : Array.isArray(v) ? v.map((p) => (p && typeof p === "object" ? (p.text || p.content || "") : String(p || ""))).filter(Boolean).join("\n") : v && typeof v === "object" ? JSON.stringify(v) : "");
60
64
  // Only what came after the person's latest message belongs to this turn: a thread the app
61
65
  // resends whole carries every earlier turn's tool answers too.
@@ -71,7 +75,7 @@ const toolsIn = (sent) => {
71
75
  if (!body || typeof body !== "object") return undefined;
72
76
  const out = [];
73
77
  const names = new Map();
74
- const add = (name, text) => { const t = textOf(text).slice(0, TOOL_TEXT); if (t.trim() && out.length < TOOLS_MAX && !out.some((o) => o.text === t)) out.push({ name: String(name || "").slice(0, 80), text: t }); };
78
+ const add = (name, text) => { const full = textOf(text); const t = full.slice(0, TOOL_TEXT); if (t.trim() && out.length < TOOLS_MAX && !out.some((o) => o.text === t)) out.push(toolText(name, full)); };
75
79
  for (const m of list(body.messages)) {
76
80
  for (const c of list(m.tool_calls)) if (c.id && c.function) names.set(c.id, c.function.name);
77
81
  for (const c of list(m.content)) if (c.type === "tool_use" && c.id) names.set(c.id, c.name);
@@ -323,7 +327,7 @@ const providerIn = (sent, events) => {
323
327
  const offered = list(body && body.tools).map((t) => t.type).filter((t) => typeof t === "string" && t !== "function" && t !== "custom");
324
328
  const named = (kind) => offered.find((t) => t.startsWith(kind)) || kind;
325
329
  const calls = new Map(), names = new Map(), answers = [];
326
- const answer = (name, text) => { const t = textOf(text).slice(0, TOOL_TEXT); if (name && t.trim() && answers.length < TOOLS_MAX && !answers.some((a) => a.name === name && a.text === t)) answers.push({ name: String(name).slice(0, 80), text: t, provider: true }); };
330
+ const answer = (name, text) => { const full = textOf(text); const t = full.slice(0, TOOL_TEXT); if (name && t.trim() && answers.length < TOOLS_MAX && !answers.some((a) => a.name === name && a.text === t)) answers.push(toolText(name, full, { provider: true })); };
327
331
  for (const e of events) {
328
332
  if (!e || typeof e !== "object") continue;
329
333
  const order = [];
@@ -813,6 +817,6 @@ function makeRecorder({ row, rulesFile, onTurn = () => {}, onEnded = () => {} })
813
817
 
814
818
  module.exports = {
815
819
  MAX, REPLY_MAX, EMBEDDING, TOOL_TEXT, MODEL_HOST,
816
- scrubbed, list, clipped, argsText, turnOf, isModelCall, isRetrieval, passagesFrom, decoded, cookieNames,
820
+ scrubbed, list, clipped, argsText, turnOf, toolText, isModelCall, isRetrieval, passagesFrom, decoded, cookieNames,
817
821
  makeRecorder,
818
822
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortad",
3
- "version": "0.3.1-rc.13",
3
+ "version": "0.3.1-rc.14",
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"