@inflexa-ai/harness 0.19.0 → 0.19.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 (64) hide show
  1. package/dist/agents/report-session-agent.d.ts +25 -9
  2. package/dist/agents/report-session-agent.js +21 -6
  3. package/dist/app/report-session-runtime.js +26 -0
  4. package/dist/app/spawn-report-session.d.ts +32 -5
  5. package/dist/app/spawn-report-session.js +19 -0
  6. package/dist/config/environment-stores.d.ts +12 -7
  7. package/dist/config/resource-limits.d.ts +6 -10
  8. package/dist/config/resource-limits.js +6 -44
  9. package/dist/contracts/report-reference.d.ts +4 -0
  10. package/dist/contracts/report-reference.js +3 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/lib/chrome.d.ts +14 -0
  13. package/dist/lib/chrome.js +7 -0
  14. package/dist/lib/page-capture.d.ts +47 -0
  15. package/dist/lib/page-capture.js +83 -0
  16. package/dist/loop/run-agent.js +104 -15
  17. package/dist/memory/ai-sdk-message-storage.d.ts +9 -4
  18. package/dist/memory/ai-sdk-message-storage.js +9 -4
  19. package/dist/memory/count-tokens.js +34 -5
  20. package/dist/prompts/report-session.d.ts +1 -1
  21. package/dist/prompts/report-session.js +21 -0
  22. package/dist/providers/ai-sdk.d.ts +81 -3
  23. package/dist/providers/ai-sdk.js +91 -10
  24. package/dist/providers/types.d.ts +17 -0
  25. package/dist/report-model/assert-rules.d.ts +44 -0
  26. package/dist/report-model/assert-rules.js +97 -0
  27. package/dist/report-model/draft-hash.d.ts +14 -0
  28. package/dist/report-model/draft-hash.js +36 -0
  29. package/dist/report-model/fixture-resolver.js +2 -71
  30. package/dist/report-model/production-resolver.d.ts +124 -0
  31. package/dist/report-model/production-resolver.js +801 -0
  32. package/dist/report-model/reference-resolver.d.ts +6 -0
  33. package/dist/report-model/validate.js +5 -0
  34. package/dist/report-render/page.d.ts +23 -0
  35. package/dist/report-render/page.js +36 -3
  36. package/dist/runtime/assemble.d.ts +28 -4
  37. package/dist/runtime/assemble.js +42 -3
  38. package/dist/sandbox/mount-plan.d.ts +7 -0
  39. package/dist/sandbox/mount-plan.js +7 -2
  40. package/dist/state/init.js +20 -7
  41. package/dist/state/report-session-state.d.ts +27 -0
  42. package/dist/state/report-session-state.js +29 -2
  43. package/dist/tasks/extract-values-script.d.ts +64 -0
  44. package/dist/tasks/extract-values-script.js +180 -0
  45. package/dist/tasks/extract-values.d.ts +124 -0
  46. package/dist/tasks/extract-values.js +193 -0
  47. package/dist/tools/define-tool.d.ts +29 -0
  48. package/dist/tools/define-tool.js +26 -0
  49. package/dist/tools/report/preview-snapshot.d.ts +1 -1
  50. package/dist/tools/report/preview-snapshot.js +11 -50
  51. package/dist/tools/report-authoring/authoring-tools.d.ts +54 -4
  52. package/dist/tools/report-authoring/authoring-tools.js +1 -1
  53. package/dist/tools/report-session/examine-page.d.ts +90 -0
  54. package/dist/tools/report-session/examine-page.js +136 -0
  55. package/dist/tools/report-session/index.d.ts +2 -0
  56. package/dist/tools/report-session/index.js +2 -0
  57. package/dist/tools/report-session/preview-report.d.ts +20 -5
  58. package/dist/tools/report-session/preview-report.js +28 -4
  59. package/dist/tools/report-session/record-version.d.ts +102 -0
  60. package/dist/tools/report-session/record-version.js +139 -0
  61. package/dist/tools/sandbox/list-available-packages.js +2 -1
  62. package/dist/tools/sandbox/list-available-refs.d.ts +5 -1
  63. package/dist/tools/sandbox/list-available-refs.js +13 -6
  64. package/package.json +10 -8
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * `createReportSessionAgent` is the composition root of the report path. It gets
5
5
  * the shared deps one time, and it hands each tool exactly what it needs. The
6
- * authoring tools and the preview tool bind to the session-state gateway, thus one
7
- * assembled definition serves every thread, and the per-session state stays behind
8
- * the tool boundary.
6
+ * authoring tools, the preview tool, the eyes tool, and the record tool bind to the
7
+ * session-state gateway, thus one assembled definition serves every thread, and the
8
+ * per-session state stays behind the tool boundary.
9
9
  *
10
10
  * The roster is read-only toward the analysis. It holds the workspace read tools,
11
11
  * the workspace search, the run inspection, and the data-profile inspection. It
