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 +5 -1
- package/lib/pyhook/sitecustomize.py +15 -6
- package/lib/stores.mjs +2 -0
- package/lib/trace.cjs +2 -2
- package/lib/wire.cjs +8 -4
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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": [
|
|
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: [
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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"
|