@inflexa-ai/harness 0.20.0 → 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/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/mount-plan.d.ts +45 -7
- package/dist/sandbox/mount-plan.js +43 -5
- package/dist/sandbox/types.d.ts +6 -0
- 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/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/workspace/paths.d.ts +50 -0
- package/dist/workspace/paths.js +63 -0
- package/package.json +2 -1
|
@@ -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
|
}
|
|
@@ -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.
|
|
@@ -17,27 +17,33 @@
|
|
|
17
17
|
* nested union. A conflict between the flat fields refuses before the core runs.
|
|
18
18
|
*/
|
|
19
19
|
import { err, ok } from "neverthrow";
|
|
20
|
+
import { basename } from "node:path";
|
|
20
21
|
import { z } from "zod";
|
|
22
|
+
import { capCodePoints, DETAIL_NEEDLE_MAX_LENGTH } from "../../loop/tool-detail.js";
|
|
21
23
|
import { isSafeId } from "../../workspace/paths.js";
|
|
22
24
|
import { addBlock, changeBlock, locate, moveBlock, removeBlock, setTitle, } from "../../report-model/draft-operations.js";
|
|
23
25
|
import { buildOutline, childOutline, readBlock } from "../../report-model/draft-read.js";
|
|
24
26
|
import { finishDraft } from "../../report-model/draft-finish.js";
|
|
25
|
-
import {
|
|
27
|
+
import { AuthoringBlockSchema } from "../../report-model/authoring-grammar.js";
|
|
26
28
|
import { defineTool } from "../define-tool.js";
|
|
27
29
|
/**
|
|
28
30
|
* The block payload.
|
|
29
31
|
*
|
|
30
|
-
* The published JSON Schema carries the whole
|
|
31
|
-
* time and the schema is the only place where the model learns the shape of the eight block kinds.
|
|
32
|
-
* `z.unknown()` emits the empty schema, which tells the model nothing and leaves it to discover each
|
|
32
|
+
* The published JSON Schema carries the whole authoring grammar, because a tool is self-describing at
|
|
33
|
+
* attach time and the schema is the only place where the model learns the shape of the eight block kinds.
|
|
34
|
+
* A bare `z.unknown()` emits the empty schema, which tells the model nothing and leaves it to discover each
|
|
33
35
|
* required field through one refusal at a time.
|
|
34
36
|
*
|
|
37
|
+
* The grammar is the authoring one, thus no artifact reference of the published schema carries a hash. The
|
|
38
|
+
* pinned snapshot owns that value, and the land path stamps it from the path.
|
|
39
|
+
*
|
|
35
40
|
* The `z.unknown()` member keeps the runtime parse permissive. Thus a malformed payload still reaches the
|
|
36
|
-
* core, which parses it with the
|
|
41
|
+
* core, which parses it with the draft grammar and refuses `malformed-block` as typed data in the ok
|
|
37
42
|
* channel. A strict input schema would turn that designed refusal into a hard input-validation error from
|
|
38
|
-
* the loop.
|
|
43
|
+
* the loop. A payload that carries a hash rides the same arm, and the stamp drops the value before the
|
|
44
|
+
* parse.
|
|
39
45
|
*/
|
|
40
|
-
const blockPayload = z.union([
|
|
46
|
+
const blockPayload = z.union([AuthoringBlockSchema, z.unknown()]);
|
|
41
47
|
/**
|
|
42
48
|
* A flat field that the model can leave out.
|
|
43
49
|
*
|
|
@@ -119,7 +125,14 @@ export async function openReportThread(gateway, scope) {
|
|
|
119
125
|
detail: `the scope names the analysis ${analysisId}, but the thread ${threadId} belongs to the analysis ${loaded.analysisId}`,
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
|
-
return ok({
|
|
128
|
+
return ok({
|
|
129
|
+
threadId,
|
|
130
|
+
analysisId,
|
|
131
|
+
state: loaded.state,
|
|
132
|
+
token: loaded.token,
|
|
133
|
+
seenDocumentHash: loaded.seenDocumentHash,
|
|
134
|
+
derivations: loaded.derivations,
|
|
135
|
+
});
|
|
123
136
|
}
|
|
124
137
|
/** Read an absentable field. An explicit `null` and an omitted key both mean that the model gave no value. */
|
|
125
138
|
function given(value) {
|
|
@@ -208,15 +221,75 @@ function readKind(block) {
|
|
|
208
221
|
function readId(block) {
|
|
209
222
|
return readStringField(block, "id");
|
|
210
223
|
}
|
|
224
|
+
/** Read one field of an untyped payload, or `undefined` when the payload is not an object. */
|
|
225
|
+
function fieldOf(value, field) {
|
|
226
|
+
return value !== null && typeof value === "object" ? value[field] : undefined;
|
|
227
|
+
}
|
|
211
228
|
function readStringField(block, field) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
229
|
+
const value = fieldOf(block, field);
|
|
230
|
+
return typeof value === "string" ? value : undefined;
|
|
231
|
+
}
|
|
232
|
+
/** The pinned path of an untyped reference payload, or `undefined` when it names none. */
|
|
233
|
+
function referencePath(reference) {
|
|
234
|
+
const path = readStringField(reference, "path");
|
|
235
|
+
return path === undefined || path.length === 0 ? undefined : path;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* The artifact path that an untyped block payload binds, or `undefined` when the kind binds no path.
|
|
239
|
+
*
|
|
240
|
+
* Where a reference sits differs per kind, thus the walk reads the one field of each kind and never
|
|
241
|
+
* searches the payload. A claim can hold a citation first, which names no path, thus the walk takes the
|
|
242
|
+
* first binding that names one. A metric holds a scalar reference, and a derivation holds its own two
|
|
243
|
+
* inputs, thus the first input names the derived value. A text block, a citation block, and a section
|
|
244
|
+
* bind no artifact at all.
|
|
245
|
+
*
|
|
246
|
+
* The payload is model-authored and unvalidated here, thus each read is a guarded read and a wrong shape
|
|
247
|
+
* gives `undefined`.
|
|
248
|
+
*/
|
|
249
|
+
function boundArtifactPath(kind, block) {
|
|
250
|
+
if (kind === "table" || kind === "chart" || kind === "figure") {
|
|
251
|
+
return referencePath(fieldOf(block, "binding"));
|
|
252
|
+
}
|
|
253
|
+
if (kind === "claim") {
|
|
254
|
+
const bindings = fieldOf(block, "bindings");
|
|
255
|
+
if (!Array.isArray(bindings)) {
|
|
256
|
+
return undefined;
|
|
216
257
|
}
|
|
258
|
+
for (const binding of bindings) {
|
|
259
|
+
const path = referencePath(binding);
|
|
260
|
+
if (path !== undefined) {
|
|
261
|
+
return path;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
if (kind === "metric") {
|
|
267
|
+
const value = fieldOf(block, "value");
|
|
268
|
+
const direct = referencePath(value);
|
|
269
|
+
if (direct !== undefined) {
|
|
270
|
+
return direct;
|
|
271
|
+
}
|
|
272
|
+
const inputs = fieldOf(value, "inputs");
|
|
273
|
+
return Array.isArray(inputs) ? referencePath(inputs[0]) : undefined;
|
|
217
274
|
}
|
|
218
275
|
return undefined;
|
|
219
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* The subject that names one added block: the title of a section in quotes, or the file name of the
|
|
279
|
+
* artifact that the block binds. A block that names neither gives no subject, and the kind stands alone.
|
|
280
|
+
*
|
|
281
|
+
* The title is model-authored text of no fixed length, thus the needle bound cuts it before the emit-site
|
|
282
|
+
* cap can cut the quote that closes it. The file name is the base name alone, because the directory
|
|
283
|
+
* segments of an analysis path repeat across every block and the file name is what tells two apart.
|
|
284
|
+
*/
|
|
285
|
+
function addedSubject(kind, block) {
|
|
286
|
+
if (kind === "section") {
|
|
287
|
+
const title = readStringField(block, "title");
|
|
288
|
+
return title === undefined || title.length === 0 ? undefined : `"${capCodePoints(title, DETAIL_NEEDLE_MAX_LENGTH)}"`;
|
|
289
|
+
}
|
|
290
|
+
const path = boundArtifactPath(kind, block);
|
|
291
|
+
return path === undefined ? undefined : basename(path);
|
|
292
|
+
}
|
|
220
293
|
/**
|
|
221
294
|
* The id of the section that holds a block, or `undefined` when the block sits at the root.
|
|
222
295
|
*
|
|
@@ -299,12 +372,18 @@ export function createReportAuthoringTools(gateway) {
|
|
|
299
372
|
description: "Add one block to the draft. The `block` schema gives the eight kinds and the fields of each one. " +
|
|
300
373
|
"You choose the `id` of the block, and it must be unique in the draft. " +
|
|
301
374
|
"Name the destination with `parentId` and one of `place`, `before`, or `after`. " +
|
|
302
|
-
"The root admits a section only, and an atom needs a section as its parent."
|
|
375
|
+
"The root admits a section only, and an atom needs a section as its parent. " +
|
|
376
|
+
"A reference names the path of a pinned artifact, and the session stamps the hash from the pinned evidence.",
|
|
303
377
|
inputSchema: addBlockInput,
|
|
304
378
|
executionMode: AUTHORING_EXECUTION_MODE,
|
|
305
379
|
describeCall: (input) => {
|
|
306
|
-
const
|
|
307
|
-
|
|
380
|
+
const block = decodeBlockPayload(input.block);
|
|
381
|
+
const kind = readKind(block);
|
|
382
|
+
if (kind === undefined) {
|
|
383
|
+
return "add a block";
|
|
384
|
+
}
|
|
385
|
+
const subject = addedSubject(kind, block);
|
|
386
|
+
return subject === undefined ? `add ${kind}` : `add ${kind} ${subject}`;
|
|
308
387
|
},
|
|
309
388
|
execute: async (input, ctx) => {
|
|
310
389
|
const opened = await openThread(ctx);
|
|
@@ -324,7 +403,8 @@ export function createReportAuthoringTools(gateway) {
|
|
|
324
403
|
const change_block = defineTool({
|
|
325
404
|
id: "change_block",
|
|
326
405
|
description: "Change one block by its id. Give a `title` to retitle a section, or a `block` to replace an atom, and never both. " +
|
|
327
|
-
"The change keeps the id of the target, thus the `id` of the payload does not matter. A kind change is permitted."
|
|
406
|
+
"The change keeps the id of the target, thus the `id` of the payload does not matter. A kind change is permitted. " +
|
|
407
|
+
"A reference names the path of a pinned artifact, and the session stamps the hash from the pinned evidence.",
|
|
328
408
|
inputSchema: changeBlockInput,
|
|
329
409
|
executionMode: AUTHORING_EXECUTION_MODE,
|
|
330
410
|
describeCall: (input) => `change ${input.targetId}`,
|
|
@@ -416,7 +496,8 @@ export function createReportAuthoringTools(gateway) {
|
|
|
416
496
|
});
|
|
417
497
|
const read_block = defineTool({
|
|
418
498
|
id: "read_block",
|
|
419
|
-
description: "Read one block by its id. An atom gives the full block with its bindings. A
|
|
499
|
+
description: "Read one block by its id. An atom gives the full block with its bindings. A binding gives its path and no hash, " +
|
|
500
|
+
"because the session owns the hash. A section gives its title and the id of each child, " +
|
|
420
501
|
"because the outline already names every block under it. Use it when the outline label is not enough.",
|
|
421
502
|
inputSchema: readBlockInput,
|
|
422
503
|
executionMode: AUTHORING_EXECUTION_MODE,
|
|
@@ -444,7 +525,7 @@ export function createReportAuthoringTools(gateway) {
|
|
|
444
525
|
if (opened.isErr()) {
|
|
445
526
|
return ok({ refused: opened.error });
|
|
446
527
|
}
|
|
447
|
-
return ok(finishDraft(opened.value.state.document, opened.value.state.snapshot));
|
|
528
|
+
return ok(finishDraft(opened.value.state.document, opened.value.state.snapshot, opened.value.derivations));
|
|
448
529
|
},
|
|
449
530
|
});
|
|
450
531
|
return {
|