@@ -19,20 +19,24 @@
19
19
  * across threads, and the provider cache reuses it.
20
20
  */
21
21
  import type { Pool } from "pg";
22
+ import type { AuthContext } from "../auth/types.js";
22
23
  import type { AgentDefinition } from "../loop/types.js";
23
24
  import type { EmbeddingProvider } from "../providers/types.js";
24
25
  import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
25
26
  import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
27
+ import type { ChromeConfig } from "../lib/chrome.js";
26
28
  import type { Logger } from "../lib/logger.js";
29
+ import type { ThreadStore } from "../memory/thread-store.js";
27
30
  import type { ReferenceResolver } from "../report-model/reference-resolver.js";
31
+ import type { ReportVersionStore } from "../state/report-versions.js";
28
32
  import type { ReportSessionStateGateway } from "../tools/report-authoring/authoring-tools.js";
29
33
  /** Canonical agent id -- distinct from the id of the old templating agent. */
30
34
  export declare const REPORT_SESSION_AGENT_ID: "report-session";
31
35
  /**
32
36
  * The shared dependencies of the report agent. The gateway binds the per-session
33
37
  * state to the thread, thus every tool that touches the state gets the same
34
- * gateway. The resolver is optional, because a resolver realization can be absent,
35
- * and the preview tool degrades as data when it is.
38
+ * gateway. The resolver factory is optional, because a resolver realization can be
39
+ * absent, and the preview tool and the record tool degrade as data when it is.
36
40
  */
