@inflexa-ai/harness 0.20.0 → 0.21.1

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.
Files changed (133) hide show
  1. package/dist/agents/conversation-agent.d.ts +26 -14
  2. package/dist/agents/conversation-agent.js +12 -23
  3. package/dist/agents/report-session-agent.d.ts +31 -5
  4. package/dist/agents/report-session-agent.js +35 -9
  5. package/dist/app/chat-turn.js +3 -1
  6. package/dist/app/message-assembly.d.ts +15 -1
  7. package/dist/app/message-assembly.js +22 -8
  8. package/dist/app/report-session-runtime.d.ts +21 -4
  9. package/dist/app/report-session-runtime.js +53 -6
  10. package/dist/app/spawn-report-session.d.ts +174 -17
  11. package/dist/app/spawn-report-session.js +233 -21
  12. package/dist/contracts/chat-events.d.ts +7 -2
  13. package/dist/contracts/message.d.ts +10 -0
  14. package/dist/contracts/report-blocks.d.ts +743 -19
  15. package/dist/contracts/report-blocks.js +186 -11
  16. package/dist/contracts/report-reference.d.ts +88 -0
  17. package/dist/contracts/report-reference.js +48 -0
  18. package/dist/index.d.ts +9 -0
  19. package/dist/index.js +20 -0
  20. package/dist/lib/chrome.d.ts +26 -6
  21. package/dist/lib/chrome.js +99 -32
  22. package/dist/lib/eyes.d.ts +57 -0
  23. package/dist/lib/eyes.js +45 -0
  24. package/dist/lib/page-capture.d.ts +14 -0
  25. package/dist/lib/page-capture.js +56 -2
  26. package/dist/loop/run-agent.js +58 -23
  27. package/dist/loop/tool-detail.d.ts +19 -2
  28. package/dist/loop/tool-detail.js +35 -2
  29. package/dist/memory/content-to-cortex.d.ts +3 -2
  30. package/dist/memory/content-to-cortex.js +21 -12
  31. package/dist/memory/conversation-display-replay.js +14 -7
  32. package/dist/memory/thread-history.d.ts +60 -3
  33. package/dist/memory/thread-history.js +55 -16
  34. package/dist/memory/thread-store.d.ts +11 -1
  35. package/dist/memory/thread-store.js +12 -2
  36. package/dist/prompts/conversation.d.ts +1 -1
  37. package/dist/prompts/conversation.js +23 -6
  38. package/dist/prompts/report-session.d.ts +1 -1
  39. package/dist/prompts/report-session.js +190 -9
  40. package/dist/report-model/authoring-grammar.d.ts +463 -0
  41. package/dist/report-model/authoring-grammar.js +77 -0
  42. package/dist/report-model/block-walk.d.ts +57 -14
  43. package/dist/report-model/block-walk.js +116 -11
  44. package/dist/report-model/draft-finish.d.ts +22 -4
  45. package/dist/report-model/draft-finish.js +39 -7
  46. package/dist/report-model/draft-operations.d.ts +3 -0
  47. package/dist/report-model/draft-operations.js +19 -5
  48. package/dist/report-model/draft-read.d.ts +8 -5
  49. package/dist/report-model/draft-read.js +21 -5
  50. package/dist/report-model/fixture-resolver.js +15 -4
  51. package/dist/report-model/pin-snapshot.d.ts +24 -6
  52. package/dist/report-model/pin-snapshot.js +204 -6
  53. package/dist/report-model/production-resolver.js +15 -4
  54. package/dist/report-model/reference-resolver.d.ts +50 -1
  55. package/dist/report-model/reference-resolver.js +94 -1
  56. package/dist/report-model/reference-stamp.d.ts +45 -0
  57. package/dist/report-model/reference-stamp.js +173 -0
  58. package/dist/report-model/structural-validation.d.ts +4 -1
  59. package/dist/report-model/structural-validation.js +48 -3
  60. package/dist/report-model/validate.d.ts +1 -1
  61. package/dist/report-model/validate.js +1 -1
  62. package/dist/report-render/asset-lookup.d.ts +19 -0
  63. package/dist/report-render/asset-lookup.js +37 -0
  64. package/dist/report-render/assets.d.ts +45 -1
  65. package/dist/report-render/assets.js +65 -6
  66. package/dist/report-render/chart-presets.d.ts +102 -0
  67. package/dist/report-render/chart-presets.js +205 -0
  68. package/dist/report-render/chart.d.ts +112 -10
  69. package/dist/report-render/chart.js +1164 -41
  70. package/dist/report-render/design.d.ts +99 -0
  71. package/dist/report-render/design.js +289 -107
  72. package/dist/report-render/number-format.d.ts +117 -0
  73. package/dist/report-render/number-format.js +479 -0
  74. package/dist/report-render/page.d.ts +121 -5
  75. package/dist/report-render/page.js +689 -7
  76. package/dist/report-render/references.d.ts +70 -16
  77. package/dist/report-render/references.js +54 -22
  78. package/dist/report-render/render.d.ts +23 -5
  79. package/dist/report-render/render.js +81 -22
  80. package/dist/report-render/table-data.d.ts +92 -0
  81. package/dist/report-render/table-data.js +129 -0
  82. package/dist/report-render/types.d.ts +15 -0
  83. package/dist/report-render/value-bridge.js +6 -2
  84. package/dist/report-render/views/chart-view.d.ts +12 -5
  85. package/dist/report-render/views/chart-view.js +14 -8
  86. package/dist/report-render/views/page-view.d.ts +36 -15
  87. package/dist/report-render/views/page-view.js +45 -23
  88. package/dist/report-render/views/prose.d.ts +12 -2
  89. package/dist/report-render/views/prose.js +29 -7
  90. package/dist/report-render/views/references-view.d.ts +22 -8
  91. package/dist/report-render/views/references-view.js +101 -13
  92. package/dist/report-render/views/values.d.ts +128 -13
  93. package/dist/report-render/views/values.js +199 -18
  94. package/dist/runtime/assemble.d.ts +39 -5
  95. package/dist/runtime/assemble.js +47 -6
  96. package/dist/sandbox/client.d.ts +10 -1
  97. package/dist/sandbox/create-sandbox.d.ts +9 -4
  98. package/dist/sandbox/create-sandbox.js +22 -16
  99. package/dist/sandbox/docker-client.js +10 -7
  100. package/dist/sandbox/mount-plan.d.ts +45 -7
  101. package/dist/sandbox/mount-plan.js +43 -5
  102. package/dist/sandbox/types.d.ts +6 -0
  103. package/dist/state/init.js +38 -9
  104. package/dist/state/report-session-state.d.ts +42 -1
  105. package/dist/state/report-session-state.js +70 -2
  106. package/dist/state/report-versions.d.ts +22 -17
  107. package/dist/state/report-versions.js +48 -35
  108. package/dist/state/snapshot-parse.js +16 -1
  109. package/dist/tools/define-tool.d.ts +32 -0
  110. package/dist/tools/define-tool.js +4 -0
  111. package/dist/tools/detail-resolver.d.ts +7 -2
  112. package/dist/tools/detail-resolver.js +7 -2
  113. package/dist/tools/iterate-report.d.ts +1 -1
  114. package/dist/tools/report/preview-snapshot.js +1 -0
  115. package/dist/tools/report-authoring/authoring-tools.d.ts +15 -6
  116. package/dist/tools/report-authoring/authoring-tools.js +99 -18
  117. package/dist/tools/report-session/derive-table.d.ts +162 -0
  118. package/dist/tools/report-session/derive-table.js +459 -0
  119. package/dist/tools/report-session/examine-page.d.ts +48 -18
  120. package/dist/tools/report-session/examine-page.js +204 -24
  121. package/dist/tools/report-session/index.d.ts +2 -0
  122. package/dist/tools/report-session/index.js +2 -0
  123. package/dist/tools/report-session/list-artifacts.d.ts +97 -0
  124. package/dist/tools/report-session/list-artifacts.js +197 -0
  125. package/dist/tools/report-session/preview-report.d.ts +6 -0
  126. package/dist/tools/report-session/preview-report.js +180 -28
  127. package/dist/tools/report-session/record-version.d.ts +22 -10
  128. package/dist/tools/report-session/record-version.js +99 -14
  129. package/dist/tools/start-report-session.d.ts +115 -0
  130. package/dist/tools/start-report-session.js +179 -0
  131. package/dist/workspace/paths.d.ts +50 -0
  132. package/dist/workspace/paths.js +63 -0
  133. package/package.json +2 -1
