@inflexa-ai/harness 0.19.2 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/conversation-agent.d.ts +26 -14
- package/dist/agents/conversation-agent.js +12 -23
- package/dist/agents/report-session-agent.d.ts +31 -5
- package/dist/agents/report-session-agent.js +31 -6
- package/dist/app/chat-turn.js +3 -1
- package/dist/app/message-assembly.d.ts +15 -1
- package/dist/app/message-assembly.js +22 -8
- package/dist/app/report-session-runtime.d.ts +21 -4
- package/dist/app/report-session-runtime.js +53 -6
- package/dist/app/spawn-report-session.d.ts +174 -17
- package/dist/app/spawn-report-session.js +233 -21
- package/dist/contracts/chat-events.d.ts +7 -2
- package/dist/contracts/message.d.ts +10 -0
- package/dist/contracts/report-blocks.d.ts +743 -19
- package/dist/contracts/report-blocks.js +186 -11
- package/dist/contracts/report-reference.d.ts +88 -0
- package/dist/contracts/report-reference.js +48 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -0
- package/dist/lib/chrome.d.ts +26 -6
- package/dist/lib/chrome.js +99 -32
- package/dist/lib/eyes.d.ts +57 -0
- package/dist/lib/eyes.js +45 -0
- package/dist/lib/page-capture.d.ts +14 -0
- package/dist/lib/page-capture.js +56 -2
- package/dist/literature/sources/pubmed.js +2 -1
- package/dist/loop/run-agent.js +58 -23
- package/dist/loop/tool-detail.d.ts +19 -2
- package/dist/loop/tool-detail.js +35 -2
- package/dist/memory/content-to-cortex.d.ts +3 -2
- package/dist/memory/content-to-cortex.js +21 -12
- package/dist/memory/conversation-display-replay.js +14 -7
- package/dist/memory/thread-history.d.ts +60 -3
- package/dist/memory/thread-history.js +55 -16
- package/dist/memory/thread-store.d.ts +11 -1
- package/dist/memory/thread-store.js +12 -2
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +23 -6
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +183 -9
- package/dist/report-model/authoring-grammar.d.ts +463 -0
- package/dist/report-model/authoring-grammar.js +77 -0
- package/dist/report-model/block-walk.d.ts +57 -14
- package/dist/report-model/block-walk.js +116 -11
- package/dist/report-model/draft-finish.d.ts +22 -4
- package/dist/report-model/draft-finish.js +39 -7
- package/dist/report-model/draft-operations.d.ts +3 -0
- package/dist/report-model/draft-operations.js +19 -5
- package/dist/report-model/draft-read.d.ts +8 -5
- package/dist/report-model/draft-read.js +21 -5
- package/dist/report-model/fixture-resolver.js +15 -4
- package/dist/report-model/pin-snapshot.d.ts +24 -6
- package/dist/report-model/pin-snapshot.js +204 -6
- package/dist/report-model/production-resolver.js +15 -4
- package/dist/report-model/reference-resolver.d.ts +50 -1
- package/dist/report-model/reference-resolver.js +94 -1
- package/dist/report-model/reference-stamp.d.ts +45 -0
- package/dist/report-model/reference-stamp.js +173 -0
- package/dist/report-model/structural-validation.d.ts +4 -1
- package/dist/report-model/structural-validation.js +48 -3
- package/dist/report-model/validate.d.ts +1 -1
- package/dist/report-model/validate.js +1 -1
- package/dist/report-render/asset-lookup.d.ts +19 -0
- package/dist/report-render/asset-lookup.js +37 -0
- package/dist/report-render/assets.d.ts +45 -1
- package/dist/report-render/assets.js +65 -6
- package/dist/report-render/chart-presets.d.ts +102 -0
- package/dist/report-render/chart-presets.js +205 -0
- package/dist/report-render/chart.d.ts +112 -10
- package/dist/report-render/chart.js +1164 -41
- package/dist/report-render/design.d.ts +99 -0
- package/dist/report-render/design.js +289 -107
- package/dist/report-render/number-format.d.ts +117 -0
- package/dist/report-render/number-format.js +479 -0
- package/dist/report-render/page.d.ts +121 -5
- package/dist/report-render/page.js +689 -7
- package/dist/report-render/references.d.ts +70 -16
- package/dist/report-render/references.js +54 -22
- package/dist/report-render/render.d.ts +23 -5
- package/dist/report-render/render.js +81 -22
- package/dist/report-render/table-data.d.ts +92 -0
- package/dist/report-render/table-data.js +129 -0
- package/dist/report-render/types.d.ts +15 -0
- package/dist/report-render/value-bridge.js +6 -2
- package/dist/report-render/views/chart-view.d.ts +12 -5
- package/dist/report-render/views/chart-view.js +14 -8
- package/dist/report-render/views/page-view.d.ts +36 -15
- package/dist/report-render/views/page-view.js +45 -23
- package/dist/report-render/views/prose.d.ts +12 -2
- package/dist/report-render/views/prose.js +29 -7
- package/dist/report-render/views/references-view.d.ts +22 -8
- package/dist/report-render/views/references-view.js +101 -13
- package/dist/report-render/views/values.d.ts +128 -13
- package/dist/report-render/views/values.js +199 -18
- package/dist/runtime/assemble.d.ts +39 -5
- package/dist/runtime/assemble.js +47 -6
- package/dist/sandbox/client.d.ts +10 -1
- package/dist/sandbox/create-sandbox.d.ts +9 -4
- package/dist/sandbox/create-sandbox.js +22 -16
- package/dist/sandbox/docker-client.js +10 -7
- package/dist/sandbox/k8s-client.js +11 -18
- package/dist/sandbox/mount-plan.d.ts +45 -7
- package/dist/sandbox/mount-plan.js +43 -5
- package/dist/sandbox/types.d.ts +10 -6
- package/dist/state/init.js +38 -9
- package/dist/state/report-session-state.d.ts +42 -1
- package/dist/state/report-session-state.js +70 -2
- package/dist/state/report-versions.d.ts +22 -17
- package/dist/state/report-versions.js +48 -35
- package/dist/state/snapshot-parse.js +16 -1
- package/dist/tasks/data-profile.d.ts +7 -4
- package/dist/tasks/data-profile.js +9 -9
- package/dist/tools/define-tool.d.ts +32 -0
- package/dist/tools/define-tool.js +4 -0
- package/dist/tools/detail-resolver.d.ts +7 -2
- package/dist/tools/detail-resolver.js +7 -2
- package/dist/tools/iterate-report.d.ts +1 -1
- package/dist/tools/report/preview-snapshot.js +1 -0
- package/dist/tools/report-authoring/authoring-tools.d.ts +15 -6
- package/dist/tools/report-authoring/authoring-tools.js +99 -18
- package/dist/tools/report-session/derive-table.d.ts +162 -0
- package/dist/tools/report-session/derive-table.js +459 -0
- package/dist/tools/report-session/examine-page.d.ts +48 -18
- package/dist/tools/report-session/examine-page.js +204 -24
- package/dist/tools/report-session/index.d.ts +2 -0
- package/dist/tools/report-session/index.js +2 -0
- package/dist/tools/report-session/list-artifacts.d.ts +97 -0
- package/dist/tools/report-session/list-artifacts.js +197 -0
- package/dist/tools/report-session/preview-report.d.ts +6 -0
- package/dist/tools/report-session/preview-report.js +180 -28
- package/dist/tools/report-session/record-version.d.ts +22 -10
- package/dist/tools/report-session/record-version.js +99 -14
- package/dist/tools/start-report-session.d.ts +115 -0
- package/dist/tools/start-report-session.js +179 -0
- package/dist/workflows/sandbox-step.d.ts +7 -4
- package/dist/workflows/sandbox-step.js +10 -12
- package/dist/workspace/paths.d.ts +50 -0
- package/dist/workspace/paths.js +63 -0
- package/package.json +2 -1
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
* document can be incomplete. A column that fails a parse reads as a typed error, and
|
|
22
22
|
* it does not crash. An absent row reads as a normal absence.
|
|
23
23
|
*
|
|
24
|
+
* The row also holds the derivation list of the session. One record chains a derived
|
|
25
|
+
* table to the evidence that made it: the output path, the output hash, the source
|
|
26
|
+
* paths with their hashes, the script hash, and the script text. A record is
|
|
27
|
+
* immutable, and the append refuses a repeated output path. A row that holds no list
|
|
28
|
+
* reads as an empty list.
|
|
29
|
+
*
|
|
24
30
|
* A persist is a compare-and-swap. The load reads the prior document, and the persist
|
|
25
31
|
* lands only when the row still holds it. Thus two concurrent turns cannot both land,
|
|
26
32
|
* and the loser reads the state again.
|
|
@@ -28,12 +34,22 @@
|
|
|
28
34
|
* A purge of the analysis removes the row through the analysis-id cascade.
|
|
29
35
|
*/
|
|
30
36
|
import { err, ok } from "neverthrow";
|
|
37
|
+
import { z } from "zod";
|
|
31
38
|
import { tryMutation, tryQuery } from "../lib/db-result.js";
|
|
32
39
|
import { DraftDocumentSchema } from "../report-model/draft.js";
|
|
33
40
|
import { parseSnapshot, reduceIssues } from "./snapshot-parse.js";
|
|
41
|
+
const DerivationSourceSchema = z.object({ path: z.string(), hash: z.string() });
|
|
42
|
+
const DerivationRecordSchema = z.object({
|
|
43
|
+
outputPath: z.string(),
|
|
44
|
+
outputHash: z.string(),
|
|
45
|
+
sources: z.array(DerivationSourceSchema),
|
|
46
|
+
scriptHash: z.string(),
|
|
47
|
+
script: z.string(),
|
|
48
|
+
});
|
|
49
|
+
const DerivationListSchema = z.array(DerivationRecordSchema);
|
|
34
50
|
const _assertReadError = true;
|
|
35
51
|
/** The full row projection every read shares. */
|
|
36
|
-
const STATE_COLUMNS = "thread_id, analysis_id, document, snapshot, rendered_document_hash, seen_document_hash, created_at";
|
|
52
|
+
const STATE_COLUMNS = "thread_id, analysis_id, document, snapshot, derivations, rendered_document_hash, seen_document_hash, created_at";
|
|
37
53
|
/**
|
|
38
54
|
* The insert. The row carries the snapshot at creation, and the document lands later
|
|
39
55
|
* through `persistDocument`. ON CONFLICT DO NOTHING keeps the first row, thus a
|
|
@@ -72,11 +88,28 @@ function rowToState(row) {
|
|
|
72
88
|
}
|
|
73
89
|
snapshot = parsed.value;
|
|
74
90
|
}
|
|
91
|
+
// A row that predates the list carries SQL NULL, and a session that derived nothing carries none
|
|
92
|
+
// either. Both read as an empty list, because a session with no derivation has none.
|
|
93
|
+
let derivations = [];
|
|
94
|
+
if (row.derivations !== null && row.derivations !== undefined) {
|
|
95
|
+
const parsed = DerivationListSchema.safeParse(row.derivations);
|
|
96
|
+
if (!parsed.success) {
|
|
97
|
+
return err({
|
|
98
|
+
type: "corrupt_session_state",
|
|
99
|
+
op: "report-session-state.read",
|
|
100
|
+
threadId: row.thread_id,
|
|
101
|
+
part: "derivations",
|
|
102
|
+
issues: reduceIssues(parsed.error),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
derivations = parsed.data;
|
|
106
|
+
}
|
|
75
107
|
return ok({
|
|
76
108
|
threadId: row.thread_id,
|
|
77
109
|
analysisId: row.analysis_id,
|
|
78
110
|
document,
|
|
79
111
|
snapshot,
|
|
112
|
+
derivations,
|
|
80
113
|
renderedDocumentHash: row.rendered_document_hash,
|
|
81
114
|
seenDocumentHash: row.seen_document_hash,
|
|
82
115
|
createdAt: row.created_at,
|
|
@@ -173,5 +206,40 @@ export function createReportSessionStateStore({ pool }) {
|
|
|
173
206
|
return row.copied === null ? "no-rendered" : "stamped";
|
|
174
207
|
});
|
|
175
208
|
}
|
|
176
|
-
|
|
209
|
+
function appendDerivation(threadId, record) {
|
|
210
|
+
return tryMutation("report-session-state.appendDerivation", async () => {
|
|
211
|
+
// One statement tells a duplicate from an absence, the same way the persist does. `existed`
|
|
212
|
+
// counts the row, and `appended` counts the update.
|
|
213
|
+
//
|
|
214
|
+
// The name rule sits in the qualification of the UPDATE, over the row that the statement locks.
|
|
215
|
+
// A read of the CTE would test the list as it stood at the start of the statement: two
|
|
216
|
+
// concurrent appends of one name would each pass that test, and both would land. Postgres
|
|
217
|
+
// re-evaluates the qualification of an UPDATE against the row that it waited for, thus the
|
|
218
|
+
// loser of the race sees the list of the winner and it appends nothing.
|
|
219
|
+
const { rows } = await pool.query({
|
|
220
|
+
text: `WITH target AS (
|
|
221
|
+
SELECT thread_id
|
|
222
|
+
FROM cortex_report_session_state
|
|
223
|
+
WHERE thread_id = $1
|
|
224
|
+
), appended AS (
|
|
225
|
+
UPDATE cortex_report_session_state AS s
|
|
226
|
+
SET derivations = COALESCE(s.derivations, '[]'::jsonb) || $2::jsonb
|
|
227
|
+
WHERE s.thread_id = $1
|
|
228
|
+
AND NOT EXISTS (
|
|
229
|
+
SELECT 1 FROM jsonb_array_elements(COALESCE(s.derivations, '[]'::jsonb)) AS held WHERE held->>'outputPath' = $3
|
|
230
|
+
)
|
|
231
|
+
RETURNING 1
|
|
232
|
+
)
|
|
233
|
+
SELECT (SELECT COUNT(*) FROM target)::int AS existed, (SELECT COUNT(*) FROM appended)::int AS appended`,
|
|
234
|
+
// The record rides as a one-element array, thus the `||` operator appends one element and
|
|
235
|
+
// never merges the fields of an object into the list.
|
|
236
|
+
values: [threadId, JSON.stringify([record]), record.outputPath],
|
|
237
|
+
});
|
|
238
|
+
const row = rows[0];
|
|
239
|
+
if (row === undefined || row.existed === 0)
|
|
240
|
+
return "absent";
|
|
241
|
+
return row.appended > 0 ? "appended" : "duplicate";
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return { readState, writeSnapshot, persistDocument, stampRendered, stampSeen, appendDerivation };
|
|
177
245
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The report version store -- the
|
|
2
|
+
* The report version store -- the one recorded version of each report thread.
|
|
3
3
|
*
|
|
4
|
-
* A version is one
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A version is one row. It holds the block document, the pinned snapshot, and the
|
|
5
|
+
* anchor. The store records a version and reads a version. A record on a thread
|
|
6
|
+
* that holds a version replaces the document, the snapshot, and the anchor of that
|
|
7
|
+
* row, whole. The version id stays, thus a consumer that names the version keeps
|
|
8
|
+
* its name.
|
|
7
9
|
*
|
|
8
10
|
* A thread holds at most one version, and a named unique constraint on the thread
|
|
9
|
-
* id enforces it. The record reads nothing before the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* id enforces it. The record reads nothing before the write, and the constraint
|
|
12
|
+
* routes it: a fresh thread takes the insert arm, and a thread that holds a version
|
|
13
|
+
* takes the replace arm. The store writes the full triple each time, thus a partial
|
|
14
|
+
* update is not representable.
|
|
12
15
|
*
|
|
13
16
|
* The store keeps the snapshot and the anchor as given, and it never pins a
|
|
14
17
|
* snapshot. A read parses the stored document and the stored snapshot with the
|
|
@@ -42,9 +45,14 @@ export interface RecordVersionInput {
|
|
|
42
45
|
/** The earlier version that this one reuses. The parent must belong to the same analysis. */
|
|
43
46
|
readonly parentVersionId?: string;
|
|
44
47
|
}
|
|
45
|
-
/** The stable reference of a
|
|
48
|
+
/** The stable reference of a recorded version, and how the record landed. */
|
|
46
49
|
export interface RecordedVersionRef {
|
|
47
50
|
readonly versionId: string;
|
|
51
|
+
/**
|
|
52
|
+
* `created` minted the row. `replaced` wrote over the one version of the
|
|
53
|
+
* thread, under the same version id.
|
|
54
|
+
*/
|
|
55
|
+
readonly outcome: "created" | "replaced";
|
|
48
56
|
}
|
|
49
57
|
/**
|
|
50
58
|
* A record the store refuses as data, before or in place of a row. Distinct from
|
|
@@ -54,9 +62,9 @@ export interface RecordedVersionRef {
|
|
|
54
62
|
* refuses a malformed document or a malformed snapshot here as typed data, and
|
|
55
63
|
* no row lands.
|
|
56
64
|
*
|
|
57
|
-
* A second record for one thread is
|
|
58
|
-
*
|
|
59
|
-
*
|
|
65
|
+
* A second record for one thread is no refusal. The unique constraint on the
|
|
66
|
+
* thread id routes it into the replace arm, and the row carries the new triple.
|
|
67
|
+
* Every constraint trip stays a `DbError`.
|
|
60
68
|
*
|
|
61
69
|
* A `parentVersionId` naming no row is not one of these -- the self foreign key
|
|
62
70
|
* refuses an unknown parent id, and `tryMutation` classifies that refusal as the
|
|
@@ -76,10 +84,6 @@ export type RecordVersionError = {
|
|
|
76
84
|
readonly analysisId: string;
|
|
77
85
|
readonly parentVersionId: string;
|
|
78
86
|
readonly parentAnalysisId: string;
|
|
79
|
-
} | {
|
|
80
|
-
readonly type: "thread_already_holds_version";
|
|
81
|
-
readonly op: string;
|
|
82
|
-
readonly threadId: string;
|
|
83
87
|
} | DbError;
|
|
84
88
|
/**
|
|
85
89
|
* A stored row that a read cannot parse with the current schemas. It names the
|
|
@@ -109,8 +113,9 @@ export interface ReportVersionStore {
|
|
|
109
113
|
/**
|
|
110
114
|
* Record a version. The record parses the document, refuses a malformed value
|
|
111
115
|
* as data with no row, and refuses a parent from a different analysis. It reads
|
|
112
|
-
* nothing before the
|
|
113
|
-
*
|
|
116
|
+
* nothing before the write. A record on a thread that holds a version replaces
|
|
117
|
+
* that row whole, under the same version id. The snapshot and the anchor store
|
|
118
|
+
* as given, and the reference names a creation or a replacement.
|
|
114
119
|
*/
|
|
115
120
|
record(input: RecordVersionInput): ResultAsync<RecordedVersionRef, RecordVersionError>;
|
|
116
121
|
/** One version by its id, or `null` when no row holds it. */
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The report version store -- the
|
|
2
|
+
* The report version store -- the one recorded version of each report thread.
|
|
3
3
|
*
|
|
4
|
-
* A version is one
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A version is one row. It holds the block document, the pinned snapshot, and the
|
|
5
|
+
* anchor. The store records a version and reads a version. A record on a thread
|
|
6
|
+
* that holds a version replaces the document, the snapshot, and the anchor of that
|
|
7
|
+
* row, whole. The version id stays, thus a consumer that names the version keeps
|
|
8
|
+
* its name.
|
|
7
9
|
*
|
|
8
10
|
* A thread holds at most one version, and a named unique constraint on the thread
|
|
9
|
-
* id enforces it. The record reads nothing before the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* id enforces it. The record reads nothing before the write, and the constraint
|
|
12
|
+
* routes it: a fresh thread takes the insert arm, and a thread that holds a version
|
|
13
|
+
* takes the replace arm. The store writes the full triple each time, thus a partial
|
|
14
|
+
* update is not representable.
|
|
12
15
|
*
|
|
13
16
|
* The store keeps the snapshot and the anchor as given, and it never pins a
|
|
14
17
|
* snapshot. A read parses the stored document and the stored snapshot with the
|
|
@@ -25,9 +28,8 @@ import { ReportDocumentSchema } from "../contracts/report-blocks.js";
|
|
|
25
28
|
import { tryMutation, tryQuery } from "../lib/db-result.js";
|
|
26
29
|
import { parseSnapshot, reduceIssues } from "./snapshot-parse.js";
|
|
27
30
|
/**
|
|
28
|
-
* The name of the unique constraint on thread_id.
|
|
29
|
-
*
|
|
30
|
-
* thread_already_holds_version refusal.
|
|
31
|
+
* The name of the unique constraint on thread_id. The write names it as its
|
|
32
|
+
* conflict target, thus the replace arm binds to the one row of the thread.
|
|
31
33
|
*/
|
|
32
34
|
const THREAD_UNIQUE_CONSTRAINT = "cortex_report_versions_one_per_thread";
|
|
33
35
|
const _assertRecordError = true;
|
|
@@ -39,13 +41,30 @@ const _assertReadError = true;
|
|
|
39
41
|
*/
|
|
40
42
|
const VERSION_COLUMNS = "version_id, analysis_id, thread_id, parent_thread_id, parent_seq::text AS parent_seq, parent_version_id, document, snapshot, created_at";
|
|
41
43
|
/**
|
|
42
|
-
* The
|
|
43
|
-
* columns directly.
|
|
44
|
+
* The write. The row carries no ordinal, thus the statement writes the given
|
|
45
|
+
* columns directly. A fresh thread takes the insert arm, and `created_at` takes
|
|
46
|
+
* the column default. A thread that holds a version trips the named constraint,
|
|
47
|
+
* and the conflict target turns the trip into the replace of that one row.
|
|
48
|
+
*
|
|
49
|
+
* The replace arm writes each given column, thus the stored row equals the input.
|
|
50
|
+
* It writes neither `version_id` nor `created_at`: the version keeps its name, and
|
|
51
|
+
* the row keeps the time of its first record.
|
|
52
|
+
*
|
|
53
|
+
* The returned id names the row that stands. It equals the minted id on the insert
|
|
54
|
+
* arm alone, thus the caller reads which arm ran.
|
|
44
55
|
*/
|
|
45
|
-
const
|
|
56
|
+
const RECORD_SQL = `INSERT INTO cortex_report_versions
|
|
46
57
|
(version_id, analysis_id, thread_id, parent_thread_id, parent_seq,
|
|
47
58
|
parent_version_id, document, snapshot)
|
|
48
|
-
VALUES ($1, $2, $3, $4, $5::bigint, $6, $7::jsonb, $8::jsonb)
|
|
59
|
+
VALUES ($1, $2, $3, $4, $5::bigint, $6, $7::jsonb, $8::jsonb)
|
|
60
|
+
ON CONFLICT ON CONSTRAINT ${THREAD_UNIQUE_CONSTRAINT} DO UPDATE SET
|
|
61
|
+
analysis_id = EXCLUDED.analysis_id,
|
|
62
|
+
parent_thread_id = EXCLUDED.parent_thread_id,
|
|
63
|
+
parent_seq = EXCLUDED.parent_seq,
|
|
64
|
+
parent_version_id = EXCLUDED.parent_version_id,
|
|
65
|
+
document = EXCLUDED.document,
|
|
66
|
+
snapshot = EXCLUDED.snapshot
|
|
67
|
+
RETURNING version_id`;
|
|
49
68
|
function rowToVersion(row) {
|
|
50
69
|
const document = ReportDocumentSchema.safeParse(row.document);
|
|
51
70
|
if (!document.success) {
|
|
@@ -67,16 +86,6 @@ function rowToVersion(row) {
|
|
|
67
86
|
createdAt: row.created_at,
|
|
68
87
|
});
|
|
69
88
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Map an insert failure to a record refusal. A trip of the thread constraint is
|
|
72
|
-
* thread_already_holds_version. Every other DbError stays unchanged.
|
|
73
|
-
*/
|
|
74
|
-
function mapInsertError(error, threadId) {
|
|
75
|
-
if (error.type === "constraint_violation" && error.constraint === THREAD_UNIQUE_CONSTRAINT) {
|
|
76
|
-
return { type: "thread_already_holds_version", op: "report-versions.record", threadId };
|
|
77
|
-
}
|
|
78
|
-
return error;
|
|
79
|
-
}
|
|
80
89
|
/**
|
|
81
90
|
* Create a `ReportVersionStore` bound to a Postgres pool. The
|
|
82
91
|
* `cortex_report_versions` table is provisioned by the state-init DDL.
|
|
@@ -85,7 +94,7 @@ export function createReportVersionStore({ pool }) {
|
|
|
85
94
|
/**
|
|
86
95
|
* The analysis scope of a parent version, or `null` when no such row exists.
|
|
87
96
|
* Absence is not a verdict here: an unknown parent id falls through to the
|
|
88
|
-
*
|
|
97
|
+
* write, where the self foreign key refuses it as a `DbError`.
|
|
89
98
|
*/
|
|
90
99
|
function parentAnalysis(parentVersionId) {
|
|
91
100
|
return tryQuery("report-versions.record.parentScope", () => pool.query({
|
|
@@ -105,13 +114,13 @@ export function createReportVersionStore({ pool }) {
|
|
|
105
114
|
if (parsedSnapshot.isErr()) {
|
|
106
115
|
return errAsync({ type: "malformed_snapshot", op: "report-versions.record", issues: parsedSnapshot.error });
|
|
107
116
|
}
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
return tryMutation("report-versions.record.
|
|
111
|
-
await pool.query({
|
|
112
|
-
text:
|
|
117
|
+
const write = () => {
|
|
118
|
+
const minted = randomUUID();
|
|
119
|
+
return tryMutation("report-versions.record.write", async () => {
|
|
120
|
+
const { rows } = await pool.query({
|
|
121
|
+
text: RECORD_SQL,
|
|
113
122
|
values: [
|
|
114
|
-
|
|
123
|
+
minted,
|
|
115
124
|
input.analysisId,
|
|
116
125
|
input.threadId,
|
|
117
126
|
input.parentThreadId,
|
|
@@ -121,11 +130,15 @@ export function createReportVersionStore({ pool }) {
|
|
|
121
130
|
JSON.stringify(input.snapshot),
|
|
122
131
|
],
|
|
123
132
|
});
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
// Each arm of the write returns its row. The replace arm keeps the
|
|
134
|
+
// version id of the row that stood, thus an id that differs from the
|
|
135
|
+
// minted one names a replacement.
|
|
136
|
+
const landed = rows[0]?.version_id ?? minted;
|
|
137
|
+
return { versionId: landed, outcome: landed === minted ? "created" : "replaced" };
|
|
138
|
+
});
|
|
126
139
|
};
|
|
127
140
|
if (input.parentVersionId === undefined) {
|
|
128
|
-
return
|
|
141
|
+
return write();
|
|
129
142
|
}
|
|
130
143
|
const parentVersionId = input.parentVersionId;
|
|
131
144
|
return parentAnalysis(parentVersionId).andThen((parentAnalysisId) => {
|
|
@@ -138,7 +151,7 @@ export function createReportVersionStore({ pool }) {
|
|
|
138
151
|
parentAnalysisId,
|
|
139
152
|
});
|
|
140
153
|
}
|
|
141
|
-
return
|
|
154
|
+
return write();
|
|
142
155
|
});
|
|
143
156
|
}
|
|
144
157
|
function getVersion(versionId) {
|
|
@@ -23,6 +23,11 @@ const ArtifactSnapshotSchema = z.object({
|
|
|
23
23
|
});
|
|
24
24
|
/** The citation list of a stored snapshot. A snapshot with no list reads as an absent field. */
|
|
25
25
|
const CitationsSchema = z.array(z.string()).optional();
|
|
26
|
+
/**
|
|
27
|
+
* The citation records of a stored snapshot, keyed by the citation key. A snapshot with no map reads as
|
|
28
|
+
* an absent field, thus a pin that predates the map loads the same as it did before.
|
|
29
|
+
*/
|
|
30
|
+
const CitationRecordsSchema = z.record(z.string(), z.object({ citation: z.string(), description: z.string().optional() })).optional();
|
|
26
31
|
/** Reduce a zod error to the dotted path and the message of each issue. */
|
|
27
32
|
export function reduceIssues(error) {
|
|
28
33
|
return error.issues.map((issue) => ({
|
|
@@ -63,6 +68,10 @@ export function parseSnapshot(stored) {
|
|
|
63
68
|
if (!citations.success) {
|
|
64
69
|
return err(prefixIssues("citations", reduceIssues(citations.error)));
|
|
65
70
|
}
|
|
71
|
+
const citationRecords = CitationRecordsSchema.safeParse(stored.citationRecords);
|
|
72
|
+
if (!citationRecords.success) {
|
|
73
|
+
return err(prefixIssues("citationRecords", reduceIssues(citationRecords.error)));
|
|
74
|
+
}
|
|
66
75
|
// The map takes a null prototype, the same as the pin, thus a path such as
|
|
67
76
|
// `__proto__` stays an ordinary entry and never reaches a prototype slot.
|
|
68
77
|
const artifacts = Object.create(null);
|
|
@@ -78,5 +87,11 @@ export function parseSnapshot(stored) {
|
|
|
78
87
|
if (issues.length > 0) {
|
|
79
88
|
return err(issues);
|
|
80
89
|
}
|
|
81
|
-
|
|
90
|
+
// An absent part stays absent in the parsed value. Thus a reload gives the same shape that the pin
|
|
91
|
+
// wrote, and an equality test over a stored snapshot holds across the round trip.
|
|
92
|
+
return ok({
|
|
93
|
+
artifacts,
|
|
94
|
+
...(citations.data === undefined ? {} : { citations: citations.data }),
|
|
95
|
+
...(citationRecords.data === undefined ? {} : { citationRecords: citationRecords.data }),
|
|
96
|
+
});
|
|
82
97
|
}
|
|
@@ -23,7 +23,6 @@ import type { RunAuthorization, RunAuthorizer } from "../execution/run-authorize
|
|
|
23
23
|
import type { StagedInput } from "../execution/staged-input.js";
|
|
24
24
|
import type { BioToolKeys } from "../tools/bio/keys.js";
|
|
25
25
|
import type { Logger } from "../lib/logger.js";
|
|
26
|
-
import type { ResolveBilling } from "../billing/resolver.js";
|
|
27
26
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
28
27
|
import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
|
|
29
28
|
import type { SandboxClient } from "../sandbox/client.js";
|
|
@@ -57,9 +56,13 @@ export interface DataProfileDeps extends EnvironmentStorePaths {
|
|
|
57
56
|
readonly skillsDir: string;
|
|
58
57
|
/** LLM usage-accounting seam for the profiler agent loop; omitted falls back to the no-op recorder. */
|
|
59
58
|
readonly usageRecorder?: UsageRecorder;
|
|
60
|
-
/**
|
|
61
|
-
*
|
|
62
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Host-supplied labels for the profiler's sandbox pod, resolved under the
|
|
61
|
+
* profiling session. The map is opaque to the harness: it stamps each entry
|
|
62
|
+
* and interprets none of it. Absent in a wiring that attributes nothing —
|
|
63
|
+
* the pod then carries the harness's own labels only.
|
|
64
|
+
*/
|
|
65
|
+
readonly resolvePodLabels?: (session: RunSession) => Promise<Record<string, string>>;
|
|
63
66
|
}
|
|
64
67
|
/**
|
|
65
68
|
* Workflow input. JSON-serialisable — DBOS persists it as the workflow's
|
|
@@ -42,8 +42,6 @@ import { ensureSearchIndex, searchIndexName } from "../workspace/search-config.j
|
|
|
42
42
|
import { DATA_PROFILE_RUN_LITERAL } from "../contracts/data-profile.js";
|
|
43
43
|
const DATA_PROFILE_STEP_LITERAL = "profile";
|
|
44
44
|
const DATA_PROFILE_AGENT_ID = "data-profiler";
|
|
45
|
-
/** Resolved-header key carrying the billing-context id (see `ResolveBilling`). */
|
|
46
|
-
const BILLING_CONTEXT_HEADER = "X-Inflexa-Billing-Context";
|
|
47
45
|
/** Sandbox-server exec budget for the profile run. */
|
|
48
46
|
const DEFAULT_DEADLINE_MS = 300_000;
|
|
49
47
|
/** Build the canonical artifact path for a staged input file. */
|
|
@@ -228,16 +226,18 @@ export async function runDataProfileBody(input, deps) {
|
|
|
228
226
|
// from the agent loop would leave the longest wait of all unreported — the same defect the
|
|
229
227
|
// run panel's activity readout was built to remove.
|
|
230
228
|
await activity.sandboxInit();
|
|
231
|
-
let
|
|
232
|
-
if (deps.
|
|
229
|
+
let podLabels;
|
|
230
|
+
if (deps.resolvePodLabels) {
|
|
233
231
|
try {
|
|
234
|
-
|
|
232
|
+
podLabels = await deps.resolvePodLabels(childSession);
|
|
235
233
|
}
|
|
236
234
|
catch (err) {
|
|
237
|
-
logger.
|
|
235
|
+
logger.warn("pod-label resolution failed", logger.errorFields(err));
|
|
238
236
|
}
|
|
239
|
-
|
|
240
|
-
|
|
237
|
+
// A wired resolver that yields nothing is the one loud case: the host
|
|
238
|
+
// asked for attribution and the pod spawns without it.
|
|
239
|
+
if (!podLabels || Object.keys(podLabels).length === 0)
|
|
240
|
+
logger.warn("sandbox spawned with no pod labels");
|
|
241
241
|
}
|
|
242
242
|
const sandbox = await deps.sandboxClient.createSandbox({
|
|
243
243
|
runId: DATA_PROFILE_RUN_LITERAL,
|
|
@@ -245,7 +245,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
245
245
|
analysisId,
|
|
246
246
|
childWorkflowId: workflowId,
|
|
247
247
|
resources: estimateDataProfileResources(stagedInputs),
|
|
248
|
-
|
|
248
|
+
podLabels,
|
|
249
249
|
}, mintSandboxIdentity(DATA_PROFILE_RUN_LITERAL));
|
|
250
250
|
try {
|
|
251
251
|
// Checkpointed clock, not `Date.now()`: `awaitExec` gates on this absolute
|
|
@@ -116,6 +116,8 @@ export interface Tool<Input = unknown, Output = unknown> {
|
|
|
116
116
|
readonly executionMode: ToolExecutionMode;
|
|
117
117
|
/** See {@link ToolDefinition.describeCall}. Absent when the tool declares none. */
|
|
118
118
|
describeCall?(input: Input): string;
|
|
119
|
+
/** See {@link ToolDefinition.describeResult}. Absent when the tool declares none. */
|
|
120
|
+
describeResult?(input: Input, result: Output): string;
|
|
119
121
|
execute(input: Input, ctx: ToolContext): Promise<Result<Output, ToolError>>;
|
|
120
122
|
}
|
|
121
123
|
export interface ToolDefinition<Schema extends z.ZodType, Output> {
|
|
@@ -156,6 +158,36 @@ export interface ToolDefinition<Schema extends z.ZodType, Output> {
|
|
|
156
158
|
* The hook never reaches the model.
|
|
157
159
|
*/
|
|
158
160
|
describeCall: ((input: z.infer<Schema>) => string) | "none";
|
|
161
|
+
/**
|
|
162
|
+
* One line that names what THIS call PRODUCED — the outcome-time counterpart
|
|
163
|
+
* of `describeCall`. A page path, a recorded id, and a listed count are facts
|
|
164
|
+
* of the result, thus no hook over the input can name one. The finished event
|
|
165
|
+
* carries this line, and it carries the call detail when the hook gives
|
|
166
|
+
* nothing.
|
|
167
|
+
*
|
|
168
|
+
* The decision is OPTIONAL, unlike `describeCall`. A call always has an input
|
|
169
|
+
* to describe or an input that cannot distinguish it, thus that decision is
|
|
170
|
+
* forced. A result is different: a tool whose ok value adds no fact that a
|
|
171
|
+
* reader wants leaves the call detail to stand for the whole call. Thus the
|
|
172
|
+
* absent key is the whole of "this tool describes no result", and the `"none"`
|
|
173
|
+
* sentinel has no counterpart here.
|
|
174
|
+
*
|
|
175
|
+
* The hook runs on an ok outcome only. An error outcome already names itself,
|
|
176
|
+
* and a hook over a failed call reads a shape that does not exist.
|
|
177
|
+
*
|
|
178
|
+
* A hook is synchronous and pure — no I/O, no ambient state — and a
|
|
179
|
+
* description must never fail a call: the loop guards the call and drops the
|
|
180
|
+
* detail on any failure (see the tool-call-detail capability). The result
|
|
181
|
+
* arrives as the tool produced it, thus the hook reads a value of its own
|
|
182
|
+
* declared `Output` type and no parse stands between the two.
|
|
183
|
+
*
|
|
184
|
+
* Return whatever reads best. Normalization — one line, control characters
|
|
185
|
+
* removed, secrets redacted, length capped — happens once at the emit site,
|
|
186
|
+
* never here.
|
|
187
|
+
*
|
|
188
|
+
* The hook never reaches the model.
|
|
189
|
+
*/
|
|
190
|
+
readonly describeResult?: (input: z.infer<Schema>, result: Output) => string;
|
|
159
191
|
execute(input: z.infer<Schema>, ctx: ToolContext): Promise<Result<Output, ToolError>>;
|
|
160
192
|
}
|
|
161
193
|
/**
|
|
@@ -75,6 +75,10 @@ export function defineTool(def) {
|
|
|
75
75
|
// `typeof` is the discriminator, not equality with the sentinel, so a
|
|
76
76
|
// hook that returns the string `"none"` still packages as a hook.
|
|
77
77
|
...(typeof def.describeCall === "function" ? { describeCall: def.describeCall } : {}),
|
|
78
|
+
// The same property-presence pattern as `describeCall`: a tool that
|
|
79
|
+
// declares no result hook carries no key, thus "has a hook" is one
|
|
80
|
+
// property check on either side.
|
|
81
|
+
...(typeof def.describeResult === "function" ? { describeResult: def.describeResult } : {}),
|
|
78
82
|
execute: def.execute,
|
|
79
83
|
};
|
|
80
84
|
}
|
|
@@ -15,8 +15,13 @@
|
|
|
15
15
|
* registry construction.
|
|
16
16
|
*
|
|
17
17
|
* It resolves through the same `computeDetail` the loop uses, so a reloaded
|
|
18
|
-
* thread
|
|
19
|
-
*
|
|
18
|
+
* thread shows the same CALL detail the live turn showed.
|
|
19
|
+
*
|
|
20
|
+
* The bound is the input. A reload reconstructs from the persisted call alone, and a
|
|
21
|
+
* tool that describes its own RESULT names a fact that the input cannot carry — a page
|
|
22
|
+
* path, a recorded version. That line reaches the live surface on the finished event and
|
|
23
|
+
* it does not reappear here, thus a reloaded call falls back to its call detail. Nothing
|
|
24
|
+
* is persisted: storage stays the pure model transcript.
|
|
20
25
|
*/
|
|
21
26
|
import { type ToolCallDetail } from "../loop/tool-detail.js";
|
|
22
27
|
import type { Logger } from "../lib/logger.js";
|
|
@@ -15,8 +15,13 @@
|
|
|
15
15
|
* registry construction.
|
|
16
16
|
*
|
|
17
17
|
* It resolves through the same `computeDetail` the loop uses, so a reloaded
|
|
18
|
-
* thread
|
|
19
|
-
*
|
|
18
|
+
* thread shows the same CALL detail the live turn showed.
|
|
19
|
+
*
|
|
20
|
+
* The bound is the input. A reload reconstructs from the persisted call alone, and a
|
|
21
|
+
* tool that describes its own RESULT names a fact that the input cannot carry — a page
|
|
22
|
+
* path, a recorded version. That line reaches the live surface on the finished event and
|
|
23
|
+
* it does not reappear here, thus a reloaded call falls back to its call detail. Nothing
|
|
24
|
+
* is persisted: storage stays the pure model transcript.
|
|
20
25
|
*/
|
|
21
26
|
import { computeDetail } from "../loop/tool-detail.js";
|
|
22
27
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
@@ -120,8 +120,8 @@ export declare const ReportBriefSchema: z.ZodObject<{
|
|
|
120
120
|
source: z.ZodString;
|
|
121
121
|
}, z.core.$strip>>;
|
|
122
122
|
chartType: z.ZodEnum<{
|
|
123
|
-
line: "line";
|
|
124
123
|
bar: "bar";
|
|
124
|
+
line: "line";
|
|
125
125
|
scatter: "scatter";
|
|
126
126
|
histogram: "histogram";
|
|
127
127
|
box: "box";
|
|
@@ -22,12 +22,13 @@ import type { Scope } from "../../auth/types.js";
|
|
|
22
22
|
import { type DraftRefusal } from "../../report-model/draft-operations.js";
|
|
23
23
|
import { type OutlineEntry, type ReadableBlock } from "../../report-model/draft-read.js";
|
|
24
24
|
import { type FinishResult } from "../../report-model/draft-finish.js";
|
|
25
|
-
import {
|
|
25
|
+
import type { DraftDocument } from "../../report-model/draft.js";
|
|
26
26
|
import type { ReportSnapshot } from "../../report-model/reference-resolver.js";
|
|
27
|
+
import type { DerivationRecord } from "../../state/report-session-state.js";
|
|
27
28
|
import { type Tool } from "../define-tool.js";
|
|
28
29
|
/** The flat destination fields. An anchor is `before` or `after`, and a place is `start` or `end`. */
|
|
29
30
|
declare const addBlockInput: z.ZodObject<{
|
|
30
|
-
block: z.ZodUnion<readonly [z.ZodType<import("../../report-model/
|
|
31
|
+
block: z.ZodUnion<readonly [z.ZodType<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown, z.core.$ZodTypeInternals<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown>>, z.ZodUnknown]>;
|
|
31
32
|
parentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
32
33
|
place: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
33
34
|
end: "end";
|
|
@@ -44,7 +45,7 @@ declare const addBlockInput: z.ZodObject<{
|
|
|
44
45
|
declare const changeBlockInput: z.ZodObject<{
|
|
45
46
|
targetId: z.ZodString;
|
|
46
47
|
title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
47
|
-
block: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<import("../../report-model/
|
|
48
|
+
block: z.ZodOptional<z.ZodUnion<readonly [z.ZodType<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown, z.core.$ZodTypeInternals<import("../../report-model/authoring-grammar.js").AuthoringBlock, unknown>>, z.ZodUnknown]>>;
|
|
48
49
|
}, z.core.$strip>;
|
|
49
50
|
declare const removeBlockInput: z.ZodObject<{
|
|
50
51
|
targetId: z.ZodString;
|
|
@@ -158,12 +159,17 @@ export interface ReportSessionState {
|
|
|
158
159
|
export type SessionStateToken = DraftDocument | null;
|
|
159
160
|
/**
|
|
160
161
|
* The outcome of a gateway load. `found` carries the state, the analysis that owns the thread, the
|
|
161
|
-
* concurrency token,
|
|
162
|
-
* are permanent conditions, and `failed` is a transient fault that names
|
|
162
|
+
* concurrency token, the seen-document hash of the look-before-record rule, and the derivations of the
|
|
163
|
+
* session. `absent` and `wrong-type` are permanent conditions, and `failed` is a transient fault that names
|
|
164
|
+
* its cause.
|
|
163
165
|
*
|
|
164
166
|
* `seenDocumentHash` is the hash that the last eyes capture saw, or `null` before the first look. The record
|
|
165
167
|
* tool compares it against the hash of the current draft, thus a never-seen page and a stale look each
|
|
166
168
|
* refuse.
|
|
169
|
+
*
|
|
170
|
+
* `derivations` is the ledger of the session, in the order that the records landed. The snapshot already
|
|
171
|
+
* carries each derived path as a member, thus a tool that binds reads the snapshot. The records themselves
|
|
172
|
+
* carry the chain, and the finish, the appendix, and the prune each read them.
|
|
167
173
|
*/
|
|
168
174
|
export type SessionStateLoad = {
|
|
169
175
|
outcome: "found";
|
|
@@ -171,6 +177,7 @@ export type SessionStateLoad = {
|
|
|
171
177
|
analysisId: string;
|
|
172
178
|
token: SessionStateToken;
|
|
173
179
|
seenDocumentHash: string | null;
|
|
180
|
+
derivations: readonly DerivationRecord[];
|
|
174
181
|
} | {
|
|
175
182
|
outcome: "absent";
|
|
176
183
|
} | {
|
|
@@ -243,7 +250,7 @@ export interface ReportSessionStateGateway {
|
|
|
243
250
|
}
|
|
244
251
|
/**
|
|
245
252
|
* The thread of a call, its analysis, the loaded state, the concurrency token that the persist compares
|
|
246
|
-
* against,
|
|
253
|
+
* against, the seen-document hash of the look-before-record rule, and the derivations of the session.
|
|
247
254
|
*/
|
|
248
255
|
export interface OpenedThread {
|
|
249
256
|
readonly threadId: string;
|
|
@@ -252,6 +259,8 @@ export interface OpenedThread {
|
|
|
252
259
|
readonly token: SessionStateToken;
|
|
253
260
|
/** The hash that the last eyes capture saw, or `null` before the first look. */
|
|
254
261
|
readonly seenDocumentHash: string | null;
|
|
262
|
+
/** The derivations of the session, in the order that the records landed. */
|
|
263
|
+
readonly derivations: readonly DerivationRecord[];
|
|
255
264
|
}
|
|
256
265
|
/**
|
|
257
266
|
* Resolve the report thread of a call, and load its state through the gateway.
|