@inflexa-ai/harness 0.23.0 → 0.25.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.
Files changed (123) hide show
  1. package/README.md +1 -2
  2. package/dist/agents/conversation-agent.d.ts +0 -9
  3. package/dist/agents/sandbox/__fixtures__/deps.js +6 -0
  4. package/dist/agents/sandbox/data-profiler.js +5 -4
  5. package/dist/agents/sandbox/shared.js +11 -0
  6. package/dist/agents/sandbox/types.d.ts +1 -1
  7. package/dist/app/data-profile-orientation.js +30 -1
  8. package/dist/app/data-profile-policy.d.ts +33 -7
  9. package/dist/app/data-profile-policy.js +31 -9
  10. package/dist/contracts/chat-parts.d.ts +29 -33
  11. package/dist/contracts/content-url.d.ts +7 -41
  12. package/dist/contracts/content-url.js +7 -48
  13. package/dist/contracts/data-profile.d.ts +166 -0
  14. package/dist/contracts/index.d.ts +3 -2
  15. package/dist/contracts/index.js +1 -1
  16. package/dist/contracts/message.d.ts +1 -1
  17. package/dist/contracts/message.js +1 -1
  18. package/dist/contracts/part-registry.js +2 -3
  19. package/dist/contracts/schemas/chat-parts.d.ts +16 -53
  20. package/dist/contracts/schemas/chat-parts.js +15 -25
  21. package/dist/contracts/schemas/index.d.ts +1 -1
  22. package/dist/contracts/schemas/index.js +1 -1
  23. package/dist/execution/input-signature.d.ts +33 -0
  24. package/dist/execution/input-signature.js +0 -0
  25. package/dist/execution/run-authorizer.d.ts +1 -1
  26. package/dist/execution/run-authorizer.js +1 -1
  27. package/dist/index.d.ts +5 -4
  28. package/dist/index.js +5 -2
  29. package/dist/input-scan/coverage.d.ts +22 -0
  30. package/dist/input-scan/coverage.js +85 -0
  31. package/dist/input-scan/enrich.d.ts +41 -0
  32. package/dist/input-scan/enrich.js +227 -0
  33. package/dist/input-scan/formats.d.ts +41 -0
  34. package/dist/input-scan/formats.js +279 -0
  35. package/dist/input-scan/index.d.ts +12 -0
  36. package/dist/input-scan/index.js +11 -0
  37. package/dist/input-scan/scan.d.ts +50 -0
  38. package/dist/input-scan/scan.js +182 -0
  39. package/dist/input-scan/shapes.d.ts +46 -0
  40. package/dist/input-scan/shapes.js +0 -0
  41. package/dist/input-scan/types.d.ts +172 -0
  42. package/dist/input-scan/types.js +12 -0
  43. package/dist/lib/zod-issues.d.ts +2 -3
  44. package/dist/lib/zod-issues.js +2 -3
  45. package/dist/loop/__fixtures__/scripted-provider.js +3 -1
  46. package/dist/loop/run-agent.d.ts +5 -0
  47. package/dist/loop/run-agent.js +10 -1
  48. package/dist/loop/run-to-terminal.d.ts +1 -1
  49. package/dist/loop/run-to-terminal.js +1 -1
  50. package/dist/memory/card-builders.d.ts +0 -27
  51. package/dist/memory/card-builders.js +0 -75
  52. package/dist/memory/conversation-display-backfill.d.ts +2 -4
  53. package/dist/memory/conversation-display-backfill.js +3 -9
  54. package/dist/memory/conversation-display-storage.d.ts +3 -4
  55. package/dist/memory/conversation-display-storage.js +3 -4
  56. package/dist/memory/reconstruct-cards.d.ts +5 -7
  57. package/dist/memory/reconstruct-cards.js +6 -20
  58. package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
  59. package/dist/prompts/sandbox/data-profiler.js +159 -275
  60. package/dist/provenance/exec-frame.js +9 -3
  61. package/dist/runtime/boot.js +2 -12
  62. package/dist/sandbox/client.d.ts +7 -0
  63. package/dist/sandbox/create-sandbox.js +1 -0
  64. package/dist/sandbox/docker-client.d.ts +1 -0
  65. package/dist/sandbox/docker-client.js +20 -15
  66. package/dist/sandbox/k8s-client.d.ts +6 -5
  67. package/dist/sandbox/k8s-client.js +55 -39
  68. package/dist/sandbox/reaper.d.ts +24 -13
  69. package/dist/sandbox/reaper.js +47 -14
  70. package/dist/sandbox/types.d.ts +6 -5
  71. package/dist/schemas/data-profile-schemas.d.ts +62 -0
  72. package/dist/schemas/data-profile-schemas.js +68 -2
  73. package/dist/state/data-profile.d.ts +7 -88
  74. package/dist/state/index.d.ts +1 -1
  75. package/dist/tasks/data-profile-activity.d.ts +8 -0
  76. package/dist/tasks/data-profile-activity.js +1 -0
  77. package/dist/tasks/data-profile-index.d.ts +45 -0
  78. package/dist/tasks/data-profile-index.js +134 -0
  79. package/dist/tasks/data-profile.d.ts +8 -23
  80. package/dist/tasks/data-profile.js +150 -117
  81. package/dist/tools/report-session/preview-report.d.ts +2 -1
  82. package/dist/tools/report-session/preview-report.js +9 -1
  83. package/dist/tools/research/generate-plan.js +1 -1
  84. package/dist/tools/research/inspect-data-profile.d.ts +30 -4
  85. package/dist/tools/research/inspect-data-profile.js +66 -10
  86. package/dist/tools/start-report-session.js +2 -2
  87. package/dist/tools/workspace/scan-inputs.d.ts +38 -0
  88. package/dist/tools/workspace/scan-inputs.js +61 -0
  89. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  90. package/dist/tools/workspace/workspace-search.js +12 -9
  91. package/dist/workspace/filesystem.d.ts +27 -0
  92. package/dist/workspace/filesystem.js +27 -0
  93. package/dist/workspace/paths.d.ts +0 -21
  94. package/dist/workspace/paths.js +0 -37
  95. package/package.json +1 -3
  96. package/dist/agents/report-builder.d.ts +0 -34
  97. package/dist/agents/report-builder.js +0 -34
  98. package/dist/execution/report-render.d.ts +0 -39
  99. package/dist/execution/report-render.js +0 -98
  100. package/dist/execution/report-runner.d.ts +0 -111
  101. package/dist/execution/report-runner.js +0 -230
  102. package/dist/prompts/report-builder.d.ts +0 -1
  103. package/dist/prompts/report-builder.js +0 -174
  104. package/dist/tools/iterate-report.d.ts +0 -227
  105. package/dist/tools/iterate-report.js +0 -883
  106. package/dist/tools/lib/preview-lock.d.ts +0 -20
  107. package/dist/tools/lib/preview-lock.js +0 -35
  108. package/dist/tools/lib/report-preflight.d.ts +0 -39
  109. package/dist/tools/lib/report-preflight.js +0 -283
  110. package/dist/tools/report/build-report.d.ts +0 -13
  111. package/dist/tools/report/build-report.js +0 -27
  112. package/dist/tools/report/index.d.ts +0 -15
  113. package/dist/tools/report/index.js +0 -15
  114. package/dist/tools/report/mint-preview-url.d.ts +0 -26
  115. package/dist/tools/report/mint-preview-url.js +0 -47
  116. package/dist/tools/report/preview-publisher.d.ts +0 -53
  117. package/dist/tools/report/preview-publisher.js +0 -45
  118. package/dist/tools/report/preview-snapshot.d.ts +0 -34
  119. package/dist/tools/report/preview-snapshot.js +0 -98
  120. package/dist/tools/report/submit-report.d.ts +0 -26
  121. package/dist/tools/report/submit-report.js +0 -148
  122. package/dist/tools/report/version-fs.d.ts +0 -15
  123. package/dist/tools/report/version-fs.js +0 -165
