@inflexa-ai/harness 0.48.0 → 0.48.2
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/dist/contracts/chat-events.d.ts +40 -0
- package/dist/contracts/chat-frame.js +11 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/message.d.ts +7 -1
- package/dist/contracts/schemas/chat-events.d.ts +319 -0
- package/dist/contracts/schemas/chat-events.js +29 -0
- package/dist/execution/run-synthesis.js +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/input-sanitization.d.ts +30 -2
- package/dist/input-sanitization.js +187 -2
- package/dist/loop/run-loop.js +50 -5
- package/dist/loop/tool-adapter.d.ts +8 -0
- package/dist/loop/tool-adapter.js +75 -4
- package/dist/loop/tool-detail.d.ts +5 -1
- package/dist/loop/tool-detail.js +7 -3
- package/dist/loop/tool-value.d.ts +57 -0
- package/dist/loop/tool-value.js +249 -0
- package/dist/loop/types.d.ts +9 -2
- package/dist/memory/conversation-display-storage.js +4 -1
- package/dist/memory/count-tokens.js +29 -2
- package/dist/runtime/dbos.d.ts +6 -0
- package/dist/runtime/dbos.js +14 -2
- package/dist/sandbox/sandbox-step-translate.d.ts +5 -9
- package/dist/sandbox/sandbox-step-translate.js +5 -9
- package/dist/state/artifacts.d.ts +4 -1
- package/dist/state/artifacts.js +4 -1
- package/dist/state/index.d.ts +2 -2
- package/dist/state/index.js +1 -1
- package/dist/state/plans.d.ts +27 -0
- package/dist/state/plans.js +57 -3
- package/dist/tasks/data-profile-activity.d.ts +3 -7
- package/dist/tasks/data-profile-activity.js +1 -1
- package/dist/tasks/data-profile.js +3 -6
- package/dist/tools/ad-hoc-router.js +1 -0
- package/dist/tools/bio/alphafold-prediction.js +1 -0
- package/dist/tools/bio/chembl.js +1 -0
- package/dist/tools/bio/comptox.js +1 -0
- package/dist/tools/bio/drug-gene-interactions.js +1 -0
- package/dist/tools/bio/gene-disease-evidence.js +1 -0
- package/dist/tools/bio/gene-preclinical-profile.js +1 -0
- package/dist/tools/bio/lookup-annotation.js +1 -0
- package/dist/tools/bio/opentargets.js +1 -0
- package/dist/tools/bio/pubchem.js +1 -0
- package/dist/tools/bio/pubmed.js +1 -0
- package/dist/tools/bio/search-clinical-trials.js +1 -0
- package/dist/tools/bio/search-faers.js +1 -0
- package/dist/tools/bio/search-gene.js +1 -0
- package/dist/tools/bio/search-geo-datasets.js +1 -0
- package/dist/tools/bio/search-interactions.js +1 -0
- package/dist/tools/bio/search-patents.js +1 -0
- package/dist/tools/bio/search-protein.js +1 -0
- package/dist/tools/bio/search-regulatory-approvals.js +1 -0
- package/dist/tools/bio/target-safety.js +1 -0
- package/dist/tools/capability-data.js +5 -0
- package/dist/tools/define-tool.d.ts +18 -0
- package/dist/tools/define-tool.js +2 -0
- package/dist/tools/display/show-user.js +1 -0
- package/dist/tools/execute-analysis.js +4 -0
- package/dist/tools/knowledge/check.d.ts +47 -1
- package/dist/tools/knowledge/check.js +10 -1
- package/dist/tools/knowledge/client.d.ts +1 -1
- package/dist/tools/knowledge/recommend.js +4 -3
- package/dist/tools/knowledge/situation.d.ts +39 -1
- package/dist/tools/knowledge/situation.js +80 -8
- package/dist/tools/knowledge/skeleton.d.ts +7 -0
- package/dist/tools/knowledge/skeleton.js +88 -2
- package/dist/tools/knowledge/template.js +1 -0
- package/dist/tools/memory/update-working-memory.js +5 -0
- package/dist/tools/read-tool-output.js +4 -0
- package/dist/tools/report-authoring/authoring-tools.js +8 -0
- package/dist/tools/report-session/examine-page.js +5 -0
- package/dist/tools/report-session/list-artifacts.js +5 -0
- package/dist/tools/report-session/preview-report.js +1 -0
- package/dist/tools/report-session/record-version.js +1 -0
- package/dist/tools/report-session/script-tools.js +2 -0
- package/dist/tools/research/context7-docs.js +2 -0
- package/dist/tools/research/generate-analogy-report.js +3 -0
- package/dist/tools/research/generate-plan.js +6 -0
- package/dist/tools/research/inspect-data-profile.js +5 -0
- package/dist/tools/research/inspect-run.js +5 -0
- package/dist/tools/research/resolve-citation.js +1 -0
- package/dist/tools/research/search-arxiv.js +1 -0
- package/dist/tools/research/search-github-repos.js +1 -0
- package/dist/tools/research/search-semantic-scholar.js +1 -0
- package/dist/tools/research/web-fetch.js +1 -0
- package/dist/tools/research/web-search.js +1 -0
- package/dist/tools/sandbox/lease-tools.js +2 -0
- package/dist/tools/sandbox/link-packages.js +1 -0
- package/dist/tools/sandbox/list-available-packages.js +5 -0
- package/dist/tools/sandbox/list-available-refs.js +5 -0
- package/dist/tools/sandbox/report-blocker.js +1 -0
- package/dist/tools/sandbox/skills.js +2 -0
- package/dist/tools/sandbox/submit-file-metadata.js +1 -0
- package/dist/tools/script/file-tools.js +6 -0
- package/dist/tools/script/run-script.js +20 -1
- package/dist/tools/start-report-session.js +1 -0
- package/dist/tools/tool-view.d.ts +63 -0
- package/dist/tools/tool-view.js +68 -0
- package/dist/tools/workspace/edit-file.js +5 -0
- package/dist/tools/workspace/execute-command.js +5 -2
- package/dist/tools/workspace/file-stat.js +5 -0
- package/dist/tools/workspace/grep.js +4 -0
- package/dist/tools/workspace/list-files.js +5 -0
- package/dist/tools/workspace/read-file.js +4 -0
- package/dist/tools/workspace/scan-inputs.js +5 -0
- package/dist/tools/workspace/show-file.js +1 -0
- package/dist/tools/workspace/show-plan.js +1 -0
- package/dist/tools/workspace/workspace-search.js +5 -0
- package/dist/tools/workspace/write-file.js +7 -2
- package/dist/workflows/execute-analysis.js +2 -1
- package/dist/workflows/sandbox-step.js +3 -7
- package/package.json +1 -1
- package/dist/tools/detail-resolver.d.ts +0 -40
- package/dist/tools/detail-resolver.js +0 -44
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shown values of a tool call: its input, its result, and its error.
|
|
3
|
+
*
|
|
4
|
+
* The emit site makes each value one time, and a host only shows it. A tool declares what it shows through its view
|
|
5
|
+
* (`tools/tool-view.ts`), and {@link makeToolCallValue} turns the output of a view member into a value with a kind.
|
|
6
|
+
* Each leaf of a value gets the steps below on its own. The steps run in this order, because each step
|
|
7
|
+
* protects the next one: the control characters go first, thus a control byte inside a secret cannot stop a pattern.
|
|
8
|
+
* The secrets go before the cut, thus a cut cannot divide a secret and let one half pass. The shown text can differ
|
|
9
|
+
* from the text that the model reads.
|
|
10
|
+
*/
|
|
11
|
+
import { err, ok } from "neverthrow";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { TOOL_CALL_FIELDS_MAX, ToolCallValueSchema } from "../contracts/schemas/chat-events.js";
|
|
14
|
+
import { isUnknownArray } from "../lib/unknown-array.js";
|
|
15
|
+
import { parseJsonAs } from "../lib/parse-json.js";
|
|
16
|
+
import { describeZodIssueShapes } from "../lib/zod-issue-shape.js";
|
|
17
|
+
import { parseUnknownRecord } from "../lib/unknown-containers.js";
|
|
18
|
+
import { normalizeUnicode, redactSecretFields, redactSecretForms, redactSecrets } from "../input-sanitization.js";
|
|
19
|
+
import { ViewCode, ViewFields, ViewJson, ViewText } from "../tools/tool-view.js";
|
|
20
|
+
import { pairSafeIndex } from "./tool-output.js";
|
|
21
|
+
/** A text longer than this is cut. The unit is the UTF-16 code unit. */
|
|
22
|
+
export const TOOL_VALUE_CAP = 8 * 1024;
|
|
23
|
+
/** The start of a cut text holds the shape of a value. */
|
|
24
|
+
export const TOOL_VALUE_HEAD_CHARS = 3 * 1024;
|
|
25
|
+
/** The end of a cut text holds the errors of a command, thus it gets more room than the start. */
|
|
26
|
+
export const TOOL_VALUE_TAIL_CHARS = 5 * 1024;
|
|
27
|
+
/**
|
|
28
|
+
* The text past each shown part that the rules also read. A secret at the edge of a shown part is whole in its window
|
|
29
|
+
* when it is shorter than this margin, thus the rules see it as they see it in the whole text.
|
|
30
|
+
*/
|
|
31
|
+
export const TOOL_VALUE_CLEAN_MARGIN = 8 * 1024;
|
|
32
|
+
const HEAD_WINDOW = TOOL_VALUE_HEAD_CHARS + TOOL_VALUE_CLEAN_MARGIN;
|
|
33
|
+
const TAIL_WINDOW = TOOL_VALUE_TAIL_CHARS + TOOL_VALUE_CLEAN_MARGIN;
|
|
34
|
+
/**
|
|
35
|
+
* A text up to this length gets the rules whole. A longer text gets them on its two windows only, thus the cost of a
|
|
36
|
+
* value does not grow with the size of a result.
|
|
37
|
+
*/
|
|
38
|
+
const WHOLE_CLEAN_MAX = HEAD_WINDOW + TAIL_WINDOW;
|
|
39
|
+
function cleanText(text) {
|
|
40
|
+
return redactSecrets(redactSecretForms(normalizeUnicode(text)));
|
|
41
|
+
}
|
|
42
|
+
function cleanStrings(value) {
|
|
43
|
+
if (typeof value === "string")
|
|
44
|
+
return cleanText(value);
|
|
45
|
+
if (isUnknownArray(value))
|
|
46
|
+
return value.map(cleanStrings);
|
|
47
|
+
const record = parseUnknownRecord(value);
|
|
48
|
+
if (record === undefined)
|
|
49
|
+
return value;
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [name, field] of Object.entries(record))
|
|
52
|
+
out[cleanText(name)] = cleanStrings(field);
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** The shown start of `head` and the shown end of `tail`, with a marker that counts the code units of `length` not shown. */
|
|
56
|
+
function cutParts(head, tail, length) {
|
|
57
|
+
const shownHead = head.slice(0, pairSafeIndex(head, TOOL_VALUE_HEAD_CHARS, -1));
|
|
58
|
+
const shownTail = tail.slice(pairSafeIndex(tail, Math.max(0, tail.length - TOOL_VALUE_TAIL_CHARS), 1));
|
|
59
|
+
const marker = `[... ${Math.max(0, length - shownHead.length - shownTail.length)} characters not shown ...]`;
|
|
60
|
+
return { text: `${shownHead}\n${marker}\n${shownTail}`, cut: true };
|
|
61
|
+
}
|
|
62
|
+
function cutText(clean) {
|
|
63
|
+
return clean.length <= TOOL_VALUE_CAP ? { text: clean, cut: false } : cutParts(clean, clean, clean.length);
|
|
64
|
+
}
|
|
65
|
+
/** The shown text of a plain text. A long text gets the rules on its two windows, and its marker counts the original text. */
|
|
66
|
+
function shownText(text) {
|
|
67
|
+
if (text.length <= WHOLE_CLEAN_MAX)
|
|
68
|
+
return cutText(cleanText(text));
|
|
69
|
+
const head = cleanText(text.slice(0, pairSafeIndex(text, HEAD_WINDOW, -1)));
|
|
70
|
+
const tail = cleanText(text.slice(pairSafeIndex(text, text.length - TAIL_WINDOW, 1)));
|
|
71
|
+
return cutParts(head, tail, text.length);
|
|
72
|
+
}
|
|
73
|
+
/** The JSON text of a value, or `undefined` for a function, a symbol, and `undefined`, which the lib type omits. */
|
|
74
|
+
function jsonText(value) {
|
|
75
|
+
return JSON.stringify(value);
|
|
76
|
+
}
|
|
77
|
+
function serializeClean(value) {
|
|
78
|
+
return jsonText(cleanStrings(redactSecretFields(value)) ?? null) ?? "null";
|
|
79
|
+
}
|
|
80
|
+
/** The fields of a plain record whose JSON text is its own fields, or `undefined` for each other value. */
|
|
81
|
+
function plainFields(value) {
|
|
82
|
+
if (value === null || value === undefined)
|
|
83
|
+
return undefined;
|
|
84
|
+
const prototype = Object.getPrototypeOf(value);
|
|
85
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
86
|
+
return undefined;
|
|
87
|
+
const fields = Object.entries(value);
|
|
88
|
+
return fields.some(([name, field]) => name === "toJSON" && typeof field === "function") ? undefined : fields;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A copy of one end of `value`: the JSON text of the copy starts (`head`) or ends (`tail`) with at least `budget.left`
|
|
92
|
+
* code units of the JSON text of `value`. The count of a string ignores its escapes, and an escape only makes the text
|
|
93
|
+
* longer, thus the copy never holds less than the budget. A value that is not an array, a plain record, or a string
|
|
94
|
+
* stays whole, because its JSON text comes from its own `toJSON`.
|
|
95
|
+
*/
|
|
96
|
+
function endOf(value, budget, end) {
|
|
97
|
+
if (typeof value === "string") {
|
|
98
|
+
const keep = Math.max(0, budget.left);
|
|
99
|
+
const kept = end === "head" ? value.slice(0, keep) : value.slice(Math.max(0, value.length - keep));
|
|
100
|
+
budget.left -= kept.length + 2;
|
|
101
|
+
return kept;
|
|
102
|
+
}
|
|
103
|
+
if (isUnknownArray(value)) {
|
|
104
|
+
budget.left -= 2;
|
|
105
|
+
const kept = [];
|
|
106
|
+
for (let index = 0; index < value.length && budget.left > 0; index += 1) {
|
|
107
|
+
kept.push(endOf(value[end === "head" ? index : value.length - 1 - index], budget, end));
|
|
108
|
+
budget.left -= 1;
|
|
109
|
+
}
|
|
110
|
+
return end === "head" ? kept : kept.reverse();
|
|
111
|
+
}
|
|
112
|
+
const fields = plainFields(value);
|
|
113
|
+
if (fields !== undefined) {
|
|
114
|
+
budget.left -= 2;
|
|
115
|
+
const kept = [];
|
|
116
|
+
for (let index = 0; index < fields.length && budget.left > 0; index += 1) {
|
|
117
|
+
const entry = fields[end === "head" ? index : fields.length - 1 - index];
|
|
118
|
+
// JSON text omits such a field, and a test of the field by its own serialization would read all of it.
|
|
119
|
+
if (entry === undefined || entry[1] === undefined || typeof entry[1] === "function" || typeof entry[1] === "symbol")
|
|
120
|
+
continue;
|
|
121
|
+
budget.left -= entry[0].length + 4;
|
|
122
|
+
kept.push([entry[0], endOf(entry[1], budget, end)]);
|
|
123
|
+
}
|
|
124
|
+
return Object.fromEntries(end === "head" ? kept : kept.reverse());
|
|
125
|
+
}
|
|
126
|
+
budget.left -= jsonText(value)?.length ?? 0;
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The shown JSON text of a value: `ok(undefined)` when the value has no JSON text, and an error with the reason when
|
|
131
|
+
* the value does not serialize, for example a cycle.
|
|
132
|
+
*/
|
|
133
|
+
function shownJson(value) {
|
|
134
|
+
try {
|
|
135
|
+
const raw = jsonText(value);
|
|
136
|
+
if (raw === undefined)
|
|
137
|
+
return ok(undefined);
|
|
138
|
+
if (raw.length <= WHOLE_CLEAN_MAX)
|
|
139
|
+
return ok(cutText(serializeClean(value)));
|
|
140
|
+
const head = serializeClean(endOf(value, { left: HEAD_WINDOW }, "head"));
|
|
141
|
+
const tail = serializeClean(endOf(value, { left: TAIL_WINDOW }, "tail"));
|
|
142
|
+
return ok(cutParts(head, tail, raw.length));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return err("the value does not serialize to JSON");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function jsonLeaf(value) {
|
|
149
|
+
return shownJson(value).map((shown) => (shown === undefined ? undefined : { kind: "json", ...shown }));
|
|
150
|
+
}
|
|
151
|
+
/** The leaf of a plain text: control characters out, each secret out, then the cut. */
|
|
152
|
+
export function textToolValue(text) {
|
|
153
|
+
return { kind: "text", ...shownText(text) };
|
|
154
|
+
}
|
|
155
|
+
/** The leaf of source code. The rules and the cut are those of {@link textToolValue}. `language` is checked with the whole value. */
|
|
156
|
+
export function codeToolValue(language, text) {
|
|
157
|
+
return { kind: "code", language, ...shownText(text) };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The leaf of a JSON value. A string shows as a JSON string, and each other value shows as its JSON text.
|
|
161
|
+
* The field-name rule runs first, then the text rules on each string, then the serialization, then the cut: a text
|
|
162
|
+
* rule never meets a JSON escape, which would hide the end of a quoted secret.
|
|
163
|
+
* A long JSON text gets the rules on a copy of each end of the value only, and its marker counts the JSON text of the
|
|
164
|
+
* original value. Only the serialization of the whole value grows with its size, and the model path pays it already.
|
|
165
|
+
* A value with no JSON text, and a value that does not serialize, for example a cycle, give no leaf: a value is a
|
|
166
|
+
* diagnostic and never fails a call.
|
|
167
|
+
*/
|
|
168
|
+
export function jsonToolValue(value) {
|
|
169
|
+
return jsonLeaf(value).unwrapOr(undefined);
|
|
170
|
+
}
|
|
171
|
+
const AdapterErrorSchema = z.object({ error: z.string() });
|
|
172
|
+
/**
|
|
173
|
+
* The leaf of an error text. The adapter writes the error of a tool as the JSON text `{ "error": message, ... }`, and
|
|
174
|
+
* the leaf shows the message. Each other error text, for example a refusal of the call policy, shows whole.
|
|
175
|
+
*/
|
|
176
|
+
export function errorToolValue(text) {
|
|
177
|
+
try {
|
|
178
|
+
return textToolValue(parseJsonAs(text, AdapterErrorSchema).error);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return textToolValue(text);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function leafValue(leaf) {
|
|
185
|
+
if (leaf instanceof ViewText)
|
|
186
|
+
return ok(textToolValue(leaf.text));
|
|
187
|
+
if (leaf instanceof ViewCode)
|
|
188
|
+
return ok(codeToolValue(leaf.language, leaf.text));
|
|
189
|
+
return jsonLeaf(leaf.value);
|
|
190
|
+
}
|
|
191
|
+
function fieldsValue(view) {
|
|
192
|
+
if (view.fields.length > TOOL_CALL_FIELDS_MAX)
|
|
193
|
+
return err(`a fields value holds more than ${TOOL_CALL_FIELDS_MAX} fields`);
|
|
194
|
+
const fields = [];
|
|
195
|
+
for (const { name, leaf } of view.fields) {
|
|
196
|
+
// A leaf that is not a leaf class is a nested `fields` value, which the kinds refuse.
|
|
197
|
+
if (!(leaf instanceof ViewText || leaf instanceof ViewCode || leaf instanceof ViewJson))
|
|
198
|
+
return err("a fields value holds a fields value");
|
|
199
|
+
const value = leafValue(leaf);
|
|
200
|
+
if (value.isErr())
|
|
201
|
+
return err(`the field ${name.slice(0, 40)}: ${value.error}`);
|
|
202
|
+
if (value.value !== undefined)
|
|
203
|
+
fields.push({ name, ...value.value });
|
|
204
|
+
}
|
|
205
|
+
return ok(fields.length === 0 ? undefined : { kind: "fields", fields });
|
|
206
|
+
}
|
|
207
|
+
function leafOfOutput(output) {
|
|
208
|
+
if (typeof output === "string")
|
|
209
|
+
return ok(textToolValue(output));
|
|
210
|
+
if (output instanceof ViewText || output instanceof ViewCode || output instanceof ViewJson)
|
|
211
|
+
return leafValue(output);
|
|
212
|
+
return jsonLeaf(output).andThen((leaf) => (leaf === undefined ? err("the value has no JSON text") : ok(leaf)));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The value of the output of a view member: a helper object gives its kind, a string gives a `text` leaf, and each other
|
|
216
|
+
* value gives a `json` leaf. Each leaf gets the cleaning and the cap of its kind. The result holds no value for an output
|
|
217
|
+
* that is absent (silent), and for a `fields` value with no field. It holds the reason for an output that cannot show: a
|
|
218
|
+
* value with no JSON text, a value that does not serialize, and a value that breaks a rule of the kinds (more fields than
|
|
219
|
+
* the limit, a nested `fields` value, a bad field name, a bad language). The caller logs the reason. It never throws.
|
|
220
|
+
*/
|
|
221
|
+
export function makeToolCallValue(output) {
|
|
222
|
+
if (output === undefined)
|
|
223
|
+
return ok(undefined);
|
|
224
|
+
const made = output instanceof ViewFields ? fieldsValue(output) : leafOfOutput(output);
|
|
225
|
+
return made.andThen((value) => {
|
|
226
|
+
if (value === undefined)
|
|
227
|
+
return ok(undefined);
|
|
228
|
+
const checked = ToolCallValueSchema.safeParse(value);
|
|
229
|
+
return checked.success ? ok(checked.data) : err(describeZodIssueShapes(checked.error, ToolCallValueSchema).join("; "));
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The event without the three values. A forwarder of loop events to the stream of a run calls it, because no reader
|
|
234
|
+
* of that stream shows a value, and a value costs bytes on each event.
|
|
235
|
+
*/
|
|
236
|
+
export function withoutToolValues(event) {
|
|
237
|
+
switch (event.type) {
|
|
238
|
+
case "tool-started": {
|
|
239
|
+
const { input: _input, ...rest } = event;
|
|
240
|
+
return rest;
|
|
241
|
+
}
|
|
242
|
+
case "tool-finished": {
|
|
243
|
+
const { input: _input, result: _result, error: _error, ...rest } = event;
|
|
244
|
+
return rest;
|
|
245
|
+
}
|
|
246
|
+
default:
|
|
247
|
+
return event;
|
|
248
|
+
}
|
|
249
|
+
}
|
package/dist/loop/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* messages, and the flat event sink.
|
|
4
4
|
*/
|
|
5
5
|
import type { ModelMessage } from "ai";
|
|
6
|
-
import type { ToolCallDetail, ToolOutcome } from "../contracts/chat-events.js";
|
|
6
|
+
import type { ToolCallDetail, ToolCallValue, ToolOutcome } from "../contracts/chat-events.js";
|
|
7
7
|
import type { ChatStreamEvent, ProviderOptions } from "../providers/types.js";
|
|
8
8
|
import type { Tool } from "../tools/define-tool.js";
|
|
9
9
|
import type { CompactionRules } from "./compaction.js";
|
|
@@ -75,13 +75,14 @@ export type EmitEvent = {
|
|
|
75
75
|
readonly source: EventSource;
|
|
76
76
|
readonly toolUseId: string;
|
|
77
77
|
readonly name: string;
|
|
78
|
-
readonly input: unknown;
|
|
79
78
|
/**
|
|
80
79
|
* One line naming what this call is doing, from the tool's own
|
|
81
80
|
* `describeCall` hook. Absent — never empty — when the tool declares no
|
|
82
81
|
* hook or the detail could not be produced (see `tool-detail.ts`).
|
|
83
82
|
*/
|
|
84
83
|
readonly detail?: ToolCallDetail;
|
|
84
|
+
/** The input that the view of the tool declares, with each listed secret form removed and a cap. Absent for a call that is not a root call. */
|
|
85
|
+
readonly input?: ToolCallValue;
|
|
85
86
|
} | {
|
|
86
87
|
readonly type: "tool-finished";
|
|
87
88
|
readonly source: EventSource;
|
|
@@ -101,6 +102,12 @@ export type EmitEvent = {
|
|
|
101
102
|
* can fall back to its own observation.
|
|
102
103
|
*/
|
|
103
104
|
readonly durationMs?: number;
|
|
105
|
+
/** The input again: a replay of a tool step does not give its `tool-started`. */
|
|
106
|
+
readonly input?: ToolCallValue;
|
|
107
|
+
/** The result of a call with the outcome `ok`. */
|
|
108
|
+
readonly result?: ToolCallValue;
|
|
109
|
+
/** The error text of a call with the outcome `error`. */
|
|
110
|
+
readonly error?: ToolCallValue;
|
|
104
111
|
} | {
|
|
105
112
|
/** The display recorder of a chat turn emits it, not the loop. See `MessageStartEvent`. */
|
|
106
113
|
readonly type: "message-start";
|
|
@@ -20,7 +20,7 @@ import { isDataUIPart, validateUIMessages } from "ai";
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { parseUnknownRecord } from "../lib/unknown-containers.js";
|
|
22
22
|
import { AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, PlanApprovalPartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, SubAgentActivityEntrySchema, SubAgentActivityPartSchema, } from "../contracts/schemas/chat-parts.js";
|
|
23
|
-
import { ToolCallOutcomeSchema } from "../contracts/schemas/chat-events.js";
|
|
23
|
+
import { ToolCallOutcomeSchema, ToolCallValueSchema } from "../contracts/schemas/chat-events.js";
|
|
24
24
|
export const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
|
|
25
25
|
const DisplayEnvelopeHeaderSchema = z.object({
|
|
26
26
|
kind: z.literal("ai-sdk-ui-messages"),
|
|
@@ -48,6 +48,9 @@ const ToolCallDisplaySchema = z.object({
|
|
|
48
48
|
outcome: ToolCallOutcomeSchema,
|
|
49
49
|
detail: z.string().optional(),
|
|
50
50
|
durationMs: z.number().optional(),
|
|
51
|
+
input: ToolCallValueSchema.optional(),
|
|
52
|
+
result: ToolCallValueSchema.optional(),
|
|
53
|
+
error: ToolCallValueSchema.optional(),
|
|
51
54
|
});
|
|
52
55
|
/**
|
|
53
56
|
* The schema behind each stored part key.
|
|
@@ -16,6 +16,33 @@ function enc() {
|
|
|
16
16
|
encoder ??= getEncoding("cl100k_base");
|
|
17
17
|
return encoder;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* A run of this many characters with no whitespace is counted in chunks. The BPE merge of `js-tiktoken` is
|
|
21
|
+
* quadratic in the length of one pre-token, and a run of one repeated character of 20,000 characters takes
|
|
22
|
+
* about 13 s whole. A base64 blob or a sequence with no line break is such a run in a tool result.
|
|
23
|
+
*/
|
|
24
|
+
// oxlint-disable-next-line eslint-js/no-restricted-syntax -- a run of 256 or more characters with no whitespace; no builtin finds a run by its length.
|
|
25
|
+
const LONG_RUN = /\S{256,}/g;
|
|
26
|
+
/** The chunk of a long run. A chunk of 64 characters costs well under a millisecond in the worst case. */
|
|
27
|
+
const RUN_CHUNK = 64;
|
|
28
|
+
/**
|
|
29
|
+
* The token count of one text. A text with no long run is one encoding, thus its count is exact. Each long run
|
|
30
|
+
* is counted in chunks, which can add a token at each chunk edge: the count is an approximation already.
|
|
31
|
+
*/
|
|
32
|
+
function encodedLength(text) {
|
|
33
|
+
let total = 0;
|
|
34
|
+
let from = 0;
|
|
35
|
+
for (const run of text.matchAll(LONG_RUN)) {
|
|
36
|
+
if (run.index > from)
|
|
37
|
+
total += enc().encode(text.slice(from, run.index)).length;
|
|
38
|
+
for (let at = 0; at < run[0].length; at += RUN_CHUNK)
|
|
39
|
+
total += enc().encode(run[0].slice(at, at + RUN_CHUNK)).length;
|
|
40
|
+
from = run.index + run[0].length;
|
|
41
|
+
}
|
|
42
|
+
if (from < text.length)
|
|
43
|
+
total += enc().encode(text.slice(from)).length;
|
|
44
|
+
return total;
|
|
45
|
+
}
|
|
19
46
|
/**
|
|
20
47
|
* The token-bearing text of one tool-result output.
|
|
21
48
|
*
|
|
@@ -82,13 +109,13 @@ function tokenizableText(block) {
|
|
|
82
109
|
*/
|
|
83
110
|
export function countTokens(content) {
|
|
84
111
|
if (typeof content === "string") {
|
|
85
|
-
return content.length === 0 ? 0 :
|
|
112
|
+
return content.length === 0 ? 0 : encodedLength(content);
|
|
86
113
|
}
|
|
87
114
|
let total = 0;
|
|
88
115
|
for (const block of content) {
|
|
89
116
|
const text = tokenizableText(block);
|
|
90
117
|
if (text.length > 0)
|
|
91
|
-
total +=
|
|
118
|
+
total += encodedLength(text);
|
|
92
119
|
}
|
|
93
120
|
return total;
|
|
94
121
|
}
|
package/dist/runtime/dbos.d.ts
CHANGED
|
@@ -98,6 +98,12 @@ export type WorkflowSchedule = {
|
|
|
98
98
|
* schedule of this list. A second call with the same name replaces the first.
|
|
99
99
|
*/
|
|
100
100
|
export declare function scheduleWorkflow(schedule: WorkflowSchedule): void;
|
|
101
|
+
/**
|
|
102
|
+
* Store each schedule of {@link scheduleWorkflow} in the system database, and delete each stored schedule of this
|
|
103
|
+
* application that no code registers. DBOS only upserts, thus a schedule of a removed workflow fires on each tick and
|
|
104
|
+
* finds no function. Each schedule of the harness goes through `scheduleWorkflow`.
|
|
105
|
+
*/
|
|
106
|
+
export declare function applyWorkflowSchedules(): Promise<void>;
|
|
101
107
|
/**
|
|
102
108
|
* Launch DBOS. Idempotent — a second call is a no-op so tests that drive
|
|
103
109
|
* the harness twice (or accidentally double-import) don't re-launch.
|
package/dist/runtime/dbos.js
CHANGED
|
@@ -130,6 +130,19 @@ const pendingSchedules = new Map();
|
|
|
130
130
|
export function scheduleWorkflow(schedule) {
|
|
131
131
|
pendingSchedules.set(schedule.scheduleName, schedule);
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Store each schedule of {@link scheduleWorkflow} in the system database, and delete each stored schedule of this
|
|
135
|
+
* application that no code registers. DBOS only upserts, thus a schedule of a removed workflow fires on each tick and
|
|
136
|
+
* finds no function. Each schedule of the harness goes through `scheduleWorkflow`.
|
|
137
|
+
*/
|
|
138
|
+
export async function applyWorkflowSchedules() {
|
|
139
|
+
if (pendingSchedules.size > 0)
|
|
140
|
+
await DBOS.applySchedules([...pendingSchedules.values()]);
|
|
141
|
+
for (const stored of await DBOS.listSchedules()) {
|
|
142
|
+
if (!pendingSchedules.has(stored.scheduleName))
|
|
143
|
+
await DBOS.deleteSchedule(stored.scheduleName);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
133
146
|
let resultRecipesRegistered = false;
|
|
134
147
|
/**
|
|
135
148
|
* DBOS refuses a registration after the launch, and the registry of SuperJSON
|
|
@@ -160,8 +173,7 @@ export async function launchDbos({ config, logger: injected }) {
|
|
|
160
173
|
registerResultSerialization();
|
|
161
174
|
const start = performance.now();
|
|
162
175
|
await DBOS.launch();
|
|
163
|
-
|
|
164
|
-
await DBOS.applySchedules([...pendingSchedules.values()]);
|
|
176
|
+
await applyWorkflowSchedules();
|
|
165
177
|
state.launched = true;
|
|
166
178
|
state.recoveryStarted = true;
|
|
167
179
|
logger.info("launched", {
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { ActivityEntryPart, StepActivityPart, StepPhase, SynthesisPhase, SynthesisProgressPart } from "../contracts/chat-parts.js";
|
|
9
9
|
import type { ChatDataPart, EmitFn } from "../loop/types.js";
|
|
10
|
-
import type { ToolDetailResolver } from "../tools/detail-resolver.js";
|
|
11
10
|
/**
|
|
12
11
|
* Stable reconciliation id for a step's reconciling parts
|
|
13
12
|
* (`data-step-activity`, `data-step-file-tree`). One id per (runId, stepId)
|
|
@@ -49,11 +48,9 @@ export declare function synthesisEmission(runId: string, stepId: string, seq: nu
|
|
|
49
48
|
* The live-activity phrase for one tool call: the tool's name, then its own
|
|
50
49
|
* description of this call when it has one.
|
|
51
50
|
*
|
|
52
|
-
* The description
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* describing, because a sandbox agent's tool list is not the conversation
|
|
56
|
-
* roster's and no single list serves both.
|
|
51
|
+
* The description is the `detail` of the loop event of the call, which the loop
|
|
52
|
+
* computed with the `describeCall` hook of the tool. The phrase never reads the
|
|
53
|
+
* input of the call.
|
|
57
54
|
*
|
|
58
55
|
* The name always leads, and that is load-bearing rather than cosmetic. This
|
|
59
56
|
* phrase is rendered alone, with no tool name beside it — unlike the chat chip,
|
|
@@ -61,11 +58,10 @@ export declare function synthesisEmission(runId: string, stepId: string, seq: nu
|
|
|
61
58
|
* `edit_file` both describe a call by its path, so a detail on its own would
|
|
62
59
|
* render a write and an edit of the same file identically.
|
|
63
60
|
*
|
|
64
|
-
*
|
|
65
|
-
* back to the name alone. No verb is added: the phrase is carried on a part that
|
|
61
|
+
* An event with no detail falls back to the name alone. No verb is added: the phrase is carried on a part that
|
|
66
62
|
* already states the phase, so "Running" would only restate it.
|
|
67
63
|
*/
|
|
68
|
-
export declare function activityForTool(name: string,
|
|
64
|
+
export declare function activityForTool(name: string, detail?: string): string;
|
|
69
65
|
/** On-change working-tree delta the sandbox executor posts per exec (paths
|
|
70
66
|
* only; the executor skips directories). */
|
|
71
67
|
export type SandboxTreeDelta = {
|
|
@@ -54,11 +54,9 @@ export function synthesisEmission(runId, stepId, seq, phase, activity, at, extra
|
|
|
54
54
|
* The live-activity phrase for one tool call: the tool's name, then its own
|
|
55
55
|
* description of this call when it has one.
|
|
56
56
|
*
|
|
57
|
-
* The description
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* describing, because a sandbox agent's tool list is not the conversation
|
|
61
|
-
* roster's and no single list serves both.
|
|
57
|
+
* The description is the `detail` of the loop event of the call, which the loop
|
|
58
|
+
* computed with the `describeCall` hook of the tool. The phrase never reads the
|
|
59
|
+
* input of the call.
|
|
62
60
|
*
|
|
63
61
|
* The name always leads, and that is load-bearing rather than cosmetic. This
|
|
64
62
|
* phrase is rendered alone, with no tool name beside it — unlike the chat chip,
|
|
@@ -66,12 +64,10 @@ export function synthesisEmission(runId, stepId, seq, phase, activity, at, extra
|
|
|
66
64
|
* `edit_file` both describe a call by its path, so a detail on its own would
|
|
67
65
|
* render a write and an edit of the same file identically.
|
|
68
66
|
*
|
|
69
|
-
*
|
|
70
|
-
* back to the name alone. No verb is added: the phrase is carried on a part that
|
|
67
|
+
* An event with no detail falls back to the name alone. No verb is added: the phrase is carried on a part that
|
|
71
68
|
* already states the phase, so "Running" would only restate it.
|
|
72
69
|
*/
|
|
73
|
-
export function activityForTool(name,
|
|
74
|
-
const detail = resolveDetail?.(name, input);
|
|
70
|
+
export function activityForTool(name, detail) {
|
|
75
71
|
return detail === undefined ? name : `${name} ${detail}`;
|
|
76
72
|
}
|
|
77
73
|
const SandboxTreeDeltaSchema = z.object({
|
|
@@ -86,6 +86,8 @@ export type ArtifactPageQuery = {
|
|
|
86
86
|
q?: string;
|
|
87
87
|
/** The whole path, with case. It selects the one row of that path. */
|
|
88
88
|
path?: string;
|
|
89
|
+
/** The role of the row. `step_output` selects the rows that the grouped run count of {@link countArtifactsByRuns} counts. */
|
|
90
|
+
role?: ArtifactRole;
|
|
89
91
|
/**
|
|
90
92
|
* Extensions in lower case, with no dot. A row is selected when the extension of its path is a member. A row with
|
|
91
93
|
* no extension never matches, and an empty list selects each row. The read compares the members as they are.
|
|
@@ -125,7 +127,8 @@ export type ArtifactPage = {
|
|
|
125
127
|
* select, grouped by extension, and the page. The count is a separate statement because a page past the end has no
|
|
126
128
|
* row to carry it. `total` is the sum of the groups.
|
|
127
129
|
*
|
|
128
|
-
* Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included
|
|
130
|
+
* Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included, unless the `role`
|
|
131
|
+
* filter selects one role. `total` and `extensionCounts` follow the filters. The cost
|
|
129
132
|
* grows with the ledger of the one analysis, because the count reads each filtered row.
|
|
130
133
|
*/
|
|
131
134
|
export declare function queryArtifactPage(pool: Querier, analysisId: string, query: ArtifactPageQuery): ResultAsync<ArtifactPage, DbError>;
|
package/dist/state/artifacts.js
CHANGED
|
@@ -193,7 +193,8 @@ const EXTENSION_SQL = `lower(substring(regexp_replace(path, '^.*/', '') from '^.
|
|
|
193
193
|
* select, grouped by extension, and the page. The count is a separate statement because a page past the end has no
|
|
194
194
|
* row to carry it. `total` is the sum of the groups.
|
|
195
195
|
*
|
|
196
|
-
* Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included
|
|
196
|
+
* Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included, unless the `role`
|
|
197
|
+
* filter selects one role. `total` and `extensionCounts` follow the filters. The cost
|
|
197
198
|
* grows with the ledger of the one analysis, because the count reads each filtered row.
|
|
198
199
|
*/
|
|
199
200
|
export function queryArtifactPage(pool, analysisId, query) {
|
|
@@ -215,6 +216,8 @@ export function queryArtifactPage(pool, analysisId, query) {
|
|
|
215
216
|
conditions.push(`strpos(lower(path), lower(${bind(query.q)}::text)) > 0`);
|
|
216
217
|
if (query.path !== undefined)
|
|
217
218
|
conditions.push(`path = ${bind(query.path)}`);
|
|
219
|
+
if (query.role !== undefined)
|
|
220
|
+
conditions.push(`role = ${bind(query.role)}`);
|
|
218
221
|
if (query.extensions !== undefined && query.extensions.length > 0) {
|
|
219
222
|
conditions.push(`${EXTENSION_SQL} = ANY(${bind([...query.extensions])}::text[])`);
|
|
220
223
|
}
|
package/dist/state/index.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis
|
|
|
16
16
|
export type { InsertRunInput, RunPage, RunReservation } from "./runs.js";
|
|
17
17
|
export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun, countStepsByRuns, } from "./step-executions.js";
|
|
18
18
|
export type { InsertStepExecutionInput, RunStepCounts, SeedStepExecutionRow, UpdateStepExecutionInput } from "./step-executions.js";
|
|
19
|
-
export { insertPlan, upsertPlan, loadPlan, loadPlanRecord } from "./plans.js";
|
|
20
|
-
export type { InsertPlanInput, UpsertPlanInput, PlanRecord, PlanRowQuerier } from "./plans.js";
|
|
19
|
+
export { insertPlan, upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./plans.js";
|
|
20
|
+
export type { InsertPlanInput, UpsertPlanInput, PlanRecord, PlanRowQuerier, PlanPage, PlanPageEntry } from "./plans.js";
|
|
21
21
|
export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
|
|
22
22
|
export type { DataProfileStatus, DataProfileTerminalWrite, DataProfileResult, DataProfileChecked, DataProfileCompanionCompleteness, DataProfileDimension, DataProfileGroup, DataProfileGroupSlot, DataProfileFile, DataProfileMemberAnnotation, DataProfileObservation, DataProfilePartition, DataProfileProbeReport, DataProfileQualityAssessment, DataProfileQuarantine, DataProfileRecipeStep, DataProfileKind, DataProfileAxis, DataProfileInputSignature, DataProfileCoverage, } from "./data-profile.js";
|
|
23
23
|
export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses, queryLatestRunsByAnalyses, queryRunProgressByRuns, queryRunsEndedSince, } from "./analyses-metrics.js";
|
package/dist/state/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifact
|
|
|
12
12
|
export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, reconcileOrphanedRuns, ORPHANED_RUN_GRACE_MS, markRunCanceledIfActive, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
13
13
|
setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, countRunsByAnalysis, queryRunsByThread, queryRunIdsByPlan, queryNewestPlanRun, } from "./runs.js";
|
|
14
14
|
export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun, countStepsByRuns, } from "./step-executions.js";
|
|
15
|
-
export { insertPlan, upsertPlan, loadPlan, loadPlanRecord } from "./plans.js";
|
|
15
|
+
export { insertPlan, upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./plans.js";
|
|
16
16
|
export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
|
|
17
17
|
export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses, queryLatestRunsByAnalyses, queryRunProgressByRuns, queryRunsEndedSince, } from "./analyses-metrics.js";
|
|
18
18
|
// Not per-entity: one analysis's whole Postgres footprint, reached from its id
|
package/dist/state/plans.d.ts
CHANGED
|
@@ -94,3 +94,30 @@ export type PlanRecord = {
|
|
|
94
94
|
export declare function loadPlanRecord(pool: PlanRowQuerier, planId: string, opts: {
|
|
95
95
|
analysisId: string;
|
|
96
96
|
}): ResultAsync<PlanRecord | null, DbError>;
|
|
97
|
+
/** One plan of {@link queryPlanPage}. */
|
|
98
|
+
export type PlanPageEntry = {
|
|
99
|
+
planId: string;
|
|
100
|
+
createdAt: string;
|
|
101
|
+
/** The `title` of the stored document when it is a string, as the document holds it, else `null`. */
|
|
102
|
+
title: string | null;
|
|
103
|
+
parentPlanId: string | null;
|
|
104
|
+
/** The newest plan of the same analysis that names this plan as its parent. */
|
|
105
|
+
supersededBy: string | null;
|
|
106
|
+
/** The newest run of the plan, in the order of {@link queryRunIdsByPlan}, or `null` for a plan with no run. */
|
|
107
|
+
newestRunId: string | null;
|
|
108
|
+
};
|
|
109
|
+
/** One page of the plans of an analysis, and the exact count of them. */
|
|
110
|
+
export type PlanPage = {
|
|
111
|
+
entries: PlanPageEntry[];
|
|
112
|
+
total: number;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* One page of the plans of an analysis, newest first, in two statements: the count of the plans and the page. The
|
|
116
|
+
* count is a separate statement because a page past the end has no row to carry it. The order is the one of
|
|
117
|
+
* `supersededBy`: the creation time, then the highest plan id. The page gives the title of the document, not the
|
|
118
|
+
* document, because a page holds up to 200 plans.
|
|
119
|
+
*/
|
|
120
|
+
export declare function queryPlanPage(pool: Querier, analysisId: string, opts: {
|
|
121
|
+
limit: number;
|
|
122
|
+
offset: number;
|
|
123
|
+
}): ResultAsync<PlanPage, DbError>;
|
package/dist/state/plans.js
CHANGED
|
@@ -117,6 +117,13 @@ const PlanRecordRowSchema = z.object({
|
|
|
117
117
|
created_at: z.string(),
|
|
118
118
|
superseded_by: z.string().nullable(),
|
|
119
119
|
});
|
|
120
|
+
/**
|
|
121
|
+
* The `superseded_by` column of a plan read: the newest plan of the same analysis that names the plan as its parent.
|
|
122
|
+
* The text refers to the plan row of the outer query as `p`.
|
|
123
|
+
*/
|
|
124
|
+
const SUPERSEDED_BY_SQL = `(SELECT c.plan_id FROM cortex_plans c
|
|
125
|
+
WHERE c.parent_plan_id = p.plan_id AND c.analysis_id = p.analysis_id
|
|
126
|
+
ORDER BY c.created_at DESC, c.plan_id DESC LIMIT 1) AS superseded_by`;
|
|
120
127
|
/**
|
|
121
128
|
* Analysis-scoped read of one plan with its parent and the newest plan that
|
|
122
129
|
* replaced it, in one statement. Returns `ok(null)` for an unknown planId OR a
|
|
@@ -129,9 +136,7 @@ export function loadPlanRecord(pool, planId, opts) {
|
|
|
129
136
|
return tryQuery("plans.loadPlanRecord", async () => {
|
|
130
137
|
const result = await pool.query({
|
|
131
138
|
text: `SELECT p.plan, p.parent_plan_id, p.created_at,
|
|
132
|
-
|
|
133
|
-
WHERE c.parent_plan_id = p.plan_id AND c.analysis_id = p.analysis_id
|
|
134
|
-
ORDER BY c.created_at DESC, c.plan_id DESC LIMIT 1) AS superseded_by
|
|
139
|
+
${SUPERSEDED_BY_SQL}
|
|
135
140
|
FROM cortex_plans p
|
|
136
141
|
WHERE p.plan_id = $1 AND p.analysis_id = $2`,
|
|
137
142
|
values: [planId, opts.analysisId],
|
|
@@ -143,3 +148,52 @@ export function loadPlanRecord(pool, planId, opts) {
|
|
|
143
148
|
return { planId, plan: row.plan, parentPlanId: row.parent_plan_id, supersededBy: row.superseded_by, createdAt: row.created_at };
|
|
144
149
|
});
|
|
145
150
|
}
|
|
151
|
+
const PlanPageRowSchema = z.object({
|
|
152
|
+
plan_id: z.string(),
|
|
153
|
+
created_at: z.string(),
|
|
154
|
+
title: z.string().nullable(),
|
|
155
|
+
parent_plan_id: z.string().nullable(),
|
|
156
|
+
superseded_by: z.string().nullable(),
|
|
157
|
+
newest_run_id: z.string().nullable(),
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* One page of the plans of an analysis, newest first, in two statements: the count of the plans and the page. The
|
|
161
|
+
* count is a separate statement because a page past the end has no row to carry it. The order is the one of
|
|
162
|
+
* `supersededBy`: the creation time, then the highest plan id. The page gives the title of the document, not the
|
|
163
|
+
* document, because a page holds up to 200 plans.
|
|
164
|
+
*/
|
|
165
|
+
export function queryPlanPage(pool, analysisId, opts) {
|
|
166
|
+
return tryQuery("plans.queryPlanPage", async () => {
|
|
167
|
+
const countResult = await pool.query({
|
|
168
|
+
text: "SELECT COUNT(*)::int AS total FROM cortex_plans WHERE analysis_id = $1",
|
|
169
|
+
values: [analysisId],
|
|
170
|
+
});
|
|
171
|
+
const rowsResult = await pool.query({
|
|
172
|
+
text: `SELECT p.plan_id, p.created_at, p.parent_plan_id,
|
|
173
|
+
CASE WHEN jsonb_typeof(p.plan->'title') = 'string' THEN p.plan->>'title' END AS title,
|
|
174
|
+
${SUPERSEDED_BY_SQL},
|
|
175
|
+
(SELECT r.run_id FROM cortex_runs r
|
|
176
|
+
WHERE r.analysis_id = p.analysis_id AND r.plan_id = p.plan_id
|
|
177
|
+
ORDER BY r.started_at DESC, r.run_id DESC LIMIT 1) AS newest_run_id
|
|
178
|
+
FROM cortex_plans p
|
|
179
|
+
WHERE p.analysis_id = $1
|
|
180
|
+
ORDER BY p.created_at DESC, p.plan_id DESC
|
|
181
|
+
LIMIT $2 OFFSET $3`,
|
|
182
|
+
values: [analysisId, opts.limit, opts.offset],
|
|
183
|
+
});
|
|
184
|
+
return {
|
|
185
|
+
entries: rowsResult.rows.map((raw) => {
|
|
186
|
+
const row = PlanPageRowSchema.parse(raw);
|
|
187
|
+
return {
|
|
188
|
+
planId: row.plan_id,
|
|
189
|
+
createdAt: row.created_at,
|
|
190
|
+
title: row.title,
|
|
191
|
+
parentPlanId: row.parent_plan_id,
|
|
192
|
+
supersededBy: row.superseded_by,
|
|
193
|
+
newestRunId: row.newest_run_id,
|
|
194
|
+
};
|
|
195
|
+
}),
|
|
196
|
+
total: countResult.rows[0]?.total ?? 0,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|