@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
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* Report-session spawn — the host-agnostic operation that makes a `report`
|
|
3
3
|
* child thread from a conversation.
|
|
4
4
|
*
|
|
5
|
-
* The operation owns no table. It composes
|
|
6
|
-
* the thread history
|
|
7
|
-
* the
|
|
8
|
-
* `createThread`
|
|
9
|
-
* `
|
|
10
|
-
* `
|
|
5
|
+
* The operation owns no table. It composes the reads and the writes that the
|
|
6
|
+
* store, the thread history, and the working memory already give. `getThread`
|
|
7
|
+
* finds the parent, and `latestSeq` gives the anchor. `listThreads` counts the
|
|
8
|
+
* report children, and `createThread` writes the insert. `render` and
|
|
9
|
+
* `appendTurn` make the seed, and `purgeThread` removes a child that holds no
|
|
10
|
+
* seed. The two storage reads that this capability adds, `latestSeq` and
|
|
11
|
+
* `countUserTurnsAfter`, live on the thread history, because that module owns
|
|
12
|
+
* the `messages` table.
|
|
11
13
|
*
|
|
12
14
|
* The reads and the insert take no lock and no transaction. A concurrent turn
|
|
13
15
|
* can append between the anchor read and the insert, and a retract can cut the
|
|
@@ -21,17 +23,50 @@
|
|
|
21
23
|
*
|
|
22
24
|
* The spawn also gates on the eyes of the composition. A report session records
|
|
23
25
|
* a version only after a look at the rendered page, and only a browser can look.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* absent
|
|
26
|
+
* Three routes give a look: the eyes seam, the capture seam, and a chrome config
|
|
27
|
+
* that names a browser. A composition with none of the three can author a draft
|
|
28
|
+
* that no gate ever accepts. The spawn refuses that session before it writes a
|
|
29
|
+
* row, because a dead-end thread is worse than a refusal that names the absent
|
|
30
|
+
* capability.
|
|
31
|
+
*
|
|
32
|
+
* The spawn seeds the context of the child at the anchor. It writes one message
|
|
33
|
+
* that holds the brief and a copy of the working-memory render. The transcript
|
|
34
|
+
* is append-only, thus the copy stays frozen at the anchor by construction, and
|
|
35
|
+
* a later change to the working memory leaves it as it is. When the seed write
|
|
36
|
+
* fails, the spawn purges the child, because a report thread with no context is
|
|
37
|
+
* a dead end.
|
|
38
|
+
*
|
|
39
|
+
* The seed takes no redaction pass, and that is deliberate. The two parts are
|
|
40
|
+
* agent-authored, the same trust tier as the working-memory render of a
|
|
41
|
+
* conversation turn. The harness redacts the new input of a user one time, at
|
|
42
|
+
* the assembly of that turn. A second pass here would scrub text that the
|
|
43
|
+
* assembly already admitted, and it would report a false clean on the rest.
|
|
44
|
+
*
|
|
45
|
+
* The spawn mints one moment, and that moment carries two pins. The anchor pins
|
|
46
|
+
* the transcript of the parent, and the injected anchor operation pins the data
|
|
47
|
+
* of the analysis. A pin at the first tool call of a later turn reads a
|
|
48
|
+
* different moment, thus the session can cite an artifact that the anchor never
|
|
49
|
+
* held.
|
|
50
|
+
*
|
|
51
|
+
* The delta read gives the state that a caller reads before a new spawn. It
|
|
52
|
+
* gives the report child with the greatest anchor. It also gives the count of
|
|
53
|
+
* user turns of the parent past that anchor. The listing orders by `updated_at`
|
|
54
|
+
* and not by the anchor, thus the read walks each page of the children.
|
|
55
|
+
*
|
|
56
|
+
* The unit of the delta is a turn, and it is not a raw seq. A turn appends after
|
|
57
|
+
* its own loop runs, thus the anchor of a child sits below the rows of the ask
|
|
58
|
+
* that made it. A raw seq comparison would name each child stale one turn after
|
|
59
|
+
* the spawn.
|
|
28
60
|
*/
|
|
29
|
-
import {
|
|
61
|
+
import { ResultAsync } from "neverthrow";
|
|
30
62
|
import type { Pool } from "pg";
|
|
31
63
|
import type { DbError } from "../lib/db-result.js";
|
|
32
64
|
import { type ChromeConfig } from "../lib/chrome.js";
|
|
65
|
+
import type { AcquireEyes } from "../lib/eyes.js";
|
|
66
|
+
import type { Logger } from "../lib/logger.js";
|
|
33
67
|
import type { CapturePage } from "../lib/page-capture.js";
|
|
34
68
|
import { type Thread, type ThreadInputError, type ThreadPage, type ThreadType } from "../memory/thread-store.js";
|
|
69
|
+
import type { EnsureSessionStateResult } from "./report-session-runtime.js";
|
|
35
70
|
/**
|
|
36
71
|
* A spawn the operation refuses before it writes a row. Distinct from
|
|
37
72
|
* `DbError`: nothing failed at the driver — the parent names a session the
|
|
@@ -44,7 +79,8 @@ import { type Thread, type ThreadInputError, type ThreadPage, type ThreadType }
|
|
|
44
79
|
* report session cannot spawn another. `empty_parent_transcript` refuses a
|
|
45
80
|
* report on a parent that holds no messages, because such a report reports
|
|
46
81
|
* nothing. `no_browser` refuses every spawn under a composition that gives no
|
|
47
|
-
*
|
|
82
|
+
* route to a look: no eyes seam, no capture seam, and no configured browser
|
|
83
|
+
* endpoint. It carries the line that explains the absent capability.
|
|
48
84
|
*/
|
|
49
85
|
export type SpawnRefusal = {
|
|
50
86
|
readonly type: "no_browser";
|
|
@@ -70,6 +106,61 @@ export interface ReportSessionPaging {
|
|
|
70
106
|
readonly page?: number;
|
|
71
107
|
readonly perPage?: number;
|
|
72
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* The intent brief of one report session. The caller writes it at the moment of
|
|
111
|
+
* the ask, and each field is short prose.
|
|
112
|
+
*
|
|
113
|
+
* The brief carries intent alone. No field names a path, a dataset, or a format,
|
|
114
|
+
* because the report session reads those from the workspace itself.
|
|
115
|
+
*/
|
|
116
|
+
export interface ReportBrief {
|
|
117
|
+
/** The question that the report must answer. */
|
|
118
|
+
readonly objective: string;
|
|
119
|
+
/** The reader of the report. */
|
|
120
|
+
readonly audience: string;
|
|
121
|
+
/** The line that the report takes through the evidence. */
|
|
122
|
+
readonly angle: string;
|
|
123
|
+
/** The material that the report must leave out. */
|
|
124
|
+
readonly exclusions?: string;
|
|
125
|
+
/** The points that the user did not decide yet. */
|
|
126
|
+
readonly openQuestions?: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The report child that carries the greatest anchor. A tie on the anchor comes
|
|
130
|
+
* from two concurrent spawns, and the newest `createdAt` wins it.
|
|
131
|
+
*/
|
|
132
|
+
export interface NewestReportChild {
|
|
133
|
+
readonly threadId: string;
|
|
134
|
+
readonly title: string | null;
|
|
135
|
+
/** The `parentSeq` of the child — the point of the parent transcript that it was built on. */
|
|
136
|
+
readonly anchor: number;
|
|
137
|
+
readonly createdAt: Date;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The state that a caller reads before a new spawn. `newestChild` is `null` when
|
|
141
|
+
* the parent holds no live report child, and an archived child reads the same
|
|
142
|
+
* way, because a steer into hidden state is not permitted. An absent parent
|
|
143
|
+
* reads the same way.
|
|
144
|
+
*/
|
|
145
|
+
export interface ReportSessionDelta {
|
|
146
|
+
readonly newestChild: NewestReportChild | null;
|
|
147
|
+
/**
|
|
148
|
+
* The count of the parent turns that a person opened past the anchor of
|
|
149
|
+
* `newestChild` — the new work of the parent past that spawn point. A
|
|
150
|
+
* synthetic record of the host adds nothing to it.
|
|
151
|
+
*
|
|
152
|
+
* It is `null` when `newestChild` is `null`, because a parent with no child
|
|
153
|
+
* gives no anchor to count from. No count query runs in that case.
|
|
154
|
+
*/
|
|
155
|
+
readonly userTurnsSinceAnchor: number | null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The construction inputs of the report-session operations.
|
|
159
|
+
*
|
|
160
|
+
* Three of the fields name a route to a look at the rendered page: `eyes`,
|
|
161
|
+
* `capture`, and a `chrome` config that names a browser. One present route
|
|
162
|
+
* passes the gate of the spawn.
|
|
163
|
+
*/
|
|
73
164
|
export interface ReportSessionSpawnDeps {
|
|
74
165
|
readonly pool: Pool;
|
|
75
166
|
/**
|
|
@@ -84,29 +175,95 @@ export interface ReportSessionSpawnDeps {
|
|
|
84
175
|
* satisfies the gate on its own.
|
|
85
176
|
*/
|
|
86
177
|
readonly capture?: CapturePage;
|
|
178
|
+
/**
|
|
179
|
+
* The eyes seam that the same composition gives to the eyes tool. A bound
|
|
180
|
+
* seam gives a browser for one look, thus it satisfies the gate with no
|
|
181
|
+
* configured endpoint. The spawn reads only the presence of the seam, and
|
|
182
|
+
* it acquires no lease.
|
|
183
|
+
*/
|
|
184
|
+
readonly eyes?: AcquireEyes;
|
|
185
|
+
/**
|
|
186
|
+
* The anchor operation of the report session runtime. The spawn runs it
|
|
187
|
+
* after the seed of the child lands, thus the transcript anchor and the data
|
|
188
|
+
* snapshot pin at one moment.
|
|
189
|
+
*
|
|
190
|
+
* The dep is optional. A composition that binds none pins no snapshot at the
|
|
191
|
+
* spawn, and the first session tool call pins instead, because the operation
|
|
192
|
+
* is idempotent.
|
|
193
|
+
*/
|
|
194
|
+
readonly anchorSession?: (threadId: string) => Promise<EnsureSessionStateResult>;
|
|
195
|
+
/** Operational logging seam. An omitted logger falls back to the no-op. */
|
|
196
|
+
readonly logger?: Logger;
|
|
87
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Whether the composition gives a route to a look at the rendered page. The
|
|
200
|
+
* three routes are the eyes seam, the capture seam, and a chrome config that
|
|
201
|
+
* names a browser. One present route opens the gate.
|
|
202
|
+
*
|
|
203
|
+
* The spawn reads the rule before it writes a row. A caller that runs work
|
|
204
|
+
* before the spawn reads the same rule, thus it skips that work under a closed
|
|
205
|
+
* gate. The refusal itself stays with the spawn, and a caller copies no line of
|
|
206
|
+
* it.
|
|
207
|
+
*
|
|
208
|
+
* The parameter names the route fields alone. Thus a caller passes the deps
|
|
209
|
+
* value that it gives to `createReportSessionSpawn`, and the two answers cannot
|
|
210
|
+
* disagree.
|
|
211
|
+
*
|
|
212
|
+
* No type binds a new route to the parameter. Thus this predicate is the one
|
|
213
|
+
* gate expression of the module, and a new route belongs here and nowhere else.
|
|
214
|
+
*/
|
|
215
|
+
export declare function compositionHasEyes(deps: Pick<ReportSessionSpawnDeps, "chrome" | "capture" | "eyes">): boolean;
|
|
88
216
|
export interface ReportSessionSpawn {
|
|
89
217
|
/**
|
|
90
218
|
* Make a `report` child of the parent conversation and return the full row.
|
|
91
219
|
* The child takes the analysis of the parent, the parent thread id, and the
|
|
92
|
-
* anchor — the parent's latest `messages.seq` at this moment.
|
|
220
|
+
* anchor — the parent's latest `messages.seq` at this moment. It also takes
|
|
221
|
+
* one seed message that holds the brief and the working-memory render.
|
|
222
|
+
*
|
|
223
|
+
* The seed lands first, and the injected anchor operation then pins the data
|
|
224
|
+
* snapshot of the child. A failed pin keeps the child and it keeps this
|
|
225
|
+
* result on the ok channel, thus the outcome vocabulary holds no pin arm.
|
|
93
226
|
*
|
|
94
227
|
* Refused with a `SpawnRefusal`, no row written: a composition with no
|
|
95
228
|
* eyes, an absent or archived parent, a parent that is not a conversation,
|
|
96
229
|
* and a parent with no messages. A store refusal (`DbError`,
|
|
97
|
-
* `ThreadInputError`) passes through unchanged.
|
|
230
|
+
* `ThreadInputError`) passes through unchanged. A failed seed write purges
|
|
231
|
+
* the child and returns the fault, thus no context-less thread survives.
|
|
98
232
|
*/
|
|
99
|
-
spawnReportSession(parentThreadId: string): ResultAsync<Thread, SpawnRefusal | DbError | ThreadInputError>;
|
|
233
|
+
spawnReportSession(parentThreadId: string, brief: ReportBrief): ResultAsync<Thread, SpawnRefusal | DbError | ThreadInputError>;
|
|
100
234
|
/**
|
|
101
235
|
* The report sessions of one analysis, through the thread listing narrowed
|
|
102
236
|
* to the type `report`. It adds no predicate of its own, so its answer and
|
|
103
237
|
* the thread listing cannot disagree.
|
|
104
238
|
*/
|
|
105
239
|
listReportSessions(analysisId: string, paging?: ReportSessionPaging): ResultAsync<ThreadPage, DbError>;
|
|
240
|
+
/**
|
|
241
|
+
* The state that a caller reads before a new spawn. It gives the live report
|
|
242
|
+
* child of the parent with the greatest anchor. It also gives the count of
|
|
243
|
+
* the parent turns that a person opened past that anchor.
|
|
244
|
+
*
|
|
245
|
+
* The count admits one turn of new work by construction. A turn appends
|
|
246
|
+
* after its own loop runs, thus the anchor of a child sits below the rows of
|
|
247
|
+
* the ask that made it, and that ask counts as one.
|
|
248
|
+
*
|
|
249
|
+
* The read picks the child first, thus one anchor bounds the count. A parent
|
|
250
|
+
* with no child needs no anchor, and no count query runs. The read walks each
|
|
251
|
+
* page of the children listing, because the listing orders by `updated_at`
|
|
252
|
+
* and not by the anchor. It reads no model judgment, and it writes nothing.
|
|
253
|
+
*/
|
|
254
|
+
reportSessionDelta(parentThreadId: string): ResultAsync<ReportSessionDelta, DbError>;
|
|
106
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* The page size of the walk over the report children of one parent. The
|
|
258
|
+
* one-version policy keeps the count of children small, thus the walk reads one
|
|
259
|
+
* page on each real parent. It is exported because a test seeds one row more
|
|
260
|
+
* than one page, and a hard-coded count in the test would not follow a change
|
|
261
|
+
* here.
|
|
262
|
+
*/
|
|
263
|
+
export declare const REPORT_CHILD_PAGE_SIZE = 100;
|
|
107
264
|
/**
|
|
108
265
|
* Build the report-session operations bound to a Postgres pool. The factory
|
|
109
|
-
* closure captures `pool` and constructs the store
|
|
110
|
-
* it, the same way the chat-turn preparation does.
|
|
266
|
+
* closure captures `pool` and constructs the store, the thread history, and the
|
|
267
|
+
* working memory from it, the same way the chat-turn preparation does.
|
|
111
268
|
*/
|
|
112
269
|
export declare function createReportSessionSpawn(deps: ReportSessionSpawnDeps): ReportSessionSpawn;
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* Report-session spawn — the host-agnostic operation that makes a `report`
|
|
3
3
|
* child thread from a conversation.
|
|
4
4
|
*
|
|
5
|
-
* The operation owns no table. It composes
|
|
6
|
-
* the thread history
|
|
7
|
-
* the
|
|
8
|
-
* `createThread`
|
|
9
|
-
* `
|
|
10
|
-
* `
|
|
5
|
+
* The operation owns no table. It composes the reads and the writes that the
|
|
6
|
+
* store, the thread history, and the working memory already give. `getThread`
|
|
7
|
+
* finds the parent, and `latestSeq` gives the anchor. `listThreads` counts the
|
|
8
|
+
* report children, and `createThread` writes the insert. `render` and
|
|
9
|
+
* `appendTurn` make the seed, and `purgeThread` removes a child that holds no
|
|
10
|
+
* seed. The two storage reads that this capability adds, `latestSeq` and
|
|
11
|
+
* `countUserTurnsAfter`, live on the thread history, because that module owns
|
|
12
|
+
* the `messages` table.
|
|
11
13
|
*
|
|
12
14
|
* The reads and the insert take no lock and no transaction. A concurrent turn
|
|
13
15
|
* can append between the anchor read and the insert, and a retract can cut the
|
|
@@ -21,20 +23,80 @@
|
|
|
21
23
|
*
|
|
22
24
|
* The spawn also gates on the eyes of the composition. A report session records
|
|
23
25
|
* a version only after a look at the rendered page, and only a browser can look.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* absent
|
|
26
|
+
* Three routes give a look: the eyes seam, the capture seam, and a chrome config
|
|
27
|
+
* that names a browser. A composition with none of the three can author a draft
|
|
28
|
+
* that no gate ever accepts. The spawn refuses that session before it writes a
|
|
29
|
+
* row, because a dead-end thread is worse than a refusal that names the absent
|
|
30
|
+
* capability.
|
|
31
|
+
*
|
|
32
|
+
* The spawn seeds the context of the child at the anchor. It writes one message
|
|
33
|
+
* that holds the brief and a copy of the working-memory render. The transcript
|
|
34
|
+
* is append-only, thus the copy stays frozen at the anchor by construction, and
|
|
35
|
+
* a later change to the working memory leaves it as it is. When the seed write
|
|
36
|
+
* fails, the spawn purges the child, because a report thread with no context is
|
|
37
|
+
* a dead end.
|
|
38
|
+
*
|
|
39
|
+
* The seed takes no redaction pass, and that is deliberate. The two parts are
|
|
40
|
+
* agent-authored, the same trust tier as the working-memory render of a
|
|
41
|
+
* conversation turn. The harness redacts the new input of a user one time, at
|
|
42
|
+
* the assembly of that turn. A second pass here would scrub text that the
|
|
43
|
+
* assembly already admitted, and it would report a false clean on the rest.
|
|
44
|
+
*
|
|
45
|
+
* The spawn mints one moment, and that moment carries two pins. The anchor pins
|
|
46
|
+
* the transcript of the parent, and the injected anchor operation pins the data
|
|
47
|
+
* of the analysis. A pin at the first tool call of a later turn reads a
|
|
48
|
+
* different moment, thus the session can cite an artifact that the anchor never
|
|
49
|
+
* held.
|
|
50
|
+
*
|
|
51
|
+
* The delta read gives the state that a caller reads before a new spawn. It
|
|
52
|
+
* gives the report child with the greatest anchor. It also gives the count of
|
|
53
|
+
* user turns of the parent past that anchor. The listing orders by `updated_at`
|
|
54
|
+
* and not by the anchor, thus the read walks each page of the children.
|
|
55
|
+
*
|
|
56
|
+
* The unit of the delta is a turn, and it is not a raw seq. A turn appends after
|
|
57
|
+
* its own loop runs, thus the anchor of a child sits below the rows of the ask
|
|
58
|
+
* that made it. A raw seq comparison would name each child stale one turn after
|
|
59
|
+
* the spawn.
|
|
28
60
|
*/
|
|
29
61
|
import { randomUUID } from "node:crypto";
|
|
30
|
-
import { errAsync } from "neverthrow";
|
|
62
|
+
import { ResultAsync, errAsync, okAsync } from "neverthrow";
|
|
31
63
|
import { hasBrowserUrl } from "../lib/chrome.js";
|
|
32
|
-
import {
|
|
64
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
65
|
+
import { conversationRecordTurn, createThreadHistory } from "../memory/thread-history.js";
|
|
33
66
|
import { createThreadStore } from "../memory/thread-store.js";
|
|
67
|
+
import { createWorkingMemory } from "../memory/working-memory.js";
|
|
34
68
|
const _assertDomainError = true;
|
|
69
|
+
/**
|
|
70
|
+
* Whether the composition gives a route to a look at the rendered page. The
|
|
71
|
+
* three routes are the eyes seam, the capture seam, and a chrome config that
|
|
72
|
+
* names a browser. One present route opens the gate.
|
|
73
|
+
*
|
|
74
|
+
* The spawn reads the rule before it writes a row. A caller that runs work
|
|
75
|
+
* before the spawn reads the same rule, thus it skips that work under a closed
|
|
76
|
+
* gate. The refusal itself stays with the spawn, and a caller copies no line of
|
|
77
|
+
* it.
|
|
78
|
+
*
|
|
79
|
+
* The parameter names the route fields alone. Thus a caller passes the deps
|
|
80
|
+
* value that it gives to `createReportSessionSpawn`, and the two answers cannot
|
|
81
|
+
* disagree.
|
|
82
|
+
*
|
|
83
|
+
* No type binds a new route to the parameter. Thus this predicate is the one
|
|
84
|
+
* gate expression of the module, and a new route belongs here and nowhere else.
|
|
85
|
+
*/
|
|
86
|
+
export function compositionHasEyes(deps) {
|
|
87
|
+
return deps.eyes !== undefined || deps.capture !== undefined || hasBrowserUrl(deps.chrome);
|
|
88
|
+
}
|
|
35
89
|
const OP = "spawn-report-session";
|
|
36
90
|
/** The line that the caller reads when the composition gives no eyes. */
|
|
37
91
|
const NO_BROWSER_DETAIL = "the composition gives no browser, thus a report session can never record a version";
|
|
92
|
+
/**
|
|
93
|
+
* The page size of the walk over the report children of one parent. The
|
|
94
|
+
* one-version policy keeps the count of children small, thus the walk reads one
|
|
95
|
+
* page on each real parent. It is exported because a test seeds one row more
|
|
96
|
+
* than one page, and a hard-coded count in the test would not follow a change
|
|
97
|
+
* here.
|
|
98
|
+
*/
|
|
99
|
+
export const REPORT_CHILD_PAGE_SIZE = 100;
|
|
38
100
|
/**
|
|
39
101
|
* Compose the child title. N counts the existing report children of the parent
|
|
40
102
|
* plus one. A parent with no title yields `Report N` alone, because the seed of
|
|
@@ -44,19 +106,144 @@ function composeTitle(parentTitle, n) {
|
|
|
44
106
|
const suffix = `Report ${n}`;
|
|
45
107
|
return parentTitle && parentTitle.length > 0 ? `${parentTitle} — ${suffix}` : suffix;
|
|
46
108
|
}
|
|
109
|
+
/** Whether the caller gave text for an optional field of the brief. */
|
|
110
|
+
function hasText(field) {
|
|
111
|
+
return field !== undefined && field.trim().length > 0;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Compose the seed message: the brief as short labeled prose, then the copy of
|
|
115
|
+
* the working-memory render. An empty render adds nothing, because
|
|
116
|
+
* `renderWorkingMemory` gives the empty string for a memory with no entry.
|
|
117
|
+
*/
|
|
118
|
+
function composeSeed(brief, workingMemoryRender) {
|
|
119
|
+
const lines = [`Objective: ${brief.objective}`, `Audience: ${brief.audience}`, `Angle: ${brief.angle}`];
|
|
120
|
+
if (hasText(brief.exclusions))
|
|
121
|
+
lines.push(`Exclusions: ${brief.exclusions}`);
|
|
122
|
+
if (hasText(brief.openQuestions))
|
|
123
|
+
lines.push(`Open questions: ${brief.openQuestions}`);
|
|
124
|
+
const briefBlock = `# Report Brief\n\n${lines.join("\n")}\n`;
|
|
125
|
+
const memory = workingMemoryRender.trim();
|
|
126
|
+
return memory.length === 0 ? briefBlock : `${briefBlock}\n${memory}\n`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The child with the greatest anchor, or `null` for no candidate. Two concurrent
|
|
130
|
+
* spawns can write one anchor, and the newest `createdAt` then wins.
|
|
131
|
+
*/
|
|
132
|
+
function pickNewestChild(children) {
|
|
133
|
+
let newest = null;
|
|
134
|
+
for (const child of children) {
|
|
135
|
+
// The store writes `parentThreadId` and `parentSeq` as one pair, thus a
|
|
136
|
+
// child with no anchor names no point of the parent transcript and it
|
|
137
|
+
// cannot advise.
|
|
138
|
+
if (child.parentSeq === null)
|
|
139
|
+
continue;
|
|
140
|
+
const candidate = {
|
|
141
|
+
threadId: child.threadId,
|
|
142
|
+
title: child.title,
|
|
143
|
+
anchor: child.parentSeq,
|
|
144
|
+
createdAt: child.createdAt,
|
|
145
|
+
};
|
|
146
|
+
const wins = newest === null ||
|
|
147
|
+
candidate.anchor > newest.anchor ||
|
|
148
|
+
(candidate.anchor === newest.anchor && candidate.createdAt.getTime() > newest.createdAt.getTime());
|
|
149
|
+
if (wins)
|
|
150
|
+
newest = candidate;
|
|
151
|
+
}
|
|
152
|
+
return newest;
|
|
153
|
+
}
|
|
47
154
|
/**
|
|
48
155
|
* Build the report-session operations bound to a Postgres pool. The factory
|
|
49
|
-
* closure captures `pool` and constructs the store
|
|
50
|
-
* it, the same way the chat-turn preparation does.
|
|
156
|
+
* closure captures `pool` and constructs the store, the thread history, and the
|
|
157
|
+
* working memory from it, the same way the chat-turn preparation does.
|
|
51
158
|
*/
|
|
52
159
|
export function createReportSessionSpawn(deps) {
|
|
53
160
|
const { pool } = deps;
|
|
161
|
+
const log = (deps.logger ?? createNoopLogger()).named("spawn-report-session");
|
|
54
162
|
const store = createThreadStore(pool);
|
|
55
163
|
const history = createThreadHistory(pool);
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
164
|
+
const workingMemory = createWorkingMemory(pool);
|
|
165
|
+
// The routes are fixed at construction, thus the gate reads one boolean and
|
|
166
|
+
// never a live probe of the sidecar.
|
|
167
|
+
const eyesAvailable = compositionHasEyes(deps);
|
|
168
|
+
/**
|
|
169
|
+
* Write the one seed message of the child and give the child back. The
|
|
170
|
+
* message rides as a record, not as user input: nobody typed it in the child
|
|
171
|
+
* thread, and a record is displayed but it never opens a turn.
|
|
172
|
+
*/
|
|
173
|
+
function seedChildContext(child, brief) {
|
|
174
|
+
return workingMemory
|
|
175
|
+
.render(child.analysisId)
|
|
176
|
+
.andThen((memory) => history.appendTurn(child.threadId, conversationRecordTurn(composeSeed(brief, memory))))
|
|
177
|
+
.map(() => child)
|
|
178
|
+
.orElse((fault) =>
|
|
179
|
+
// A report thread with no context is a dead end, thus the purge
|
|
180
|
+
// removes the child. The seed fault is the cause, thus it returns
|
|
181
|
+
// from each route. A failed purge leaves the child, and the caller
|
|
182
|
+
// still reads why the seed stopped.
|
|
183
|
+
store
|
|
184
|
+
.purgeThread(child.threadId)
|
|
185
|
+
.andThen(() => errAsync(fault))
|
|
186
|
+
.orElse(() => errAsync(fault)));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Pin the data snapshot of the child, through the injected anchor operation.
|
|
190
|
+
* The spawn mints one moment, and the transcript anchor and this snapshot are
|
|
191
|
+
* the two pins of that moment.
|
|
192
|
+
*
|
|
193
|
+
* The pin runs after the seed. A failed seed purges the child, and
|
|
194
|
+
* `cortex_report_session_state` carries no foreign key to the thread table,
|
|
195
|
+
* thus a pin before a purged seed would leave an orphan row.
|
|
196
|
+
*
|
|
197
|
+
* A failed pin keeps the child. The operation is idempotent, thus the first
|
|
198
|
+
* session tool call pins again, and a purge on a transient store fault would
|
|
199
|
+
* cost the user the whole session. Thus the failure rides the logger alone.
|
|
200
|
+
*/
|
|
201
|
+
async function pinSessionSnapshot(child) {
|
|
202
|
+
const anchor = deps.anchorSession;
|
|
203
|
+
if (anchor === undefined)
|
|
204
|
+
return child;
|
|
205
|
+
try {
|
|
206
|
+
const pinned = await anchor(child.threadId);
|
|
207
|
+
if (pinned.outcome === "failed") {
|
|
208
|
+
log.warn("the snapshot pin of the new session failed", {
|
|
209
|
+
threadId: child.threadId,
|
|
210
|
+
analysisId: child.analysisId,
|
|
211
|
+
kind: pinned.kind,
|
|
212
|
+
detail: pinned.detail,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (cause) {
|
|
217
|
+
// The dep speaks a promise, thus a foreign realization can reject.
|
|
218
|
+
// The spawn keeps the child on that route as well.
|
|
219
|
+
log.warn("the snapshot pin of the new session did not complete", {
|
|
220
|
+
threadId: child.threadId,
|
|
221
|
+
analysisId: child.analysisId,
|
|
222
|
+
...log.errorFields(cause),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return child;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Each live report child of the parent, across every page of the listing. The
|
|
229
|
+
* listing orders by `updated_at`, thus one page can hide the child with the
|
|
230
|
+
* greatest anchor. The default listing excludes an archived child, and this
|
|
231
|
+
* walk keeps that scope.
|
|
232
|
+
*/
|
|
233
|
+
function collectReportChildren(analysisId, parentThreadId, page, found) {
|
|
234
|
+
return store
|
|
235
|
+
.listThreads({ analysisId, type: "report", parentThreadId, page, perPage: REPORT_CHILD_PAGE_SIZE })
|
|
236
|
+
.andThen((result) => {
|
|
237
|
+
const all = [...found, ...result.threads];
|
|
238
|
+
// An empty page also stops the walk. `hasMore` compares the offset
|
|
239
|
+
// against a count from a second statement, thus a concurrent purge
|
|
240
|
+
// can hold the flag true after the last row.
|
|
241
|
+
if (!result.hasMore || result.threads.length === 0)
|
|
242
|
+
return okAsync(all);
|
|
243
|
+
return collectReportChildren(analysisId, parentThreadId, page + 1, all);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function spawnReportSession(parentThreadId, brief) {
|
|
60
247
|
// The gate runs before the parent read. A session with no route to a look
|
|
61
248
|
// reaches the record gate and refuses there forever, thus the spawn is the
|
|
62
249
|
// one honest place to say so.
|
|
@@ -84,19 +271,44 @@ export function createReportSessionSpawn(deps) {
|
|
|
84
271
|
// The result is two titles a user renames, and no identifier collides.
|
|
85
272
|
return store
|
|
86
273
|
.listThreads({ analysisId: parent.analysisId, type: "report", parentThreadId })
|
|
87
|
-
.andThen((children) => store
|
|
274
|
+
.andThen((children) => store
|
|
275
|
+
.createThread({
|
|
88
276
|
threadId: randomUUID(),
|
|
89
277
|
analysisId: parent.analysisId,
|
|
90
278
|
title: composeTitle(parent.title, children.total + 1),
|
|
91
279
|
type: "report",
|
|
92
280
|
parentThreadId,
|
|
93
281
|
parentSeq: anchor,
|
|
94
|
-
})
|
|
282
|
+
})
|
|
283
|
+
// The seed follows the insert, thus the copy of the working
|
|
284
|
+
// memory is the state at the anchor.
|
|
285
|
+
.andThen((child) => seedChildContext(child, brief))
|
|
286
|
+
// The pin follows the seed. `pinSessionSnapshot` never
|
|
287
|
+
// rejects, thus the safe bridge adds no error variant.
|
|
288
|
+
.andThen((child) => ResultAsync.fromSafePromise(pinSessionSnapshot(child))));
|
|
95
289
|
});
|
|
96
290
|
});
|
|
97
291
|
}
|
|
98
292
|
function listReportSessions(analysisId, paging = {}) {
|
|
99
293
|
return store.listThreads({ analysisId, type: "report", ...paging });
|
|
100
294
|
}
|
|
101
|
-
|
|
295
|
+
function reportSessionDelta(parentThreadId) {
|
|
296
|
+
// The children listing takes the analysis of the parent, thus the parent
|
|
297
|
+
// read comes first. An absent or archived parent gives no advice, and the
|
|
298
|
+
// spawn refuses it later with `parent_not_found`.
|
|
299
|
+
return store.getThread(parentThreadId).andThen((parent) => {
|
|
300
|
+
if (parent === null)
|
|
301
|
+
return okAsync({ newestChild: null, userTurnsSinceAnchor: null });
|
|
302
|
+
// The child comes first, because the anchor of that child bounds the
|
|
303
|
+
// count. A parent with no child gives no anchor, thus the read stops
|
|
304
|
+
// here and the count query never runs.
|
|
305
|
+
return collectReportChildren(parent.analysisId, parentThreadId, 0, []).andThen((children) => {
|
|
306
|
+
const newestChild = pickNewestChild(children);
|
|
307
|
+
if (newestChild === null)
|
|
308
|
+
return okAsync({ newestChild: null, userTurnsSinceAnchor: null });
|
|
309
|
+
return history.countUserTurnsAfter(parentThreadId, newestChild.anchor).map((userTurnsSinceAnchor) => ({ newestChild, userTurnsSinceAnchor }));
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return { spawnReportSession, listReportSessions, reportSessionDelta };
|
|
102
314
|
}
|
|
@@ -55,7 +55,7 @@ export type ToolOutcome = "ok" | "error" | "denied";
|
|
|
55
55
|
export type ToolCallOutcome = ToolOutcome | "incomplete";
|
|
56
56
|
/**
|
|
57
57
|
* One line naming what a tool call is doing, produced by the tool's own
|
|
58
|
-
* `describeCall` hook and normalized at the emit site.
|
|
58
|
+
* `describeCall` or `describeResult` hook and normalized at the emit site.
|
|
59
59
|
*
|
|
60
60
|
* Opaque display text. A consumer renders it and derives nothing from it — no
|
|
61
61
|
* splitting, no keying on separators. The contract is harness-owned so it can
|
|
@@ -82,7 +82,12 @@ export interface ToolFinishedEvent {
|
|
|
82
82
|
name: string;
|
|
83
83
|
/** How the call ended. See {@link ToolOutcome}. */
|
|
84
84
|
outcome: ToolOutcome;
|
|
85
|
-
/**
|
|
85
|
+
/**
|
|
86
|
+
* See {@link ToolCallDetail}. It can differ from the detail the matching
|
|
87
|
+
* `tool-started` carried: a tool that describes its own result names the
|
|
88
|
+
* outcome here, and a call that produced none carries the started detail
|
|
89
|
+
* again.
|
|
90
|
+
*/
|
|
86
91
|
detail?: ToolCallDetail;
|
|
87
92
|
/**
|
|
88
93
|
* The time in milliseconds around this call's own dispatch. Absent — never
|
|
@@ -64,4 +64,14 @@ export interface CortexMessage {
|
|
|
64
64
|
* nothing" stay distinguishable.
|
|
65
65
|
*/
|
|
66
66
|
usage?: TokenUsageRollup;
|
|
67
|
+
/**
|
|
68
|
+
* How long the TURN this message completed took, in milliseconds, as the live
|
|
69
|
+
* header measured it. It rides beside {@link usage} and it carries the same
|
|
70
|
+
* rule: only the assistant message that ended a turn holds one.
|
|
71
|
+
*
|
|
72
|
+
* Absent means that nobody measured the turn — a message stored before the
|
|
73
|
+
* duration was persisted, or any message that did not end a turn. A measured
|
|
74
|
+
* zero is a figure, thus it stays a value and it never reads as an absence.
|
|
75
|
+
*/
|
|
76
|
+
durationMs?: number;
|
|
67
77
|
}
|