@@ -9,30 +9,45 @@
9
9
  * page lives at `report-sessions/{threadId}/index.html` under the workspace root, which the preview writes.
10
10
  * A missed page means that no preview ran, and it is a typed outcome, not a throw.
11
11
  *
12
- * The `file://` URL resolves on the filesystem of the Chrome sidecar, because the connection is out of
13
- * process. Thus the sidecar must mount the workspace tree of the harness host at the same path. A sidecar
12
+ * The composition says where a browser comes from, and the eyes seam is that answer. One look acquires one
13
+ * lease, and the tool captures against the endpoint of that lease.
14
+ *
15
+ * The `file://` URL resolves on the filesystem of the browser, because the connection is out of process.
16
+ * Thus the browser of a lease must hold the workspace tree of the harness host at the same path. A browser
14
17
  * with no such mount reports the page as an unreachable request, and the tool gives back that fault.
15
18
  *
16
- * A composition that names no browser and injects no capture seam has no eyes at all. The tool reports that
17
- * condition once, up front, and it stamps nothing. A per-attempt capture failure would instead read as a
18
- * transient fault and invite a repeat of a call that can never pass.
19
+ * The tool releases the lease after the look. The release runs on a pass and on a failed capture alike. That
20
+ * release is hygiene, and it is not the guarantee against a leak. A process can die between the acquire and
21
+ * the release, thus the realization bounds the life of what it provisions. As a result a failed release
22
+ * changes no outcome, and the log is its whole record.
23
+ *
24
+ * A composition with no capture seam, no eyes seam, and no configured endpoint has no eyes at all. The tool
25
+ * reports that condition one time, up front, and it stamps nothing. A per-attempt capture failure would
26
+ * instead read as a transient fault, and it would invite a repeat of a call that can never pass.
19
27
  *