package/README.md CHANGED
@@ -36,7 +36,6 @@ import {
36
36
  createNoopRunCharge, // RunCharge seam
37
37
  createNoopUsageRecorder, // UsageRecorder seam
38
38
  createNoopArtifactRegistry, // ArtifactRegistry seam
39
- UnavailablePreviewPublisher, // PreviewPublisher seam
40
39
  createDbosRunLauncher, // shared RunLauncher
41
40
  createConsoleLogger, // Logger — writes to console
42
41
  createNoopLogger, // Logger — discards (the fallback when none is wired)
@@ -47,7 +46,7 @@ import {
47
46
  } from "@inflexa-ai/harness";
48
47
  ```
49
48
 
50
- `assembleCoreRuntime` is the single composition point: it registers the durable workflows and builds the conversation agent over them. The harness declares **six capability seams** an embedder wires — `RunAuthorizer`, `ResolveBilling`, `ArtifactRegistry`, `RunCharge`, `UsageRecorder`, `PreviewPublisher` — plus the **shared `RunLauncher`** (one host-neutral realization, `createDbosRunLauncher`). Local, dependency-free realizations of all six ship from the barrel; an embedder constructs them (or its own) and passes them in. The harness only ever sees the interface.
49
+ `assembleCoreRuntime` is the single composition point: it registers the durable workflows and builds the conversation agent over them. The harness declares **five capability seams** an embedder wires — `RunAuthorizer`, `ResolveBilling`, `ArtifactRegistry`, `RunCharge`, `UsageRecorder` — plus the **shared `RunLauncher`** (one host-neutral realization, `createDbosRunLauncher`). Local, dependency-free realizations of all five ship from the barrel; an embedder constructs them (or its own) and passes them in. The harness only ever sees the interface.
51
50
 
52
51
  ## Logging
53
52
 
@@ -35,7 +35,6 @@ import { type BioToolKeys } from "../tools/bio/keys.js";
35
35
  import type { EnvironmentStorePaths } from "../config/environment-stores.js";
36
36
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
37
37
  import type { RunLauncher } from "../execution/run-launcher.js";
38
- import type { SubmitReportDeps } from "../tools/iterate-report.js";
39
38
  import type { Logger } from "../lib/logger.js";
40
39
  import type { UsageRecorder } from "../billing/usage-recorder.js";
41
40
  import type { CitationResolver } from "../citations/types.js";
@@ -98,16 +97,8 @@ export interface ConversationAgentDeps extends EnvironmentStorePaths {
98
97
  * workflows through it so the durability engine stays out of the tools.
99
98
  */
100
99
  readonly runLauncher: RunLauncher;
101
- /**
102
- * Preview-publishing seam factory. No tool of the roster reads it. The field
103
- * stays, because a change to the deps breaks an embedder at its composition
104
- * root.
105
- */
106
- readonly createPreviewPublisher: SubmitReportDeps["createPreviewPublisher"];
107
100
  /** API keys for the external bio/chem data sources. */
108
101
  readonly bioKeys: BioToolKeys;
109
- /** Root templates dir. No tool of the roster reads it. */
110
- readonly templatesDir: string;
111
102
  /** Skills root. No tool of the roster reads it. */
112
103
  readonly skillsDir: string;
113
104
  /** Headless-Chrome config for report snapshot/preview rendering. */
@@ -30,6 +30,9 @@ export function makeFakeSandboxClient() {
30
30
  async isAlive() {
31
31
  return { alive: true, oomKilled: false };
32
32
  },
33
+ async isAliveById() {
34
+ return { alive: true, oomKilled: false };
35
+ },
33
36
  async teardown() { },
34
37
  async teardownById() { },
35
38
  async listManagedSandboxes() {
@@ -42,6 +45,9 @@ export function makeFakeWorkspaceFs() {
42
45
  readFile() {
43
46
  return okAsync({ kind: "ok", content: Buffer.alloc(0), truncated: false });
44
47
  },
48
+ readBytes() {
49
+ return okAsync({ kind: "ok", bytes: Buffer.alloc(0) });
50
+ },
45
51
  list() {
46
52
  return okAsync({ kind: "ok", entries: [] });
47
53
  },
@@ -10,10 +10,11 @@ export const meta = {
10
10
  capabilities: ["data profiling", "literature review", "methodology research", "analysis planning", "experimental design analysis"],
11
11
  suitableFor: ["bulk-rna-seq", "single-cell", "proteomics", "metabolomics", "genomics", "transcriptomics", "chemical-structures", "compound-screening"],
12
12
  skills: [],
13
- tools: [...BASE_SANDBOX_TOOLS],
14
- // Profiling fans out one programmatic pass per input file (head/wc preview +
15
- // a Python script + read-back) before the single submit_profile, so a
16
- // many-file analysis needs more headroom than the sandbox default.
13
+ // `scanInputs` is the profiler's own instrument: the workflow injects one scan into
14
+ // its briefing, and the tool is how it re-scans a subtree it wants to group
15
+ // differently. Declared rather than bolted on, so the resolved roster stays
16
+ // `meta.tools` plus the always-on set.
17
+ tools: [...BASE_SANDBOX_TOOLS, "scanInputs"],
17
18
  defaultMaxSteps: 85,
18
19
  plannable: false,
19
20
  };
@@ -30,6 +30,7 @@ import { chemblTool, genePreclinicalProfileTool, lookupAnnotationTool, openTarge
30
30
  import { createNcbiTools, createChemDbTools } from "../../tools/bio/keys.js";
31
31
  // Workspace read surface.
32
32
  import { createFileStatTool, createGrepTool, createListFilesTool, createReadFileTool, createWorkspaceSearchTool } from "../../tools/workspace/index.js";
33
+ import { createScanInputsTool } from "../../tools/workspace/scan-inputs.js";
33
34
  // Workspace mutate surface.
34
35
  import { createEditFileTool, createExecuteCommandTool, createWorkspaceMutator, createWriteFileTool } from "../../tools/workspace/index.js";
35
36
  // Skills (declared per agent via meta.skills).
@@ -60,6 +61,16 @@ function resolveSandboxTools(deps, tools) {
60
61
  const registry = {
61
62
  listAvailablePackages: createListAvailablePackagesTool({ ...(deps.packagesFile ? { packagesFile: deps.packagesFile } : {}) }),
62
63
  listAvailableRefs: createListAvailableRefsTool({ ...(deps.refStorePath ? { refStorePath: deps.refStorePath } : {}) }),
64
+ scanInputs: createScanInputsTool({
65
+ workspaceFs: deps.workspaceFs,
66
+ analysisId: deps.step.analysisId,
67
+ sandboxClient: deps.sandboxClient,
68
+ sandbox: deps.step.sandbox,
69
+ workflowId: deps.step.workflowId,
70
+ stepId: deps.step.stepId,
71
+ nextFunctionId: deps.step.nextFunctionId,
72
+ deadlineMs: deps.step.deadlineMs,
73
+ }),
63
74
  resolveLibraryId: resolveLibraryIdTool,
64
75
  queryDocs: queryDocsTool,
65
76
  inspectRun: createInspectRunTool(deps.pool),
@@ -12,7 +12,7 @@
12
12
  * in the tool's `perSource` report; the rest of the call still answers.
13
13
  */
14
14
  /** Closed allowlist of tools any sandbox agent may declare. */
15
- export type SandboxToolName = "listAvailablePackages" | "listAvailableRefs" | "resolveLibraryId" | "queryDocs" | "inspectRun" | "pubmed" | "resolve_citation" | "searchGene" | "lookupAnnotation" | "searchInteractions" | "chembl" | "pubchem" | "opentargets" | "geneDiseaseEvidence" | "drugGeneInteractions" | "genePreclinicalProfile" | "searchFaers" | "searchClinicalTrials" | "searchGeoDatasets" | "targetSafety" | "comptox";
15
+ export type SandboxToolName = "listAvailablePackages" | "listAvailableRefs" | "scanInputs" | "resolveLibraryId" | "queryDocs" | "inspectRun" | "pubmed" | "resolve_citation" | "searchGene" | "lookupAnnotation" | "searchInteractions" | "chembl" | "pubchem" | "opentargets" | "geneDiseaseEvidence" | "drugGeneInteractions" | "genePreclinicalProfile" | "searchFaers" | "searchClinicalTrials" | "searchGeoDatasets" | "targetSafety" | "comptox";
16
16
  /** Planner-facing metadata + tool allowlist for one sandbox agent. */
17
17
  export interface AgentMeta {
18
18
  /** Stable agent id — also the key in the catalog and the `AgentDefinition.id`. */
@@ -18,6 +18,9 @@
18
18
  export const DATA_PROFILE_ORIENTATION_MAX_CHARS = 1200;
19
19
  /** Files listed before the tail is elided (the count still reports the total). */
20
20
  const MAX_FILES = 8;
21
+ /** Kinds listed before the tail is elided. A dataset with more is described by its first few. */
22
+ const MAX_KINDS = 6;
23
+ const MAX_KIND_DESCRIPTION_CHARS = 90;
21
24
  /** Quality concerns listed — the profiler orders them, so these are the top ones. */
22
25
  const MAX_CONCERNS = 3;
23
26
  const MAX_DESIGN_CHARS = 200;
@@ -105,9 +108,35 @@ export function buildDataProfileOrientation(result, maxChars = DATA_PROFILE_ORIE
105
108
  const more = concerns.length > shown.length ? ` (+${concerns.length - shown.length} more)` : "";
106
109
  lines.push(`Concerns: ${shown.join("; ")}${more}`);
107
110
  }
111
+ // Kinds come first because they are what the dataset IS at this budget: four lines
112
+ // describing 3513 files, where the per-file list could only show the first eight of
113
+ // them and imply the rest away.
114
+ const kinds = result.kinds ?? [];
115
+ if (kinds.length > 0) {
116
+ const shown = kinds.slice(0, MAX_KINDS);
117
+ const datasetFiles = kinds.reduce((sum, kind) => sum + kind.count, 0);
118
+ const header = shown.length < kinds.length
119
+ ? `Kinds (${shown.length} of ${kinds.length}, ${datasetFiles} files):`
120
+ : `Kinds (${kinds.length}, ${datasetFiles} files):`;
121
+ lines.push(header);
122
+ for (const kind of shown) {
123
+ const facts = [`${kind.count}x`, present(kind.format)].filter((f) => f !== undefined).join(", ");
124
+ lines.push(`- ${kind.name} (${facts}) — ${clip(kind.memberRepresents, MAX_KIND_DESCRIPTION_CHARS)}`);
125
+ }
126
+ const axes = result.axes ?? [];
127
+ if (axes.length > 0) {
128
+ lines.push(`Axes: ${axes.map((axis) => `${axis.label} (${axis.cardinality})`).join(", ")}`);
129
+ }
130
+ if (result.coverage && result.coverage.unmatched > 0) {
131
+ lines.push(`Coverage: ${result.coverage.matched} of ${result.coverage.total} files match a kind`);
132
+ }
133
+ }
108
134
  if (result.files.length > 0) {
109
135
  const shown = result.files.slice(0, MAX_FILES);
110
- const header = shown.length < result.files.length ? `Files (${shown.length} of ${result.files.length}):` : `Files (${result.files.length}):`;
136
+ // With kinds present these are the individually notable files — the metadata
137
+ // sheet, the README, the outlier — not a sample of the dataset.
138
+ const label = kinds.length > 0 ? "Notable files" : "Files";
139
+ const header = shown.length < result.files.length ? `${label} (${shown.length} of ${result.files.length}):` : `${label} (${result.files.length}):`;
111
140
  lines.push(header);
112
141
  for (const file of shown) {
113
142
  lines.push(`- ${file.path} — ${clip(file.description, MAX_FILE_DESCRIPTION_CHARS)}${formatFileFacts(file)}`);
@@ -5,13 +5,37 @@
5
5
  * — staging stays embedder-side per the data-profile-init spec, and the atomic running→expired
6
6
  * flip stays an SQL guard (`expireStaleDataProfile`), not part of this decision.
7
7
  */
8
- export type DataProfileLifecycleStatus = "pending" | "running" | "completed" | "failed";
8
+ import type { DataProfileInputSignature, DataProfileLifecycleStatus } from "../contracts/data-profile.js";
9
+ export type { DataProfileLifecycleStatus };
9
10
  /**
10
- * A completed profile is stale when the analysis's seed input set no longer
11
- * matches the set the profile actually covered — a file was added or swapped
12
- * since profiling. An empty seed set is never stale (nothing to compare).
11
+ * The analysis's inputs as they are NOW. A caller holding sizes and mtimes supplies
12
+ * the signature; one holding only the ledger's seeded ids supplies just those, and the
13
+ * comparison degrades honestly to what those ids can settle.
13
14
  */
14
- export declare function isDataProfileStale(seedInputFileIds: readonly string[], profiledInputFileIds: readonly string[]): boolean;
15
+ export interface CurrentInputSet {
16
+ readonly fileIds: readonly string[];
17
+ readonly signature?: DataProfileInputSignature;
18
+ }
19
+ /** The comparand a profile snapshot carries. Which one is present depends on its era. */
20
+ export interface ProfiledInputSnapshot {
21
+ readonly inputSignature?: DataProfileInputSignature;
22
+ readonly inputFileIds?: readonly string[];
23
+ }
24
+ /**
25
+ * A completed profile is stale when the analysis's current input set no longer matches
26
+ * the set the profile actually covered.
27
+ *
28
+ * The signature is preferred where both sides carry one: it covers size and mtime as
29
+ * well as identity, so an in-place edit is drift too. A snapshot predating the
30
+ * signature falls back to its identity list rather than being treated as drift, since
31
+ * that list answers the same question for the set it covered. A snapshot carrying
32
+ * NEITHER is drift: nothing establishes what it covered, and one re-profile heals it —
33
+ * exactly how a wholly absent `result` is already treated.
34
+ *
35
+ * An empty current set is never stale: there is nothing to compare, and an analysis
36
+ * whose inputs were removed is handled by clearing the profile, not by re-profiling it.
37
+ */
38
+ export declare function isDataProfileStale(current: CurrentInputSet, profiled: ProfiledInputSnapshot | null | undefined): boolean;
15
39
  export type DataProfileAction = {
16
40
  readonly kind: "none";
17
41
  } | {
@@ -21,8 +45,10 @@ export type DataProfileAction = {
21
45
  };
22
46
  export interface DataProfileDecisionInput {
23
47
  readonly status: DataProfileLifecycleStatus;
24
- readonly seedInputFileIds: readonly string[];
25
- readonly profiledInputFileIds: readonly string[];
48
+ /** The analysis's inputs as they are now — the seeded ids, plus a signature where the caller has one. */
49
+ readonly current: CurrentInputSet;
50
+ /** The comparand the stored profile carries, or null when it holds none. */
51
+ readonly profiled: ProfiledInputSnapshot | null;
26
52
  }
27
53
  /**
28
54
  * Decide the next profiling action from a (post-expire) status:
@@ -6,17 +6,39 @@
6
6
  * flip stays an SQL guard (`expireStaleDataProfile`), not part of this decision.
7
7
  */
8
8
  /**
9
- * A completed profile is stale when the analysis's seed input set no longer
10
- * matches the set the profile actually covered — a file was added or swapped
11
- * since profiling. An empty seed set is never stale (nothing to compare).
9
+ * A completed profile is stale when the analysis's current input set no longer matches
10
+ * the set the profile actually covered.
11
+ *
12
+ * The signature is preferred where both sides carry one: it covers size and mtime as
13
+ * well as identity, so an in-place edit is drift too. A snapshot predating the
14
+ * signature falls back to its identity list rather than being treated as drift, since
15
+ * that list answers the same question for the set it covered. A snapshot carrying
16
+ * NEITHER is drift: nothing establishes what it covered, and one re-profile heals it —
17
+ * exactly how a wholly absent `result` is already treated.
18
+ *
19
+ * An empty current set is never stale: there is nothing to compare, and an analysis
20
+ * whose inputs were removed is handled by clearing the profile, not by re-profiling it.
12
21
  */
13
- export function isDataProfileStale(seedInputFileIds, profiledInputFileIds) {
14
- if (seedInputFileIds.length === 0)
22
+ export function isDataProfileStale(current, profiled) {
23
+ if (current.fileIds.length === 0 && current.signature === undefined)
15
24
  return false;
16
- if (seedInputFileIds.length !== profiledInputFileIds.length)
25
+ if (!profiled)
17
26
  return true;
18
- const profiled = new Set(profiledInputFileIds);
19
- return seedInputFileIds.some((id) => !profiled.has(id));
27
+ if (profiled.inputSignature) {
28
+ // A caller with no signature of its own can still compare counts, which catches
29
+ // an added or removed file. It cannot catch a same-count swap, and claiming
30
+ // otherwise would be a verdict the comparand does not support.
31
+ if (!current.signature)
32
+ return profiled.inputSignature.count !== current.fileIds.length;
33
+ return profiled.inputSignature.digest !== current.signature.digest || profiled.inputSignature.count !== current.signature.count;
34
+ }
35
+ if (profiled.inputFileIds) {
36
+ if (current.fileIds.length !== profiled.inputFileIds.length)
37
+ return true;
38
+ const covered = new Set(profiled.inputFileIds);
39
+ return current.fileIds.some((id) => !covered.has(id));
40
+ }
41
+ return true;
20
42
  }
21
43
  /**
22
44
  * Decide the next profiling action from a (post-expire) status:
@@ -30,7 +52,7 @@ export function isDataProfileStale(seedInputFileIds, profiledInputFileIds) {
30
52
  export function decideDataProfileAction(input) {
31
53
  if (input.status === "pending")
32
54
  return { kind: "trigger" };
33
- if (input.status === "completed" && isDataProfileStale(input.seedInputFileIds, input.profiledInputFileIds)) {
55
+ if (input.status === "completed" && isDataProfileStale(input.current, input.profiled)) {
34
56
  return { kind: "retrigger" };
35
57
  }
36
58
  return { kind: "none" };
@@ -331,36 +331,8 @@ export interface RunFailedPart {
331
331
  /** Machine-readable failure reason (e.g., "budget_exceeded"). */
332
332
  reason?: string;
333
333
  }
334
- export interface PreviewPart {
335
- type: "data-report-preview";
336
- /** Unique per emission (UUID). */
337
- id: string;
338
- /** Groups all versions of the same preview. */
339
- previewId: string;
340
- /** Version number (1, 2, 3, ...). */
341
- version: number;
342
- /** Report title. */
343
- title: string;
344
- /** Version-relative path (e.g., "v1/index.html"). */
345
- previewPath: string;
346
- /** Report format. */
347
- format: "html" | "pdf";
348
- }
349
- export interface DataPreviewFailedPart {
350
- type: "data-report-preview-failed";
351
- /** Unique per emission (UUID). */
352
- id: string;
353
- /** Groups all versions of the same preview. */
354
- previewId: string;
355
- /** Attempted version number that failed. */
356
- version: number;
357
- /** Human-readable failure reason. */
358
- reason: string;
359
- /** Optional taxonomy of failure kinds. */
360
- errorKind?: "render" | "submit" | "build" | "timeout" | "internal";
361
- }
362
334
  /**
363
- * The record that a turn started a report session. It rides the chat stream
335
+ * The record that a turn started a child session. It rides the chat stream
364
336
  * live, and it persists into the turn at the position of the spawn.
365
337
  *
366
338
  * The part is a placement record and a freshness signal only. The thread
@@ -368,11 +340,35 @@ export interface DataPreviewFailedPart {
368
340
  * archived state. When the thread of the part is archived or absent, a
369
341
  * consumer renders nothing for it.
370
342
  */
371
- export interface ReportSessionStartedPart {
372
- type: "data-report-session-started";
373
- /** The id of the report thread that the spawn made. */
343
+ export interface ChildSessionStartedPart {
344
+ type: "data-child-session-started";
345
+ /** The id of the child thread that the spawn made. */
374
346
  threadId: string;
375
347
  /** The id of the conversation thread that spawned it. */
376
348
  parentThreadId: string;
349
+ /**
350
+ * The type of the child thread, in the thread store's vocabulary.
351
+ * Known values today: `"report"`.
352
+ */
353
+ threadType: string;
354
+ }
355
+ /**
356
+ * The record that a report-session preview rendered the page. It rides the
357
+ * chat stream live, and it persists into the turn at the position of the
358
+ * render.
359
+ *
360
+ * The part is a placement record and a freshness signal only. The version
361
+ * store and the session-page mint stay the authority for what is viewable:
362
+ * the part carries no path, no format, and no URL. A consumer that wants the
363
+ * page reads those surfaces.
364
+ */
365
+ export interface ReportRenderedPart {
366
+ type: "data-report-rendered";
367
+ /** Unique per emission. */
368
+ id: string;
369
+ /** The ISO timestamp of the successful render. */
370
+ renderedAt: string;
371
+ /** The title of the rendered document. */
372
+ title?: string;
377
373
  }
378
- export type CortexChatPart = PresentationPart | PlanPart | RunCardPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | PreviewPart | DataPreviewFailedPart | ReportSessionStartedPart;
374
+ export type CortexChatPart = PresentationPart | PlanPart | RunCardPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | ChildSessionStartedPart | ReportRenderedPart;
@@ -1,60 +1,26 @@
1
1
  /**
2
2
  * Content URL construction — shared between the frontend and Cortex.
3
3
  *
4
- * The `res` claim formulas and URL shapes are the canonical TypeScript contract
5
- * for the content-token flow. The storage backend mirrors each formula in Go; both
6
- * sides are locked by the shared test vectors at
7
- * `src/__tests__/fixtures/preview-res.json` (previews) and
8
- * `src/__tests__/fixtures/report-session-res.json` (report-session pages), each a
4
+ * The `res` claim formula and URL shape are the canonical TypeScript contract
5
+ * for the content-token flow. The storage backend mirrors the formula in Go; both
6
+ * sides are locked by the shared test vector at
7
+ * `src/__tests__/fixtures/report-session-res.json` (report-session pages), a
9
8
  * byte-identical copy of the storage backend's `kernel/contenttoken/testdata` file.
10
9
  *
11
- * End-to-end contract (previews):
12
- * token res claim : "previews/{analysisId}/{previewId}"
13
- * URL : {contentBaseUrl}/previews/{analysisId}/{previewId}/{previewPath}?t={token}
14
- * Caddy predicate : request.path.startsWith("/" + res + "/")
15
- *
16
10
  * End-to-end contract (report-session pages):
17
11
  * token res claim : "report-sessions/{analysisId}/{threadId}" (token kind "rs")
18
12
  * URL : {contentBaseUrl}/report-sessions/{analysisId}/{threadId}/{pagePath}?t={token}
19
13
  * Caddy predicate : request.path.startsWith("/" + res + "/")
20
14
  *
21
- * This is URL space only. On disk a preview lives inside its analysis's workspace tree, at
22
- * `{resolveWorkspaceRoot(analysisId)}/previews/{previewId}/{previewPath}` (the workspace-layout
23
- * spec), and a session page at `{resolveWorkspaceRoot(analysisId)}/report-sessions/{threadId}/{pagePath}`
15
+ * This is URL space only. On disk a session page lives inside its analysis's workspace tree, at
16
+ * `{resolveWorkspaceRoot(analysisId)}/report-sessions/{threadId}/{pagePath}`
24
17
  * (the report-session-agent spec) — the `res` claim carries the analysis id because the URL needs an
25
18
  * authorization boundary, whereas the resolved root already identifies the analysis and so does not
26
- * repeat it. A host that serves either space owns the map between the two.
19
+ * repeat it. A host that serves the space owns the map between the two.
27
20
  *
28
21
  * The harness's contracts module owns this TS implementation so both the harness and
29
22
  * react-client consume one canonical formula.
30
23
  */
31
- /**
32
- * Canonical `res` claim formula for a preview. Returns the literal string
33
- * `previews/{analysisId}/{previewId}` with no leading or trailing slash.
34
- *
35
- * This is the URL sub-path served by the content-server (Caddy) and the token's
36
- * authorization boundary — NOT a filesystem sub-path; see the module doc.
37
- *
38
- * MUST match `fmt.Sprintf("previews/%s/%s", analysisID, previewID)` in
39
- * the storage backend's Go preview-access implementation. Drift is caught by the shared test
40
- * vector at `src/__tests__/fixtures/preview-res.json`.
41
- */
42
- export declare function previewResourceId(analysisId: string, previewId: string): string;
43
- /**
44
- * Build a content-token preview URL for an iframe src.
45
- *
46
- * Produces `{contentBaseUrl}/{res}/{previewPath}?t={token}` where `res` is
47
- * the output of `previewResourceId(analysisId, previewId)`. The query parameter
48
- * name is `t` (matching Caddy's `from_query t` directive). The token is
49
- * URL-encoded.
50
- *
51
- * @param contentBaseUrl - Content server base URL (trailing slash tolerated)
52
- * @param analysisId - The analysis ID (authorization boundary)
53
- * @param previewId - The preview ID (groups all versions)
54
- * @param previewPath - Version-relative path (e.g., "v1/index.html"). Leading slash tolerated.
55
- * @param token - Short-lived content token minted by the storage backend (required)
56
- */
57
- export declare function buildPreviewUrl(contentBaseUrl: string, analysisId: string, previewId: string, previewPath: string, token: string): string;
58
24
  /**
59
25
  * Canonical `res` claim formula for a report-session page. Returns the literal string
60
26
  * `report-sessions/{analysisId}/{threadId}` with no leading or trailing slash.
@@ -1,67 +1,26 @@
1
1
  /**
2
2
  * Content URL construction — shared between the frontend and Cortex.
3
3
  *
4
- * The `res` claim formulas and URL shapes are the canonical TypeScript contract
5
- * for the content-token flow. The storage backend mirrors each formula in Go; both
6
- * sides are locked by the shared test vectors at
7
- * `src/__tests__/fixtures/preview-res.json` (previews) and
8
- * `src/__tests__/fixtures/report-session-res.json` (report-session pages), each a
4
+ * The `res` claim formula and URL shape are the canonical TypeScript contract
5
+ * for the content-token flow. The storage backend mirrors the formula in Go; both
6
+ * sides are locked by the shared test vector at
7
+ * `src/__tests__/fixtures/report-session-res.json` (report-session pages), a
9
8
  * byte-identical copy of the storage backend's `kernel/contenttoken/testdata` file.
10
9
  *
11
- * End-to-end contract (previews):
12
- * token res claim : "previews/{analysisId}/{previewId}"
13
- * URL : {contentBaseUrl}/previews/{analysisId}/{previewId}/{previewPath}?t={token}
14
- * Caddy predicate : request.path.startsWith("/" + res + "/")
15
- *
16
10
  * End-to-end contract (report-session pages):
17
11
  * token res claim : "report-sessions/{analysisId}/{threadId}" (token kind "rs")
18
12
  * URL : {contentBaseUrl}/report-sessions/{analysisId}/{threadId}/{pagePath}?t={token}
19
13
  * Caddy predicate : request.path.startsWith("/" + res + "/")
20
14
  *
21
- * This is URL space only. On disk a preview lives inside its analysis's workspace tree, at
22
- * `{resolveWorkspaceRoot(analysisId)}/previews/{previewId}/{previewPath}` (the workspace-layout
23
- * spec), and a session page at `{resolveWorkspaceRoot(analysisId)}/report-sessions/{threadId}/{pagePath}`
15
+ * This is URL space only. On disk a session page lives inside its analysis's workspace tree, at
16
+ * `{resolveWorkspaceRoot(analysisId)}/report-sessions/{threadId}/{pagePath}`
24
17
  * (the report-session-agent spec) — the `res` claim carries the analysis id because the URL needs an
25
18
  * authorization boundary, whereas the resolved root already identifies the analysis and so does not
26
- * repeat it. A host that serves either space owns the map between the two.
19
+ * repeat it. A host that serves the space owns the map between the two.
27
20
  *
28
21
  * The harness's contracts module owns this TS implementation so both the harness and
29
22
  * react-client consume one canonical formula.
30
23
  */
31
- /**
32
- * Canonical `res` claim formula for a preview. Returns the literal string
33
- * `previews/{analysisId}/{previewId}` with no leading or trailing slash.
34
- *
35
- * This is the URL sub-path served by the content-server (Caddy) and the token's
36
- * authorization boundary — NOT a filesystem sub-path; see the module doc.
37
- *
38
- * MUST match `fmt.Sprintf("previews/%s/%s", analysisID, previewID)` in
39
- * the storage backend's Go preview-access implementation. Drift is caught by the shared test
40
- * vector at `src/__tests__/fixtures/preview-res.json`.
41
- */
42
- export function previewResourceId(analysisId, previewId) {
43
- return `previews/${analysisId}/${previewId}`;
44
- }
45
- /**
46
- * Build a content-token preview URL for an iframe src.
47
- *
48
- * Produces `{contentBaseUrl}/{res}/{previewPath}?t={token}` where `res` is
49
- * the output of `previewResourceId(analysisId, previewId)`. The query parameter
50
- * name is `t` (matching Caddy's `from_query t` directive). The token is
51
- * URL-encoded.
52
- *
53
- * @param contentBaseUrl - Content server base URL (trailing slash tolerated)
54
- * @param analysisId - The analysis ID (authorization boundary)
55
- * @param previewId - The preview ID (groups all versions)
56
- * @param previewPath - Version-relative path (e.g., "v1/index.html"). Leading slash tolerated.
57
- * @param token - Short-lived content token minted by the storage backend (required)
58
- */
59
- export function buildPreviewUrl(contentBaseUrl, analysisId, previewId, previewPath, token) {
60
- const base = contentBaseUrl.replace(/\/+$/, "");
61
- const path = previewPath.replace(/^\/+/, "");
62
- const res = previewResourceId(analysisId, previewId);
63
- return `${base}/${res}/${path}?t=${encodeURIComponent(token)}`;
64
- }
65
24
  /**
66
25
  * Canonical `res` claim formula for a report-session page. Returns the literal string
67
26
  * `report-sessions/{analysisId}/{threadId}` with no leading or trailing slash.