37
41
  export interface ReportSessionAgentDeps {
38
42
  /** Model id -- the provenance and metric label. The provider owns the wire model. */
@@ -43,12 +47,24 @@ export interface ReportSessionAgentDeps {
43
47
  readonly embedding: EmbeddingProvider;
44
48
  /** Workspace filesystem read seam -- the four read tools. */
45
49
  readonly workspaceFs: WorkspaceFilesystem;
46
- /** Session-state gateway -- the authoring tools and the preview tool bind to it. */
50
+ /** Session-state gateway -- the authoring tools, the preview tool, the eyes tool, and the record tool bind to it. */
47
51
  readonly gateway: ReportSessionStateGateway;
48
- /** Workspace-root resolution seam -- the preview tool resolves the page root per call. */
52
+ /** Workspace-root resolution seam -- the preview tool and the eyes tool resolve the page root per call. */
49
53
  readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
50
- /** Reference-resolution seam -- absent until a realization lands; the preview tool degrades as data. */
51
- readonly resolver?: ReferenceResolver;
54
+ /** Append-only version store -- the record tool gates the whole document first, then records one version. */
55
+ readonly store: ReportVersionStore;
56
+ /** Thread reader -- the record tool reads the anchor of the report thread. */
57
+ readonly threads: Pick<ThreadStore, "getThread">;
58
+ /** Headless-Chrome config -- the eyes tool opens the rendered page. */
59
+ readonly chrome: ChromeConfig;
60
+ /**
61
+ * Reference-resolution factory -- absent until a realization lands; the preview tool and the record tool
62
+ * degrade as data. The factory binds one analysis, thus a tool makes the resolver over the scope of the call.
63
+ */
64
+ readonly makeResolver?: (scope: {
65
+ analysisId: string;
66
+ auth: AuthContext;
67
+ }) => ReferenceResolver;
52
68
  /** Operational logging seam; omitted falls back to no-op. */
53
69
  readonly logger?: Logger;
54
70
  }
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * `createReportSessionAgent` is the composition root of the report path. It gets
5
5
  * the shared deps one time, and it hands each tool exactly what it needs. The
6
- * authoring tools and the preview tool bind to the session-state gateway, thus one
7
- * assembled definition serves every thread, and the per-session state stays behind
8
- * the tool boundary.
6
+ * authoring tools, the preview tool, the eyes tool, and the record tool bind to the
7
+ * session-state gateway, thus one assembled definition serves every thread, and the
8
+ * per-session state stays behind the tool boundary.
9
9
  *
10
10
  * The roster is read-only toward the analysis. It holds the workspace read tools,
11
11
  * the workspace search, the run inspection, and the data-profile inspection. It
@@ -19,7 +19,7 @@
19
19
  * across threads, and the provider cache reuses it.
20
20
  */
21
21
  import { createReportAuthoringTools } from "../tools/report-authoring/authoring-tools.js";
22
- import { createPreviewReportTool } from "../tools/report-session/preview-report.js";
22
+ import { createExaminePageTool, createPreviewReportTool, createRecordVersionTool } from "../tools/report-session/index.js";
23
23
  import { createFileStatTool, createGrepTool, createListFilesTool, createReadFileTool, createWorkspaceSearchTool } from "../tools/workspace/index.js";
24
24
  import { createInspectDataProfileTool, createInspectRunTool } from "../tools/research/index.js";
25
25
  import { reportSessionPrompt } from "../prompts/report-session.js";
@@ -33,7 +33,7 @@ export const REPORT_SESSION_AGENT_ID = "report-session";
33
33
  const REPORT_SESSION_MAX_ITERATIONS = 50;
34
34
  /** Build the report `AgentDefinition` with every tool bound to its deps. */
35
35
  export function createReportSessionAgent(deps) {
36
- const { model, pool, embedding, workspaceFs, gateway, resolveWorkspaceRoot, resolver, logger } = deps;
36
+ const { model, pool, embedding, workspaceFs, gateway, resolveWorkspaceRoot, store, threads, chrome, makeResolver, logger } = deps;
37
37
  const authoring = createReportAuthoringTools(gateway);
38
38
  const tools = [
39
39
  // The read surface toward the analysis. It roams the tree read-only.
@@ -58,7 +58,22 @@ export function createReportSessionAgent(deps) {
58
58
  createPreviewReportTool({
59
59
  gateway,
60
60
  resolveWorkspaceRoot,
61
- ...(resolver ? { resolver } : {}),
61
+ ...(makeResolver ? { makeResolver } : {}),
62
+ ...(logger ? { logger } : {}),
63
+ }),
64
+ // The eyes tool. It opens the rendered page in headless Chrome.
65
+ createExaminePageTool({
66
+ gateway,
67
+ resolveWorkspaceRoot,
68
+ chrome,
69
+ ...(logger ? { logger } : {}),
70
+ }),
71
+ // The record tool. It gates the whole document, then records one version.
72
+ createRecordVersionTool({
73
+ gateway,
74
+ store,
75
+ threads,
76
+ ...(makeResolver ? { makeResolver } : {}),
62
77
  ...(logger ? { logger } : {}),
63
78
  }),
64
79
  ];
@@ -115,6 +115,7 @@ export function createReportSessionRuntime(deps) {
115
115
  state: { document: state.document ?? EMPTY_DRAFT, snapshot: state.snapshot },
116
116
  analysisId: state.analysisId,
117
117
  token: state.document,
118
+ seenDocumentHash: state.seenDocumentHash,
118
119
  };
119
120
  }
120
121
  const gateway = {
@@ -152,6 +153,31 @@ export function createReportSessionRuntime(deps) {
152
153
  return { outcome: "failed", detail: describeDbError(error) };
153
154
  });
154
155
  },
156
+ async stampRendered(threadId, hash) {
157
+ const result = await store.stampRendered(threadId, hash);
158
+ return result.match((outcome) => (outcome === "stamped" ? { outcome: "stamped" } : { outcome: "absent" }), (error) => {
159
+ log.error("the rendered-hash stamp failed", { threadId, ...log.errorFields(error.cause) });
160
+ return { outcome: "failed", detail: describeDbError(error) };
161
+ });
162
+ },
163
+ async stampSeen(threadId) {
164
+ const result = await store.stampSeen(threadId);
165
+ return result.match((outcome) => {
166
+ switch (outcome) {
167
+ case "stamped":
168
+ return { outcome: "stamped" };
169
+ case "no-rendered":
170
+ // The row holds no rendered hash, thus no preview stamped one and the copy found
171
+ // none. The eyes direct a new preview from this arm.
172
+ return { outcome: "no-rendered" };
173
+ case "absent":
174
+ return { outcome: "absent" };
175
+ }
176
+ }, (error) => {
177
+ log.error("the seen-hash stamp failed", { threadId, ...log.errorFields(error.cause) });
178
+ return { outcome: "failed", detail: describeDbError(error) };
179
+ });
180
+ },
155
181
  };
156
182
  return { gateway, ensureSessionState };
157
183
  }
@@ -18,10 +18,19 @@
18
18
  * A conversation thread id comes from the host UI. But the spawn is a harness
19
19
  * operation with no UI in front of it. Thus it mints a `randomUUID` and returns
20
20
  * the full row. A managed deployment gets the same behavior with no host code.
21
+ *
22
+ * The spawn also gates on the eyes of the composition. A report session records
23
+ * a version only after a look at the rendered page, and only a browser can look.
24
+ * Thus a composition with no browser and no injected capture seam can author a
25
+ * draft that no gate ever accepts. The spawn refuses that session before it
26
+ * writes a row, because a dead-end thread is worse than a refusal that names the
27
+ * absent capability.
21
28
  */
22
29
  import { type ResultAsync } from "neverthrow";
23
30
  import type { Pool } from "pg";
24
31
  import type { DbError } from "../lib/db-result.js";
32
+ import { type ChromeConfig } from "../lib/chrome.js";
33
+ import type { CapturePage } from "../lib/page-capture.js";
25
34
  import { type Thread, type ThreadInputError, type ThreadPage, type ThreadType } from "../memory/thread-store.js";