20
28
  * On a capture the tool copies the rendered hash onto the seen hash through the gateway. Thus the look
21
29
  * counts, and the record tool lets the current draft record. The copy takes the rendered hash and never the
22
- * current one, thus a later edit makes the look stale and the record refuses.
30
+ * current one, thus a later edit makes the look stale and the record refuses. A picture of the window alone
31
+ * is a true picture of the current document, thus it stamps the same and the coverage of the result is what
32
+ * tells the agent what it saw.
23
33
  *
24
34
  * The gateway reports whether a rendered hash existed to copy. When the row holds none, no preview stamped
25
35
  * one and the look cannot count. The tool then gives a missed-stamp outcome that directs a new preview,
26
36
  * because a repeated look never stamps a marker that no preview wrote.
27
37
  *
28
- * The chrome navigation and the workspace-root seam each speak the throw protocol. The tool runs the capture
29
- * inside a guard, thus a genuine fault becomes a typed outcome and a control-flow exception propagates.
38
+ * The eyes seam, the chrome navigation, and the workspace-root seam each speak the throw protocol. The tool
39
+ * guards each of them, thus a fault of a look becomes a typed outcome and the loop never sees a throw.
40
+ *
41
+ * A hang is not a throw, and a guard alone never ends one. Thus the acquire and the release each carry a
42
+ * deadline. A realization that hangs while it starts a browser would otherwise hold the whole agent turn,
43
+ * and no outcome would ever arrive.
30
44
  */
31
45
  import { z } from "zod";
32
46
  import { type ChromeConfig } from "../../lib/chrome.js";
47
+ import { type AcquireEyes } from "../../lib/eyes.js";
33
48
  import { type Logger } from "../../lib/logger.js";
34
49
  import { type CapturePage, type FailedRequest, type PageCapture } from "../../lib/page-capture.js";
35
- import type { ResolveWorkspaceRoot } from "../../workspace/paths.js";
50
+ import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
36
51
  import { type Tool } from "../define-tool.js";
37
52
  import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
38
53
  export type { CapturePage, FailedRequest, PageCapture };
@@ -41,11 +56,13 @@ declare const examinePageInput: z.ZodObject<{}, z.core.$strip>;
41
56
  export type ExaminePageInput = z.infer<typeof examinePageInput>;
42
57
  /**
43
58
  * The typed outcome of the eyes tool. Each arm is ok-channel data, thus the tool never throws for a
44
- * degraded condition. `examined` carries the console errors, the failed requests, and the page path. The
45
- * screenshot does not ride the JSON. It rides the image path of the tool result, thus the model sees the
46
- * picture and the JSON text holds no bytes. `missed-stamp` means that the row holds no rendered hash, thus
47
- * no preview stamped one and the agent must run a new preview before the next look. `no-browser` means that
48
- * the composition gives no browser, thus no look is possible at all and a repeat gives the same answer.
59
+ * degraded condition. `examined` carries the coverage of the picture, the console errors, the failed
60
+ * requests, and the page path. The coverage names what the agent saw: the whole document, or the window
61
+ * alone when the browser refused a full-page bitmap. The screenshot does not ride the JSON. It rides the
62
+ * image path of the tool result, thus the model sees the picture and the JSON text holds no bytes.
63
+ * `missed-stamp` means that the row holds no rendered hash, thus no preview stamped one and the agent must
64
+ * run a new preview before the next look. `no-browser` means that the composition gives no browser, thus no
65
+ * look is possible at all and a repeat gives the same answer.
49
66
  */