26
35
  /**
27
36
  * A spawn the operation refuses before it writes a row. Distinct from
@@ -34,9 +43,15 @@ import { type Thread, type ThreadInputError, type ThreadPage, type ThreadType }
34
43
  * state is not permitted. `parent_not_a_conversation` keeps the tree flat: a
35
44
  * report session cannot spawn another. `empty_parent_transcript` refuses a
36
45
  * report on a parent that holds no messages, because such a report reports
37
- * nothing.
46
+ * nothing. `no_browser` refuses every spawn under a composition that gives no
47
+ * eyes, and it carries the line that explains the absent capability.
38
48
  */
39
49
  export type SpawnRefusal = {
50
+ readonly type: "no_browser";
51
+ readonly op: string;
52
+ readonly parentThreadId: string;
53
+ readonly detail: string;
54
+ } | {
40
55
  readonly type: "parent_not_found";
41
56
  readonly op: string;
42
57
  readonly parentThreadId: string;
@@ -57,6 +72,18 @@ export interface ReportSessionPaging {
57
72
  }
58
73
  export interface ReportSessionSpawnDeps {
59
74
  readonly pool: Pool;
75
+ /**
76
+ * The chrome config that the same composition gives to the eyes tool. The
77
+ * spawn reads only whether it names a browser, thus the spawn and the tool
78
+ * decide the availability of the eyes from one value.
79
+ */
80
+ readonly chrome: ChromeConfig;
81
+ /**
82
+ * The capture seam that the same composition gives to the eyes tool. A
83
+ * present seam is a route to a look with no browser endpoint, thus it
84
+ * satisfies the gate on its own.
85
+ */
86
+ readonly capture?: CapturePage;
60
87
  }
61
88
  export interface ReportSessionSpawn {
62
89
  /**
@@ -64,10 +91,10 @@ export interface ReportSessionSpawn {
64
91
  * The child takes the analysis of the parent, the parent thread id, and the
65
92
  * anchor — the parent's latest `messages.seq` at this moment.
66
93
  *
67
- * Refused with a `SpawnRefusal`, no row written: an absent or archived
68
- * parent, a parent that is not a conversation, and a parent with no
69
- * messages. A store refusal (`DbError`, `ThreadInputError`) passes through
70
- * unchanged.
94
+ * Refused with a `SpawnRefusal`, no row written: a composition with no
95
+ * eyes, an absent or archived parent, a parent that is not a conversation,
96
+ * and a parent with no messages. A store refusal (`DbError`,
97
+ * `ThreadInputError`) passes through unchanged.
71
98
  */
72
99
  spawnReportSession(parentThreadId: string): ResultAsync<Thread, SpawnRefusal | DbError | ThreadInputError>;
73
100
  /**
@@ -18,13 +18,23 @@
18
18
  * A conversation thread id comes from the host UI. But the spawn is a harness
19
19
  * operation with no UI in front of it. Thus it mints a `randomUUID` and returns
20
20
  * the full row. A managed deployment gets the same behavior with no host code.
21
+ *
22
+ * The spawn also gates on the eyes of the composition. A report session records
23
+ * a version only after a look at the rendered page, and only a browser can look.
24
+ * Thus a composition with no browser and no injected capture seam can author a
25
+ * draft that no gate ever accepts. The spawn refuses that session before it
26
+ * writes a row, because a dead-end thread is worse than a refusal that names the
27
+ * absent capability.
21
28
  */
22
29
  import { randomUUID } from "node:crypto";
23
30
  import { errAsync } from "neverthrow";
31
+ import { hasBrowserUrl } from "../lib/chrome.js";
24
32
  import { createThreadHistory } from "../memory/thread-history.js";
25
33
  import { createThreadStore } from "../memory/thread-store.js";
26
34
  const _assertDomainError = true;
27
35
  const OP = "spawn-report-session";
36
+ /** The line that the caller reads when the composition gives no eyes. */
37
+ const NO_BROWSER_DETAIL = "the composition gives no browser, thus a report session can never record a version";
28
38
  /**
29
39
  * Compose the child title. N counts the existing report children of the parent
30
40
  * plus one. A parent with no title yields `Report N` alone, because the seed of
@@ -43,7 +53,16 @@ export function createReportSessionSpawn(deps) {
43
53
  const { pool } = deps;
44
54
  const store = createThreadStore(pool);
45
55
  const history = createThreadHistory(pool);
56
+ // The eyes of the composition are fixed at construction, thus the gate reads
57
+ // one boolean and never a live probe of the sidecar.
58
+ const eyesAvailable = deps.capture !== undefined || hasBrowserUrl(deps.chrome);
46
59
  function spawnReportSession(parentThreadId) {
60
+ // The gate runs before the parent read. A session with no route to a look
61
+ // reaches the record gate and refuses there forever, thus the spawn is the
62
+ // one honest place to say so.
63
+ if (!eyesAvailable) {
64
+ return errAsync({ type: "no_browser", op: OP, parentThreadId, detail: NO_BROWSER_DETAIL });
65
+ }
47
66
  return store.getThread(parentThreadId).andThen((parent) => {
48
67
  // An absent parent and an archived one arrive the same way — `getThread`
49
68
  // filters the tombstone — and both refuse as `parent_not_found`.
@@ -18,17 +18,22 @@
18
18
  * one. Neither store is ever written through these paths.
19
19
  *
20
20
  * Both are optional, and absence is a NORMAL state rather than an error: an
21
- * omitted path makes its tool report the store as unavailable or unknown, which
22
- * is a truthful answer an agent can act on. Absence must never be reported as a
23
- * failure, and never papered over with a guessed path.
21
+ * omitted path falls back to the store's container mountpoint — correct for a
22
+ * host whose own process sees the store the way a sandbox does, and harmless for
23
+ * one that does not, because every consumer stats the root before it reports
24
+ * anything. A host with nothing mounted there gets the same "unavailable" or
25
+ * "unknown" answer an omitted path has always produced, so the fallback can no
26
+ * more invent a store than an explicit path can. Set a path only to name a
27
+ * location the container mountpoint does not describe; absence must never be
28
+ * reported as a failure.
24
29
  */
25
30
  export interface EnvironmentStorePaths {
26
31
  /**
27
32
  * Host path of the reference store — the same bytes sandboxes mount at
28
- * `/mnt/refs`. Omit when no store is provisioned; reference discovery then
29
- * reports the store as unavailable. Note that an installed store reads as
30
- * absent if this is omitted, which is indistinguishable to an agent from
31
- * having no data — so omit it only when there is genuinely nothing staged.
33
+ * `/mnt/refs`. Omit when the host's own process sees the store at that same
34
+ * path, as a K8s pod holding the ref-store PVC does; set it when the host
35
+ * reads the store somewhere else, as a native process bind-mounting a host
36
+ * directory into Docker does.
32
37
  */
33
38
  readonly refStorePath?: string;
34
39
  /**
@@ -1,8 +1,11 @@
1
1
  /**
2
- * Resource limits — ported from orchestrator/src/config/resource-limits.ts.
2
+ * Resource limits — validates sandbox resource requests against cluster
3
+ * maximums.
3
4
  *
4
- * Validates sandbox resource requests against cluster maximums.
5
- * Used by K8sSandbox.
5
+ * The ceilings themselves are the embedder's: they arrive as
6
+ * `SandboxClientConfig.resourceLimits` and as the `resourcePolicy` this module
7
+ * validates. Nothing here reads the environment, so a host keeps one validated
8
+ * view of its own configuration rather than two that can disagree.
6
9
  */
7
10
  import { z } from "zod";
8
11
  export declare const GpuSpecSchema: z.ZodObject<{
@@ -47,13 +50,6 @@ export declare const DEFAULT_SANDBOX_MAX_STEPS = 75;
47
50
  export declare class ResourceLimitsConfigError extends Error {
48
51
  constructor(message: string);
49
52
  }
50
- /**
51
- * Load resource limits from environment variables.
52
- *
53
- * Required: SANDBOX_MAX_CPU, SANDBOX_MAX_MEMORY_GB, SANDBOX_MAX_GPU_COUNT
54
- * @throws ResourceLimitsConfigError if configuration is invalid
55
- */
56
- export declare function loadResourceLimits(): ResourceLimits;
57
53
  /**
58
54
  * Validate an embedder-supplied resource policy.
59
55
  *
@@ -1,8 +1,11 @@
1
1
  /**
2
- * Resource limits — ported from orchestrator/src/config/resource-limits.ts.
2
+ * Resource limits — validates sandbox resource requests against cluster
3
+ * maximums.
3
4
  *
4
- * Validates sandbox resource requests against cluster maximums.
5
- * Used by K8sSandbox.
5
+ * The ceilings themselves are the embedder's: they arrive as
6
+ * `SandboxClientConfig.resourceLimits` and as the `resourcePolicy` this module
7
+ * validates. Nothing here reads the environment, so a host keeps one validated
8
+ * view of its own configuration rather than two that can disagree.
6
9
  */
7
10
  import { z } from "zod";
8
11
  // ── Schemas ─────────────────────────────────────────────────────────
@@ -38,47 +41,6 @@ export class ResourceLimitsConfigError extends Error {
38
41
  this.name = "ResourceLimitsConfigError";
39
42
  }
40
43
  }
41
- // ── Parsing ─────────────────────────────────────────────────────────
42
- function parsePositiveNumber(name, value) {
43
- if (!value) {
44
- throw new ResourceLimitsConfigError(`${name} environment variable is required`);
45
- }
46
- const num = Number(value);
47
- if (isNaN(num) || num <= 0) {
48
- throw new ResourceLimitsConfigError(`${name} must be a positive number, got: ${value}`);
49
- }
50
- return num;
51
- }
52
- function parseNonNegativeInt(name, value) {
53
- if (!value) {
54
- throw new ResourceLimitsConfigError(`${name} environment variable is required`);
55
- }
56
- const num = Number(value);
57
- if (isNaN(num) || num < 0) {
58
- throw new ResourceLimitsConfigError(`${name} must be a non-negative number, got: ${value}`);
59
- }
60
- if (!Number.isInteger(num)) {
61
- throw new ResourceLimitsConfigError(`${name} must be an integer, got: ${value}`);
62
- }
63
- return num;
64
- }
65
- /**
66
- * Load resource limits from environment variables.
67
- *
68
- * Required: SANDBOX_MAX_CPU, SANDBOX_MAX_MEMORY_GB, SANDBOX_MAX_GPU_COUNT
69
- * @throws ResourceLimitsConfigError if configuration is invalid
70
- */
71
- export function loadResourceLimits() {
72
- const maxCpu = parsePositiveNumber("SANDBOX_MAX_CPU", process.env.SANDBOX_MAX_CPU);
73
- const maxMemoryGb = parsePositiveNumber("SANDBOX_MAX_MEMORY_GB", process.env.SANDBOX_MAX_MEMORY_GB);
74
- const maxGpuCount = parseNonNegativeInt("SANDBOX_MAX_GPU_COUNT", process.env.SANDBOX_MAX_GPU_COUNT);
75
- const limits = { maxCpu, maxMemoryGb, maxGpuCount };
76
- const result = ResourceLimitsSchema.safeParse(limits);
77
- if (!result.success) {
78
- throw new ResourceLimitsConfigError(`Invalid resource limits configuration: ${result.error.message}`);
79
- }
80
- return result.data;
81
- }
82
44
  /**
83
45
  * Validate an embedder-supplied resource policy.
84
46
  *
@@ -313,6 +313,8 @@ export declare const ScalarReferenceSchema: z.ZodDiscriminatedUnion<[z.ZodObject
313
313
  * the path. `unreadable-artifact` means that the entry exists, and that the resolver cannot read the
314
314
  * artifact as a table. Two conditions give the second reason. The file type refuses a read of a cell, or
315
315
  * the bytes are unrecoverable.
316
+ *
317
+ * `extraction-unavailable` means that the value tier must use an extraction arm that is not wired.
316
318
  */
317
319
  export declare const UnresolvedReasonSchema: z.ZodEnum<{
318
320
  "artifact-missing": "artifact-missing";
@@ -321,6 +323,7 @@ export declare const UnresolvedReasonSchema: z.ZodEnum<{
321
323
  "ambiguous-match": "ambiguous-match";
322
324
  "assertion-failed": "assertion-failed";
323
325
  "unreadable-artifact": "unreadable-artifact";
326
+ "extraction-unavailable": "extraction-unavailable";
324
327
  }>;
325
328
  /** A reference that resolution could not bind, with the reason and an optional detail. */
326
329
  export declare const UnresolvedReferenceSchema: z.ZodObject<{
@@ -419,6 +422,7 @@ export declare const UnresolvedReferenceSchema: z.ZodObject<{
419
422
  "ambiguous-match": "ambiguous-match";
420
423
  "assertion-failed": "assertion-failed";
421
424
  "unreadable-artifact": "unreadable-artifact";
425
+ "extraction-unavailable": "extraction-unavailable";
422
426
  }>;
423
427
  detail: z.ZodOptional<z.ZodString>;
424
428
  }, z.core.$strict>;
@@ -175,6 +175,8 @@ export const ScalarReferenceSchema = z.discriminatedUnion("kind", [ArtifactValue
175
175
  * the path. `unreadable-artifact` means that the entry exists, and that the resolver cannot read the
176
176
  * artifact as a table. Two conditions give the second reason. The file type refuses a read of a cell, or
177
177
  * the bytes are unrecoverable.
178
+ *
179
+ * `extraction-unavailable` means that the value tier must use an extraction arm that is not wired.
178
180
  */
179
181
  export const UnresolvedReasonSchema = z.enum([
180
182
  "artifact-missing",
@@ -183,6 +185,7 @@ export const UnresolvedReasonSchema = z.enum([
183
185
  "ambiguous-match",
184
186
  "assertion-failed",
185
187
  "unreadable-artifact",
188
+ "extraction-unavailable",
186
189
  ]);
187
190
  /** A reference that resolution could not bind, with the reason and an optional detail. */
188
191
  export const UnresolvedReferenceSchema = z.strictObject({
package/dist/index.d.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * the agent with its own seam realizations.
6
6
  */
7
7
  export { assembleCoreRuntime } from "./runtime/assemble.js";
8
- export type { CoreRuntime, CoreRuntimeDeps, CoreWorkflowDeps, RegisteredWorkflows, ConversationAssemblyDeps, SandboxStepCallable } from "./runtime/assemble.js";
8
+ export type { CoreRuntime, CoreRuntimeDeps, CoreWorkflowDeps, RegisteredWorkflows, ConversationAssemblyDeps, SandboxStepCallable, MakeReportReferenceResolver, ReportResolverScope, } from "./runtime/assemble.js";
9
+ export type { ReferenceResolver, ReportSnapshot, ResolvedValue } from "./report-model/reference-resolver.js";
9
10
  export type { ThreadAgentResolver, UnregisteredThreadType } from "./runtime/assemble.js";
10
11
  export { bootHarness } from "./runtime/boot.js";
11
12
  export type { BootHarnessDeps, BootedHarness } from "./runtime/boot.js";
@@ -11,8 +11,22 @@ import type { Logger } from "./logger.js";
11
11
  export interface ChromeConfig {
12
12
  /** Operational logging seam; omitted falls back to no-op. */
13
13
  readonly logger?: Logger;
14
+ /**
15
+ * The endpoint of the Chrome sidecar. The connection is out of process, thus the sidecar has its own
16
+ * filesystem and its own network position. A `file://` URL resolves against the filesystem of the
17
+ * sidecar and never against the filesystem of the harness host. A caller that navigates to a workspace
18
+ * file must run a sidecar that mounts the same workspace tree at the same path.
19
+ *
20
+ * An absent value means that the composition gives no browser at all. A caller that needs a look must
21
+ * report that condition, not attempt a connection.
22
+ */
14
23
  readonly browserUrl?: string;
15
24
  readonly maxPages?: number;
16
25
  }
26
+ /**
27
+ * Whether the config names a browser to connect to. A composition with no endpoint has no eyes, and a
28
+ * caller reads that up front instead of failing once for each attempted connection.
29
+ */
30
+ export declare function hasBrowserUrl(cfg: ChromeConfig): boolean;
17
31
  export declare function getBrowser(browserUrl?: string, injected?: Logger): Promise<Browser>;
18
32
  export declare function withPage<T>(cfg: ChromeConfig, fn: (page: Page, context: BrowserContext) => Promise<T>): Promise<T>;
@@ -8,6 +8,13 @@
8
8
  */
9
9
  import puppeteer from "puppeteer-core";
10
10
  import { createNoopLogger } from "./console-logger.js";
11
+ /**
12
+ * Whether the config names a browser to connect to. A composition with no endpoint has no eyes, and a
13
+ * caller reads that up front instead of failing once for each attempted connection.
14
+ */
15
+ export function hasBrowserUrl(cfg) {
16
+ return cfg.browserUrl !== undefined && cfg.browserUrl.trim().length > 0;
17
+ }
11
18
  let browser;
12
19
  let connecting;
13
20
  let semaphore;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The one page capture over the Chrome sidecar.
3
+ *
4
+ * A capture navigates to a page URL, collects the console errors and the failed requests, waits for the
5
+ * readiness signal of the page, and gives back a base64 screenshot. Two tools capture a report page: the
6
+ * preview snapshot of the old report path and the eyes tool of a report session. Both read this module.
7
+ *
8
+ * The readiness contract is the reason for one shared body. The renderer emits the event name and the
9
+ * sentinel name, and `report-render/page.ts` owns both constants together with the two budgets. This module
10
+ * imports them, thus a rename or a retime reaches the waiter at compile time. A second copy of the wait
11
+ * script would instead degrade every capture to the readiness timeout, and no type would break.
12
+ *
13
+ * The capture speaks the throw protocol, because the chrome connection does. A caller that needs a typed
14
+ * outcome guards the call.
15
+ */
16
+ import { type ChromeConfig } from "./chrome.js";
17
+ /** One request that the page could not load, with the reason that the browser gave. */
18
+ export interface FailedRequest {
19
+ url: string;
20
+ reason: string;
21
+ }
22
+ /** The picture and the faults of one page capture. */
23
+ export interface PageCapture {
24
+ screenshotBase64: string;
25
+ consoleErrors: string[];
26
+ failedRequests: FailedRequest[];
27
+ }
28
+ /**
29
+ * The capture seam. It navigates to a page URL, and it gives back the screenshot and the faults. The seam
30
+ * speaks the throw protocol, because the chrome connection does. A test injects a seam that reads no
31
+ * browser, thus a tool orchestration runs with no chrome sidecar.
32
+ */
33
+ export type CapturePage = (url: string) => Promise<PageCapture>;
34
+ /** The extra settle steps that one call site needs. A call site that needs neither passes nothing. */
35
+ export interface CaptureOptions {
36
+ /** A CSS selector to wait for after the readiness signal. The wait is best-effort. */
37
+ readonly waitForSelector?: string;
38
+ /** An extra settle time in milliseconds after the readiness signal, for a late paint. */
39
+ readonly waitMs?: number;
40
+ }
41
+ /**
42
+ * Capture one page: the screenshot, the console errors, and the failed requests.
43
+ *
44
+ * The readiness wait is best-effort. A page that never signals still captures at the readiness budget, thus
45
+ * a broken page gives a picture that shows what broke.
46
+ */
47
+ export declare function capturePage(chrome: ChromeConfig, url: string, options?: CaptureOptions): Promise<PageCapture>;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The one page capture over the Chrome sidecar.
3
+ *
4
+ * A capture navigates to a page URL, collects the console errors and the failed requests, waits for the
5
+ * readiness signal of the page, and gives back a base64 screenshot. Two tools capture a report page: the
6
+ * preview snapshot of the old report path and the eyes tool of a report session. Both read this module.
7
+ *
8
+ * The readiness contract is the reason for one shared body. The renderer emits the event name and the
9
+ * sentinel name, and `report-render/page.ts` owns both constants together with the two budgets. This module
10
+ * imports them, thus a rename or a retime reaches the waiter at compile time. A second copy of the wait
11
+ * script would instead degrade every capture to the readiness timeout, and no type would break.
12
+ *
13
+ * The capture speaks the throw protocol, because the chrome connection does. A caller that needs a typed
14
+ * outcome guards the call.
15
+ */
16
+ import { withPage } from "./chrome.js";
17
+ import { PAGE_NAV_TIMEOUT_MS, PAGE_READY_TIMEOUT_MS, THEME_READY_EVENT, THEME_READY_SENTINEL } from "../report-render/page.js";
18
+ /** The budget of the optional selector wait. The wait is best-effort, thus a miss captures the page anyway. */
19
+ const SELECTOR_TIMEOUT_MS = 5_000;
20
+ /**
21
+ * The body of the readiness wait, in the browser context. The sentinel arm resolves a page that dispatched
22
+ * the event before this wait registered, thus a plain listener never blocks forever. The timer arm bounds a
23
+ * page that never signals.
24
+ *
25
+ * The browser runs this body as source text. Thus the body reads no module binding, and each value that it
26
+ * needs arrives as a parameter. The page evaluation sends a parameter as data, thus no value becomes code.
27
+ */
28
+ function waitForThemeReady(sentinel, event, timeout) {
29
+ return new Promise((resolve) => {
30
+ // The sentinel name arrives as a value, thus no declared property of the global object describes it.
31
+ if (window[sentinel]) {
32
+ resolve();
33
+ return;
34
+ }
35
+ const timer = setTimeout(() => {
36
+ resolve();
37
+ }, timeout);
38
+ document.addEventListener(event, () => {
39
+ clearTimeout(timer);
40
+ resolve();
41
+ }, { once: true });
42
+ });
43
+ }
44
+ /**
45
+ * Capture one page: the screenshot, the console errors, and the failed requests.
46
+ *
47
+ * The readiness wait is best-effort. A page that never signals still captures at the readiness budget, thus
48
+ * a broken page gives a picture that shows what broke.
49
+ */
50
+ export function capturePage(chrome, url, options = {}) {
51
+ return withPage(chrome, async (page) => {
52
+ const consoleErrors = [];
53
+ const failedRequests = [];
54
+ page.on("console", (msg) => {
55
+ if (msg.type() === "error")
56
+ consoleErrors.push(msg.text());
57
+ });
58
+ page.on("pageerror", (err) => consoleErrors.push(`pageerror: ${err.message}`));
59
+ page.on("requestfailed", (req) => {
60
+ failedRequests.push({ url: req.url(), reason: req.failure()?.errorText ?? "unknown" });
61
+ });
62
+ await page.goto(url, { waitUntil: "networkidle2", timeout: PAGE_NAV_TIMEOUT_MS });
63
+ // The two names and the budget ride as arguments, because the browser context has no module scope.
64
+ await page.evaluate(waitForThemeReady, THEME_READY_SENTINEL, THEME_READY_EVENT, PAGE_READY_TIMEOUT_MS).catch(() => {
65
+ /* the picture captures as it stands */
66
+ });
67
+ if (options.waitForSelector !== undefined) {
68
+ await page.waitForSelector(options.waitForSelector, { timeout: SELECTOR_TIMEOUT_MS }).catch(() => {
69
+ /* the picture captures as it stands */
70
+ });
71
+ }
72
+ if (options.waitMs !== undefined && options.waitMs > 0) {
73
+ const settle = options.waitMs;
74
+ await new Promise((resolve) => setTimeout(resolve, settle));
75
+ }
76
+ const screenshot = await page.screenshot({ encoding: "base64", fullPage: false });
77
+ return {
78
+ screenshotBase64: typeof screenshot === "string" ? screenshot : Buffer.from(screenshot).toString("base64"),
79
+ consoleErrors,
80
+ failedRequests,
81
+ };
82
+ });
83
+ }