50
67
  export type ExaminePageResult = {
51
68
  outcome: "refused";
@@ -62,6 +79,7 @@ export type ExaminePageResult = {
62
79
  detail: string;
63
80
  } | {
64
81
  outcome: "examined";
82
+ coverage: PageCapture["coverage"];
65
83
  consoleErrors: string[];
66
84
  failedRequests: FailedRequest[];
67
85
  pagePath: string;
@@ -70,19 +88,31 @@ export type ExaminePageResult = {
70
88
  * The construction deps of the eyes tool.
71
89
  *
72
90
  * `resolveWorkspaceRoot` maps the analysis of the call onto its workspace root, thus one singleton tool
73
- * serves every analysis. `chrome` configures the headless browser, and the sidecar that it names must mount
74
- * the workspace tree at the same path, because the tool navigates to a `file://` URL. `capture` is optional
75
- * and defaults to the shared capture, thus a test injects a seam that reads no browser.
91
+ * serves every analysis. `eyes` gives a browser for one look, and the scope of the acquire carries that
92
+ * analysis and its root. `chrome` carries the connection settings of the capture, and the browser that it
93
+ * names must hold the workspace tree at the same path, because the tool navigates to a `file://` URL.
94
+ * `capture` is optional and it replaces the whole transport, thus a test injects a seam that reads no
95
+ * browser.
76
96
  */
77
97
  export interface ExaminePageToolDeps {
78
98
  readonly gateway: ReportSessionStateGateway;
79
99
  readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
80
100
  readonly chrome: ChromeConfig;
101
+ readonly eyes?: AcquireEyes;
81
102
  readonly capture?: CapturePage;
82
103
  readonly logger?: Logger;
104
+ /**
105
+ * The two budgets of one look, in milliseconds. A test seam: it shortens the budgets, thus a hung
106
+ * realization settles inside one test run. Absent, the tool reads {@link ACQUIRE_DEADLINE_MS} and
107
+ * {@link RELEASE_DEADLINE_MS}, which no composition tunes.
108
+ */
109
+ readonly deadlines?: {
110
+ readonly acquireMs?: number;
111
+ readonly releaseMs?: number;
112
+ };
83
113
  }
84
114
  /**
85
- * Make the eyes tool over the session-state gateway, the workspace-root seam, and the chrome config.
115
+ * Make the eyes tool over the session-state gateway, the workspace-root seam, and the eyes of the composition.
86
116
  *
87
117
  * The tool reads the thread id from the scope of the call, and it resolves the page path under the workspace
88
118
  * root. Thus one factory serves every thread. The tool holds no per-session value.
@@ -9,68 +9,238 @@
9
9
  * page lives at `report-sessions/{threadId}/index.html` under the workspace root, which the preview writes.
10
10
  * A missed page means that no preview ran, and it is a typed outcome, not a throw.
11
11
  *
12
- * The `file://` URL resolves on the filesystem of the Chrome sidecar, because the connection is out of
13
- * process. Thus the sidecar must mount the workspace tree of the harness host at the same path. A sidecar
12
+ * The composition says where a browser comes from, and the eyes seam is that answer. One look acquires one
13
+ * lease, and the tool captures against the endpoint of that lease.
14
+ *
15
+ * The `file://` URL resolves on the filesystem of the browser, because the connection is out of process.
16
+ * Thus the browser of a lease must hold the workspace tree of the harness host at the same path. A browser
14
17
  * with no such mount reports the page as an unreachable request, and the tool gives back that fault.
15
18
  *
16
- * A composition that names no browser and injects no capture seam has no eyes at all. The tool reports that
17
- * condition once, up front, and it stamps nothing. A per-attempt capture failure would instead read as a
18
- * transient fault and invite a repeat of a call that can never pass.
19
+ * The tool releases the lease after the look. The release runs on a pass and on a failed capture alike. That
20
+ * release is hygiene, and it is not the guarantee against a leak. A process can die between the acquire and
21
+ * the release, thus the realization bounds the life of what it provisions. As a result a failed release
22
+ * changes no outcome, and the log is its whole record.
23
+ *
24
+ * A composition with no capture seam, no eyes seam, and no configured endpoint has no eyes at all. The tool
25
+ * reports that condition one time, up front, and it stamps nothing. A per-attempt capture failure would
26
+ * instead read as a transient fault, and it would invite a repeat of a call that can never pass.
19
27
  *
20
28
  * On a capture the tool copies the rendered hash onto the seen hash through the gateway. Thus the look
21
29
  * counts, and the record tool lets the current draft record. The copy takes the rendered hash and never the
22
- * current one, thus a later edit makes the look stale and the record refuses.
30
+ * current one, thus a later edit makes the look stale and the record refuses. A picture of the window alone
31
+ * is a true picture of the current document, thus it stamps the same and the coverage of the result is what
32
+ * tells the agent what it saw.
23
33
  *
24
34
  * The gateway reports whether a rendered hash existed to copy. When the row holds none, no preview stamped
25
35
  * one and the look cannot count. The tool then gives a missed-stamp outcome that directs a new preview,
26
36
  * because a repeated look never stamps a marker that no preview wrote.
27
37
  *
28
- * The chrome navigation and the workspace-root seam each speak the throw protocol. The tool runs the capture
29
- * inside a guard, thus a genuine fault becomes a typed outcome and a control-flow exception propagates.
38
+ * The eyes seam, the chrome navigation, and the workspace-root seam each speak the throw protocol. The tool
39
+ * guards each of them, thus a fault of a look becomes a typed outcome and the loop never sees a throw.
40
+ *
41
+ * A hang is not a throw, and a guard alone never ends one. Thus the acquire and the release each carry a
42
+ * deadline. A realization that hangs while it starts a browser would otherwise hold the whole agent turn,
43
+ * and no outcome would ever arrive.
30
44
  */
31
- import { ok } from "neverthrow";
45
+ import { err, ok } from "neverthrow";
32
46
  import { access } from "node:fs/promises";
33
47
  import { join } from "node:path";
34
48
  import { pathToFileURL } from "node:url";
35
49
  import { z } from "zod";
36
50
  import { hasBrowserUrl } from "../../lib/chrome.js";
37
51
  import { createNoopLogger } from "../../lib/console-logger.js";
52
+ import { createStaticEyes } from "../../lib/eyes.js";
38
53
  import { defaultErrorFields } from "../../lib/logger.js";
39
54
  import { capturePage } from "../../lib/page-capture.js";
55
+ import { reportSessionDir } from "../../workspace/paths.js";
40
56
  import { defineTool, withToolResultImage } from "../define-tool.js";
41
57
  import { openReportThread } from "../report-authoring/authoring-tools.js";
42
58
  /** The empty input. The tool examines the current page of the thread, thus it needs no field. */
43
59
  const examinePageInput = z.object({});
60
+ /**
61
+ * Resolve the transport of the tool from its deps, one time.
62
+ *
63
+ * The precedence is `capture`, then `eyes`, then the chrome config as static eyes. An injected capture wins
64
+ * over the two others, because it replaces the whole transport and a test injects it.
65
+ *
66
+ * The chrome arm serves a direct construction alone, for example the tests of this file. The assembled
67
+ * runtime wraps the configured endpoint into the static realization at its composition root. Thus one wrap
68
+ * serves that runtime, and this arm never fires there.
69
+ */
70
+ function resolveEyes(deps) {
71
+ if (deps.capture !== undefined) {
72
+ return { kind: "capture", capture: deps.capture };
73
+ }
74
+ if (deps.eyes !== undefined) {
75
+ return { kind: "lease", acquire: deps.eyes };
76
+ }
77
+ if (hasBrowserUrl(deps.chrome)) {
78
+ return { kind: "lease", acquire: createStaticEyes(deps.chrome) };
79
+ }
80
+ return { kind: "none" };
81
+ }
82
+ /** The log line of each stage. The outcome of the tool is one arm, thus the stage rides in the log alone. */
83
+ const LOOK_FAULT_MESSAGE = {
84
+ acquire: "the eyes gave no browser for the look",
85
+ capture: "the page capture failed",
86
+ };
87
+ /**
88
+ * The deadline of one acquire, in milliseconds.
89
+ *
90
+ * A realization can start a container and boot a browser inside it, and that cold start costs seconds. The
91
+ * budget covers a slow cold start with a wide margin. It also ends a realization that hangs: one look sits
92
+ * inside one agent turn, and a hung acquire holds that whole turn with no outcome at all.
93
+ */
94
+ const ACQUIRE_DEADLINE_MS = 60_000;
95
+ /**
96
+ * The deadline of one release, in milliseconds.
97
+ *
98
+ * A release stops what one acquire started, and the look already holds its picture. Thus the budget is short.
99
+ * A release that runs past the budget costs nothing, because the realization bounds the life of what it
100
+ * provisions.
101
+ */
102
+ const RELEASE_DEADLINE_MS = 10_000;
103
+ /** The win of the timer arm. A symbol cannot collide with a value that the operation gives. */
104
+ const EXPIRED = Symbol("expired");
105
+ /**
106
+ * Run one operation against a deadline, and give the value, the cause, or the expiry.
107
+ *
108
+ * The eyes seam takes no abort signal, thus a race is the whole bound. The loser of the race stays in
109
+ * flight, and the caller of an expired operation owns whatever arrives late.
110
+ *
111
+ * Each path clears the timer. `sleep` of `async-utils.ts` holds a timer that nothing can clear. Thus an
112
+ * operation that wins its race would still keep the process alive for the rest of the budget.
113
+ */
114
+ async function withDeadline(work, ms) {
115
+ let timer;
116
+ // The settled arm never rejects. Thus a fault that arrives after the expiry stays contained here, and it
117
+ // never reaches the process as an unhandled rejection.
118
+ const settled = work.then((value) => ok(value), (cause) => err({ kind: "threw", cause }));
119
+ const expiry = new Promise((resolve) => {
120
+ timer = setTimeout(() => resolve(EXPIRED), ms);
121
+ });
122
+ const winner = await Promise.race([settled, expiry]);
123
+ clearTimeout(timer);
124
+ return winner === EXPIRED ? err({ kind: "expired" }) : winner;
125
+ }
126
+ /** The cause of one such fault. An expiry names its budget, thus the detail of the outcome names it too. */
127
+ function faultCause(fault, expiry, ms) {
128
+ return fault.kind === "threw" ? fault.cause : new Error(`${expiry} within ${ms} ms`);
129
+ }
130
+ /**
131
+ * Start one seam call, and give the promise of its outcome.
132
+ *
133
+ * A realization comes from the embedder, and one that is hand-written can throw before it gives its promise.
134
+ * The catch turns that throw into a rejection. Thus the deadline below reads one shape, and a look keeps the
135
+ * no-throw contract of the tool.
136
+ */
137
+ function startSeamCall(call) {
138
+ try {
139
+ return call();
140
+ }
141
+ catch (cause) {
142
+ return Promise.reject(cause);
143
+ }
144
+ }
145
+ /**
146
+ * Release a lease that arrived after the deadline of its acquire.
147
+ *
148
+ * The acquire stays in flight past that deadline, thus a browser can still arrive with no owner. The
149
+ * realization bounds the life of that browser, and this release ends it sooner. The look already gave its
150
+ * outcome, thus a fault here reaches the log alone.
151
+ */
152
+ function releaseLateLease(pending, logger) {
153
+ void pending
154
+ .then((lease) => lease.release())
155
+ .catch((cause) => {
156
+ logger.warn("the eyes lease did not release", logger.errorFields(cause));
157
+ });
158
+ }
159
+ /**
160
+ * Run one look over the resolved transport, and give the picture or the typed fault.
161
+ *
162
+ * The lease arm acquires one browser, captures against the endpoint of that lease, and releases in a
163
+ * `finally`. Thus a failed capture releases too. The shared capture takes a chrome config, thus the endpoint
164
+ * of the lease replaces the configured one and each other field of the composition stays.
165
+ *
166
+ * A failed release changes no outcome of the look. The capture already ran, and the realization bounds the
167
+ * life of what it provisions. Thus the log is the whole record of a failed release.
168
+ *
169
+ * The two seam calls each run against a deadline. An expired acquire reads as an acquire fault, thus a hung
170
+ * realization gives the same outcome as a refused one. An expired release reads as a failed release, thus it
171
+ * reaches the log and it changes no outcome.
172
+ */
173
+ async function runLook(args) {
174
+ const { transport } = args;
175
+ if (transport.kind === "capture") {
176
+ try {
177
+ return ok(await transport.capture(args.url));
178
+ }
179
+ catch (cause) {
180
+ return err({ stage: "capture", cause });
181
+ }
182
+ }
183
+ // The pending acquire stays apart from its deadline. An expiry leaves the acquire in flight, and a lease
184
+ // that arrives late still gets a release.
185
+ const pending = startSeamCall(() => transport.acquire(args.scope));
186
+ const acquired = await withDeadline(pending, args.acquireMs);
187
+ if (acquired.isErr()) {
188
+ if (acquired.error.kind === "expired")
189
+ releaseLateLease(pending, args.logger);
190
+ return err({ stage: "acquire", cause: faultCause(acquired.error, "the eyes gave no browser", args.acquireMs) });
191
+ }
192
+ const lease = acquired.value;
193
+ try {
194
+ return ok(await capturePage({ ...args.chrome, browserUrl: lease.browserUrl }, args.url));
195
+ }
196
+ catch (cause) {
197
+ return err({ stage: "capture", cause });
198
+ }
199
+ finally {
200
+ const released = await withDeadline(startSeamCall(() => lease.release()), args.releaseMs);
201
+ if (released.isErr()) {
202
+ const cause = faultCause(released.error, "the eyes lease did not release", args.releaseMs);
203
+ args.logger.warn("the eyes lease did not release", args.logger.errorFields(cause));
204
+ }
205
+ }
206
+ }
44
207
  /** The IANA media type of the screenshot. `page.screenshot` gives PNG bytes. */
45
208
  const SCREENSHOT_MEDIA_TYPE = "image/png";
46
209
  /** The line that the agent reads when the composition gives no browser. */
47
210
  const NO_BROWSER_DETAIL = "the composition gives no browser, thus this session cannot look at its page";
48
211
  /**
49
- * Make the eyes tool over the session-state gateway, the workspace-root seam, and the chrome config.
212
+ * Make the eyes tool over the session-state gateway, the workspace-root seam, and the eyes of the composition.
50
213
  *
51
214
  * The tool reads the thread id from the scope of the call, and it resolves the page path under the workspace
52
215
  * root. Thus one factory serves every thread. The tool holds no per-session value.
53
216
  */
54
217
  export function createExaminePageTool(deps) {
55
218
  const logger = (deps.logger ?? createNoopLogger()).named("examine-page");
56
- // An injected seam is the eyes of a test. A named browser is the eyes of a deployment. With neither, the
57
- // composition has no eyes, and that answer is fixed at construction.
58
- const eyesAvailable = deps.capture !== undefined || hasBrowserUrl(deps.chrome);
59
- const capture = deps.capture ?? ((url) => capturePage(deps.chrome, url));
219
+ // The eyes of the composition are fixed here, thus one look reads one resolved transport and never the
220
+ // precedence again.
221
+ const transport = resolveEyes(deps);
222
+ const acquireMs = deps.deadlines?.acquireMs ?? ACQUIRE_DEADLINE_MS;
223
+ const releaseMs = deps.deadlines?.releaseMs ?? RELEASE_DEADLINE_MS;
60
224
  return defineTool({
61
225
  id: "examine_page",
62
226
  description: "Open the rendered report page in a real headless browser, and report what you see. " +
63
- "Give back a screenshot, the console errors, and the failed requests. " +
227
+ "Give back a screenshot, the coverage of that screenshot, the console errors, and the failed requests. " +
228
+ "The coverage says whether the picture holds the whole page, or the top window alone. " +
64
229
  "Run it after the preview to look at the current page, and to confirm that the layout and the charts read clean. " +
65
230
  "The report tool records a version only after you look at the current page. " +
66
231
  "If the page has no confirmed render, run the preview again first.",
67
232
  inputSchema: examinePageInput,
68
233
  executionMode: "inline",
69
234
  describeCall: "none",
235
+ // A look gives a picture, and a picture has no one-line form. Thus the outcome of the look is the
236
+ // whole line: a watcher reads that the eyes saw the page, or that the page was absent, or that the
237
+ // capture failed.
238
+ describeResult: (_input, result) => result.outcome,
70
239
  execute: async (_input, ctx) => {
71
240
  // The check runs before every read, thus one clear signal replaces a capture failure for each
72
- // page. The arm stamps no seen hash, because no eyes saw the page.
73
- if (!eyesAvailable) {
241
+ // page. The arm stamps no seen hash, because no eyes saw the page. It also narrows the transport
242
+ // for the look below, thus the acquire needs no assertion.
243
+ if (transport.kind === "none") {
74
244
  logger.warn("the composition gives no browser, thus no look can run");
75
245
  return ok({ outcome: "no-browser", detail: NO_BROWSER_DETAIL });
76
246
  }
@@ -87,7 +257,7 @@ export function createExaminePageTool(deps) {
87
257
  logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
88
258
  return ok({ outcome: "capture-failed", detail: "the workspace root did not resolve" });
89
259
  }
90
- const relativePagePath = join("report-sessions", threadId, "index.html");
260
+ const relativePagePath = join(reportSessionDir(threadId), "index.html");
91
261
  const pagePath = join(root, relativePagePath);
92
262
  try {
93
263
  await access(pagePath);
@@ -100,14 +270,23 @@ export function createExaminePageTool(deps) {
100
270
  logger.warn("the session page could not be read", { threadId, analysisId, ...defaultErrorFields(cause) });
101
271
  return ok({ outcome: "capture-failed", detail: "the session page could not be read" });
102
272
  }
103
- let captured;
104
- try {
105
- captured = await capture(pathToFileURL(pagePath).href);
106
- }
107
- catch (cause) {
108
- logger.warn("the page capture failed", { threadId, analysisId, ...defaultErrorFields(cause) });
273
+ // The scope of the acquire carries the analysis and the root that this call resolved. Thus a
274
+ // realization that starts a browser mounts the same tree, and it holds no second resolver.
275
+ const looked = await runLook({
276
+ transport,
277
+ chrome: deps.chrome,
278
+ scope: { analysisId, workspaceRoot: root },
279
+ url: pathToFileURL(pagePath).href,
280
+ acquireMs,
281
+ releaseMs,
282
+ logger,
283
+ });
284
+ if (looked.isErr()) {
285
+ const { stage, cause } = looked.error;
286
+ logger.warn(LOOK_FAULT_MESSAGE[stage], { threadId, analysisId, ...defaultErrorFields(cause) });
109
287
  return ok({ outcome: "capture-failed", detail: cause instanceof Error ? cause.message : String(cause) });
110
288
  }
289
+ const captured = looked.value;
111
290
  const stamped = await deps.gateway.stampSeen(threadId);
112
291
  if (stamped.outcome === "no-rendered") {
113
292
  // The row holds no rendered hash, thus no preview stamped one and the look cannot count. A
@@ -127,6 +306,7 @@ export function createExaminePageTool(deps) {
127
306
  // JSON keeps the faults and the page path only, thus the JSON text holds no bytes.
128
307
  return ok(withToolResultImage({
129
308
  outcome: "examined",
309
+ coverage: captured.coverage,
130
310
  consoleErrors: captured.consoleErrors,
131
311
  failedRequests: captured.failedRequests,
132
312
  pagePath: relativePagePath,
@@ -8,3 +8,5 @@
8
8
  export { createPreviewReportTool, type PreviewReportInput, type PreviewReportResult, type PreviewReportToolDeps, type ResolvePageAsset, } from "./preview-report.js";
9
9
  export { createExaminePageTool, type CapturePage, type ExaminePageInput, type ExaminePageResult, type ExaminePageToolDeps, type FailedRequest, type PageCapture, } from "./examine-page.js";
10
10
  export { createRecordVersionTool, type RecordVersionInput, type RecordVersionResult, type RecordVersionToolDeps } from "./record-version.js";
11
+ export { createDeriveTableTool, type DeriveTableInput, type DeriveTableResult, type DeriveTableToolDeps } from "./derive-table.js";
12
+ export { createListPinnedArtifactsTool, type ListPinnedArtifactsInput, type ListPinnedArtifactsResult, type ListPinnedArtifactsToolDeps, type PinnedArtifact, type PinnedCitation, } from "./list-artifacts.js";
@@ -8,3 +8,5 @@
8
8
  export { createPreviewReportTool, } from "./preview-report.js";
9
9
  export { createExaminePageTool, } from "./examine-page.js";
10
10
  export { createRecordVersionTool } from "./record-version.js";
11
+ export { createDeriveTableTool } from "./derive-table.js";
12
+ export { createListPinnedArtifactsTool, } from "./list-artifacts.js";
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The pinned-artifact listing tool of a report session.
3
+ *
4
+ * The tool reads the frozen snapshot of the thread, and it gives the pinned set: the path, the content
5
+ * hash, the file type, and the header columns of a tabular artifact, plus the pinned citations with the
6
+ * short citation of each. Thus one call orients the agent, and a reference binds to a path or to a
7
+ * citation key of that set.
8
+ *
9
+ * The order is the code-unit order of the path. Two calls over one snapshot give one listing, thus the
10
+ * agent reads a stable set. A snapshot can pin many thousands of staged inputs, thus the listing stops at
11
+ * a cap and it carries the total count and a truncation marker.
12
+ *
13
+ * The columns come from a bounded header read of a listed entry. The path of a snapshot entry is
14
+ * untrusted, thus the read contains it with the one workspace-path resolver first. The extension gates
15
+ * the read, because a file type states a role and not a data format.
16
+ *
17
+ * An absent file, an unreadable file, and an unresolvable workspace root each give no columns and no
18
+ * error. Absence is a normal condition here: the listing is orientation, and the hash is the evidence.
19
+ */
20
+ import { z } from "zod";
21
+ import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
22
+ import { type Logger } from "../../lib/logger.js";
23
+ import { type Tool } from "../define-tool.js";
24
+ import { type ReportSessionStateGateway, type SessionRefusal } from "../report-authoring/authoring-tools.js";
25
+ /** The empty input. The tool lists the pinned set of the thread, thus it needs no field. */
26
+ declare const listPinnedArtifactsInput: z.ZodObject<{}, z.core.$strip>;
27
+ export type ListPinnedArtifactsInput = z.infer<typeof listPinnedArtifactsInput>;
28
+ /**
29
+ * One pinned artifact of the listing.
30
+ *
31
+ * `fileType` states a role, and it does not state a data format. `columns` is present only for a header
32
+ * that the read recovered, thus an absent `columns` says nothing about the content of the file.
33
+ */
34
+ export interface PinnedArtifact {
35
+ path: string;
36
+ hash: string;
37
+ fileType?: string;
38
+ columns?: string[];
39
+ }
40
+ /**
41
+ * One pinned citation of the listing: the key, and the short citation of its pinned record.
42
+ *
43
+ * `citation` names which paper the key is, thus the agent binds a citation block with no guess. A key that
44
+ * the pin recorded no citation for carries none, because absence is a normal condition. The description of
45
+ * the record stays out, because the listing is an orientation surface.
46
+ */
47
+ export interface PinnedCitation {
48
+ key: string;
49
+ citation?: string;
50
+ }
51
+ /**
52
+ * The typed outcome of the listing tool. Each arm is ok-channel data, thus the tool never throws for one
53
+ * of them.
54
+ *
55
+ * `total` counts the whole pinned set, and `truncated` says that the set holds more entries than
56
+ * `artifacts` names. Thus the agent reads a partial listing as a partial listing.
57
+ *
58
+ * `citations` gives the pinned citations in the code-unit order of the keys that the pin stored. A citation
59
+ * reference binds to one of those keys, thus the agent reads a key here and never out of a refusal.
60
+ */
61
+ export type ListPinnedArtifactsResult = {
62
+ outcome: "refused";
63
+ refusal: SessionRefusal;
64
+ } | {
65
+ outcome: "listed";
66
+ artifacts: PinnedArtifact[];
67
+ total: number;
68
+ truncated: boolean;
69
+ citations: PinnedCitation[];
70
+ };
71
+ /**
72
+ * The construction deps of the listing tool.
73
+ *
74
+ * `resolveWorkspaceRoot` maps the analysis of the call onto its workspace root, thus one singleton tool
75
+ * serves every analysis and it resolves the root per call from the scope.
76
+ */
77
+ export interface ListPinnedArtifactsToolDeps {
78
+ readonly gateway: ReportSessionStateGateway;
79
+ readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
80
+ readonly logger?: Logger;
81
+ }
82
+ /**
83
+ * The header columns of a file on disk, or `undefined` when the file gives none.
84
+ *
85
+ * `path` decides whether the file carries a header at all, and `absolute` is where the bytes sit. The
86
+ * derivation tool reads the columns of its own output the same way, thus the two surfaces describe a table
87
+ * with one rule.
88
+ */
89
+ export declare function readHeaderColumns(path: string, absolute: string): Promise<string[] | undefined>;
90
+ /**
91
+ * Make the pinned-artifact listing tool over the session-state gateway.
92
+ *
93
+ * The tool reads the thread id from the scope of the call, and it loads the snapshot through the gateway.
94
+ * Thus one factory serves every thread, and the tool holds no per-session value.
95
+ */
96
+ export declare function createListPinnedArtifactsTool(deps: ListPinnedArtifactsToolDeps): Tool<ListPinnedArtifactsInput, ListPinnedArtifactsResult>;
97
+ export {};