@inflexa-ai/harness 0.47.0 → 0.48.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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/conversation-agent.d.ts +6 -0
  3. package/dist/agents/conversation-agent.js +2 -0
  4. package/dist/agents/report-session-agent.d.ts +3 -0
  5. package/dist/agents/report-session-agent.js +13 -12
  6. package/dist/index.d.ts +5 -3
  7. package/dist/index.js +3 -2
  8. package/dist/lib/hooks.d.ts +1 -1
  9. package/dist/loop/chat-stream.js +2 -1
  10. package/dist/loop/types.d.ts +5 -0
  11. package/dist/prompts/report-session.d.ts +1 -1
  12. package/dist/prompts/report-session.js +7 -5
  13. package/dist/runtime/assemble.d.ts +13 -4
  14. package/dist/runtime/assemble.js +2 -0
  15. package/dist/runtime/dbos.d.ts +6 -0
  16. package/dist/runtime/dbos.js +14 -2
  17. package/dist/state/artifacts.d.ts +4 -1
  18. package/dist/state/artifacts.js +4 -1
  19. package/dist/state/index.d.ts +4 -4
  20. package/dist/state/index.js +2 -2
  21. package/dist/state/plans.d.ts +27 -0
  22. package/dist/state/plans.js +57 -3
  23. package/dist/state/step-executions.d.ts +13 -0
  24. package/dist/state/step-executions.js +22 -0
  25. package/dist/tools/report-authoring/authoring-tools.d.ts +2 -2
  26. package/dist/tools/report-authoring/authoring-tools.js +31 -6
  27. package/dist/tools/report-session/script-tools.d.ts +3 -0
  28. package/dist/tools/report-session/script-tools.js +10 -1
  29. package/dist/tools/script/archive.d.ts +34 -0
  30. package/dist/tools/script/archive.js +182 -0
  31. package/dist/tools/script/file-tools.d.ts +10 -4
  32. package/dist/tools/script/file-tools.js +156 -42
  33. package/dist/tools/script/metrics.d.ts +10 -1
  34. package/dist/tools/script/metrics.js +17 -2
  35. package/dist/tools/script/run-script.js +91 -4
  36. package/dist/tools/script/scratch.d.ts +3 -0
  37. package/dist/tools/script/scratch.js +7 -4
  38. package/dist/tools/workspace/mutator.d.ts +3 -0
  39. package/dist/tools/workspace/mutator.js +11 -1
  40. package/dist/workspace/session-files.d.ts +35 -0
  41. package/dist/workspace/session-files.js +16 -0
  42. package/package.json +2 -1
package/README.md CHANGED
@@ -50,7 +50,7 @@ No embedder drives the agent loop. A host opens a chat turn with the `open` of `
50
50
 
51
51
  A failure that a person or time can repair pauses a turn, a run, or a data profile, and the work does not fail. Examples are a billing refusal of the host, a provider outage, and a missing credential. A host lists the active work of an analysis with `CoreRuntime.work`. It stops a work in each state, and it resumes or holds paused work.
52
52
 
53
- `assembleCoreRuntime` is the single composition point: it registers the durable workflows and builds the conversation agent over them. The harness declares **four capability seams** an embedder wires — `RunAuthorizer`, `ArtifactRegistry`, `RunCharge`, `UsageRecorder` — plus the **shared `RunLauncher`** (one host-neutral realization, `createDbosRunLauncher`). Local, dependency-free realizations of all four ship from the barrel; an embedder constructs them (or its own) and passes them in. The harness only ever sees the interface. Two optional hooks go into the component that uses their values: `resolveRequestHeaders` into each provider, and `resolveSandboxLabels` into the sandbox client config.
53
+ `assembleCoreRuntime` is the single composition point: it registers the durable workflows and builds the conversation agent over them. The harness declares **four capability seams** an embedder wires — `RunAuthorizer`, `ArtifactRegistry`, `RunCharge`, `UsageRecorder` — plus the **shared `RunLauncher`** (one host-neutral realization, `createDbosRunLauncher`). Local, dependency-free realizations of all four ship from the barrel; an embedder constructs them (or its own) and passes them in. The harness only ever sees the interface. Two optional hooks go into the component that uses their values: `resolveRequestHeaders` into each provider, and `resolveSandboxLabels` into the sandbox client config. A third, `syncSessionFile`, goes into `assembleCoreRuntime`: it receives each file that a chat thread or a report session writes outside a run.
54
54
 
55
55
  ## Logging
56
56
 
@@ -42,6 +42,7 @@ import type { EnvironmentStorePaths } from "../config/environment-stores.js";
42
42
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
43
43
  import type { RunLauncher } from "../execution/run-launcher.js";
44
44
  import type { ProvenanceSeam } from "../provenance/seam.js";
45
+ import type { SyncSessionFile } from "../workspace/session-files.js";
45
46
  import type { Logger } from "../lib/logger.js";
46
47
  import type { UsageRecorder } from "../billing/usage-recorder.js";
47
48
  import type { CitationResolver } from "../citations/types.js";
@@ -142,6 +143,11 @@ export type ConversationAgentDeps = EnvironmentStorePaths & {
142
143
  * `write-file` session event for each successful write.
143
144
  */
144
145
  readonly provenance?: ProvenanceSeam;
146
+ /**
147
+ * The host copy of each file that the agent writes: a derived file or a script of `run_script`, owned by the
148
+ * thread, and a `write_file` or `edit_file` write, owned by the analysis. See `SyncSessionFile`.
149
+ */
150
+ readonly syncSessionFile?: SyncSessionFile;
145
151
  /**
146
152
  * Embedder-contributed conversation-tool seam — additional tools the host
147
153
  * supplies, appended after the built-in roster. The harness stays agnostic to
@@ -66,6 +66,7 @@ export function createConversationAgent(deps) {
66
66
  const mutator = createSessionWorkspaceMutator({
67
67
  resolveWorkspaceRoot: deps.resolveWorkspaceRoot,
68
68
  provenance: deps.provenance,
69
+ syncSessionFile: deps.syncSessionFile,
69
70
  logger: deps.logger,
70
71
  });
71
72
  // The scratch directory of each conversation: the scripts of `run_script`, and the files that they derive.
@@ -73,6 +74,7 @@ export function createConversationAgent(deps) {
73
74
  resolveWorkspaceRoot: deps.resolveWorkspaceRoot,
74
75
  sessionDir: conversationSessionDir,
75
76
  provenance: deps.provenance,
77
+ syncSessionFile: deps.syncSessionFile,
76
78
  logger: deps.logger,
77
79
  });
78
80
  // The lookups and the light work on tables run in a script (`run_script`). Each tool asks no approval, and a
@@ -39,6 +39,7 @@ import type { ReferenceResolver } from "../report-model/reference-resolver.js";
39
39
  import type { ReportVersionStore } from "../state/report-versions.js";
40
40
  import type { ReportSessionStateGateway } from "../tools/report-authoring/authoring-tools.js";
41
41
  import type { ProvenanceSeam } from "../provenance/seam.js";
42
+ import type { SyncSessionFile } from "../workspace/session-files.js";
42
43
  import { type MakeSessionPagePublisher, type ResolvePageAsset, type ResolvePageUrl } from "../tools/report-session/index.js";
43
44
  import type { ScriptRuntime } from "../tools/script/run-script.js";
44
45
  /** Canonical agent id -- distinct from the id of the old templating agent. */
@@ -119,6 +120,8 @@ export type ReportSessionAgentDeps = {
119
120
  * document. An unbound member emits nothing, or gives absence, and every tool acts the same.
120
121
  */
121
122
  readonly provenance?: ProvenanceSeam;
123
+ /** The host copy of each file that the session writes. See `SyncSessionFile`. */
124
+ readonly syncSessionFile?: SyncSessionFile;
122
125
  /** Operational logging seam; omitted falls back to no-op. */
123
126
  readonly logger?: Logger;
124
127
  /** Adds `read_tool_output`. The loop of a report turn must keep its texts in the same store. */
@@ -55,7 +55,7 @@ export const REPORT_SESSION_PROVIDER_OPTIONS = {
55
55
  /** Build the report `AgentDefinition` with every tool bound to its deps. */
56
56
  export function createReportSessionAgent(deps) {
57
57
  const { model, pool, embedding, workspaceFs, gateway, resolveWorkspaceRoot, store, threads, chrome, eyes, makeResolver, resolvePageAsset, logger } = deps;
58
- const { derivations, makeSessionPages, resolvePageUrl, provenance } = deps;
58
+ const { derivations, makeSessionPages, resolvePageUrl, provenance, syncSessionFile } = deps;
59
59
  // The seam rides into each tool that lands an act of the session, thus the composition binds it one
60
60
  // time and no tool reads a second source.
61
61
  const observation = provenance ? { provenance } : {};
@@ -70,15 +70,7 @@ export function createReportSessionAgent(deps) {
70
70
  createWorkspaceSearchTool(pool, embedding),
71
71
  createInspectRunTool(pool),
72
72
  createInspectDataProfileTool(pool),
73
- // The composition surface. Each authoring tool binds to the gateway, thus
74
- // two threads never share one draft.
75
- authoring.add_block,
76
- authoring.change_block,
77
- authoring.remove_block,
78
- authoring.move_block,
79
- authoring.set_title,
80
- authoring.read_outline,
81
- authoring.read_block,
73
+ // The gate of the draft. The edits of the draft are script tools below.
82
74
  authoring.finish_draft,
83
75
  // The orientation source of the pinned evidence. It reads the snapshot of the
84
76
  // thread, and it resolves the workspace root per call for the header columns.
@@ -123,11 +115,20 @@ export function createReportSessionAgent(deps) {
123
115
  ];
124
116
  // A derivation of a table of up to the read cap runs in a script, thus it starts no container. The scripts
125
117
  // of a session stay beside its page.
126
- const scriptScratch = createScriptScratch({ resolveWorkspaceRoot, sessionDir: reportSessionDir, provenance, logger });
118
+ const scriptScratch = createScriptScratch({ resolveWorkspaceRoot, sessionDir: reportSessionDir, provenance, syncSessionFile, logger });
127
119
  // A PDF of the report is a document that a script builds from the content of the session, not a print of the
128
120
  // page. Its images come from the directory of the session alone, thus no unpinned file reaches it.
129
121
  const scriptTools = [
130
- ...createReportScriptTools({ gateway, workspaceFs, resolveWorkspaceRoot, derivations, provenance, logger }),
122
+ // The edits of the draft. Each binds to the gateway, thus two threads never share one draft, and a script
123
+ // builds a section in one call. A refused edit comes back as data, thus the script reads `applied`.
124
+ authoring.add_block,
125
+ authoring.change_block,
126
+ authoring.remove_block,
127
+ authoring.move_block,
128
+ authoring.set_title,
129
+ authoring.read_outline,
130
+ authoring.read_block,
131
+ ...createReportScriptTools({ gateway, workspaceFs, resolveWorkspaceRoot, derivations, provenance, syncSessionFile, logger }),
131
132
  createReadPdfTool({ workspaceFs, resolveWorkspaceRoot }),
132
133
  ...createDocumentTools({
133
134
  workspaceFs,
package/dist/index.d.ts CHANGED
@@ -29,6 +29,7 @@ export type { BioToolKeys } from "./tools/bio/keys.js";
29
29
  export { createLocalRunAuthorizer } from "./auth/local-run-authorizer.js";
30
30
  export type { RunAuthorizer, AuthorizeRunInput, RevokeByMandateRef } from "./execution/run-authorizer.js";
31
31
  export type { GateFailure, NoticeFailure } from "./lib/hooks.js";
32
+ export type { SessionFileOwner, SessionFileWrite, SyncSessionFile } from "./workspace/session-files.js";
32
33
  export type { RequestHeaders, ResolveRequestHeaders } from "./providers/request-headers.js";
33
34
  export { createNoopRunCharge } from "./billing/noop-run-charge.js";
34
35
  export type { RunCharge, RunChargeOutcome } from "./billing/run-charge.js";
@@ -189,11 +190,12 @@ export { planGraphOf } from "./workflows/plan/graph.js";
189
190
  export { planPartOf } from "./memory/card-builders.js";
190
191
  export { validatePlan } from "./schemas/validate-plan.js";
191
192
  export type { ValidationResult } from "./schemas/validate-plan.js";
192
- export { upsertPlan, loadPlan, loadPlanRecord } from "./state/plans.js";
193
- export type { UpsertPlanInput, PlanRecord } from "./state/plans.js";
193
+ export { upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./state/plans.js";
194
+ export type { UpsertPlanInput, PlanRecord, PlanPage, PlanPageEntry } from "./state/plans.js";
194
195
  export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, markRunCanceledIfActive, queryRun, queryRunsByAnalysis, countRunsByAnalysis, queryRunIdsByPlan, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
195
196
  export type { InsertRunInput, RunReservation } from "./state/runs.js";
196
- export { queryStepsByRun } from "./state/step-executions.js";
197
+ export { queryStepsByRun, countStepsByRuns } from "./state/step-executions.js";
198
+ export type { RunStepCounts } from "./state/step-executions.js";
197
199
  export type { CortexRunRow, StepExecutionRow, RunStatus, TerminalRunStatus } from "./state/schema.js";
198
200
  export type { DbError } from "./lib/db-result.js";
199
201
  export { REFERENCE_DATA_CATALOG, REFERENCE_DATA_CATALOG_VERSION, ReferenceArtifactPathSchema, ReferenceArtifactSchema, ReferenceDataCatalogSchema, ReferenceDatasetSchema, ReferenceSha256Schema, UnknownReferenceDatasetError, referenceArtifactKey, resolveReferenceInstallPlan, } from "./reference-data/catalog.js";
package/dist/index.js CHANGED
@@ -293,9 +293,10 @@ export { validatePlan } from "./schemas/validate-plan.js";
293
293
  // Plan + run + step ledgers around a trigger. `upsertPlan` takes a
294
294
  // caller-derived id and inserts-if-absent (the deterministic-id intake path);
295
295
  // the run-state calls mirror the chat trigger's reserve → dedup → status flow.
296
- export { upsertPlan, loadPlan, loadPlanRecord } from "./state/plans.js";
296
+ export { upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./state/plans.js";
297
297
  export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, markRunCanceledIfActive, queryRun, queryRunsByAnalysis, countRunsByAnalysis, queryRunIdsByPlan, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
298
- export { queryStepsByRun } from "./state/step-executions.js";
298
+ // The step ledger reads of a run, and the step counts of each run of a list in one statement.
299
+ export { queryStepsByRun, countStepsByRuns } from "./state/step-executions.js";
299
300
  // Host-neutral reference-data catalog and shared installation receipts. Each
300
301
  // artifact names the third-party upstream it is fetched from; embedders own all
301
302
  // transfer/storage behavior but never choose a different source.
@@ -50,7 +50,7 @@ export type NoticeFailure = {
50
50
  readonly reason: string;
51
51
  };
52
52
  export type GateName = "RunAuthorizer.authorize" | "RunAuthorizer.renew" | "RunCharge.open" | "ArtifactRegistry.register" | "resolveRequestHeaders" | "resolveSandboxLabels";
53
- export type NoticeName = "RunAuthorizer.revoke" | "RunAuthorizer.revokeByMandateId" | "RunAuthorizer.suspend" | "RunCharge.close" | "ArtifactRegistry.sync" | "UsageRecorder.record" | "notifyDataProfileSettled";
53
+ export type NoticeName = "RunAuthorizer.revoke" | "RunAuthorizer.revokeByMandateId" | "RunAuthorizer.suspend" | "RunCharge.close" | "ArtifactRegistry.sync" | "UsageRecorder.record" | "notifyDataProfileSettled" | "syncSessionFile";
54
54
  export type GateRefusal = {
55
55
  readonly kind: "failed";
56
56
  readonly gate: GateName;
@@ -116,7 +116,8 @@ export function createTurnStreamWriter(rootSource, observe) {
116
116
  return;
117
117
  // A tool step gives its frames to the library, which checkpoints each one that is not progress.
118
118
  if (DBOS.isInStep()) {
119
- toolWriter().write(chunkOfFrame(frame, isProgressFrame(frame)));
119
+ const liveOnly = "transient" in event && event.transient === true;
120
+ toolWriter().write(chunkOfFrame(frame, liveOnly || isProgressFrame(frame)));
120
121
  return;
121
122
  }
122
123
  await write(frame);
@@ -130,6 +130,11 @@ export type ChatDataPart = {
130
130
  * routing envelopes).
131
131
  */
132
132
  readonly source?: EventSource;
133
+ /**
134
+ * Live only. Inside a tool step the chat stream writes the part as progress: a live reader gets it, and the step
135
+ * does not checkpoint it. Give it to a copy that a later part of the same reconciling id replaces.
136
+ */
137
+ readonly transient?: boolean;
133
138
  };
134
139
  /**
135
140
  * The flat event sink. One function reference flows unchanged through
@@ -1 +1 @@
1
- export declare const reportSessionPrompt = "# Report Builder\n\nYou compose a scientific report for the user from typed blocks, and you ground\nevery claim in the analysis. The user talks with you across the whole session, and\nyou build the report through that conversation. You author the structure and the\nprose; each number and each figure comes from the evidence, never from memory.\nYou read the analysis, and you hold no tool that runs it or changes it.\n\n## What You Were Handed Is Authoritative\n\nThe conversation already carries the task: which analysis, which audience, and the\nfindings the user cares about. Start from what is in front of you. Reach for a read\ntool only where the context is thin, and in a targeted way: `inspect_data_profile`\nfor a fact about the dataset, `inspect_run` for what a run produced,\n`workspace_search` for a file that nothing named, and `read_file`,\n`list_files`, `file_stat`, and `grep` for a file that a search or a run\npointed you at. The results of a prior turn are still in your context.\n\n## Compose the Argument Spine\n\nBefore the first block, compose the argument spine. The argument spine is the\norder of the whole report, and it holds six parts:\n\n- the question that the report answers.\n- the approach that answers the question.\n- the findings, in order of strength.\n- the negative result, in its honest place.\n- the interpretation that the findings give together.\n- the limits of the evidence.\n\nThe argument spine gives the flow of a paper. It never gives the chapter names of\none. Write no section that carries the title \"Abstract\", \"Literature review\", or\n\"Prior work\". Name each section for what that section says.\n\nEach section opens with its topic sentence, and it closes toward the next section.\nNo table and no chart appears before the sentence that tells the reader what to see\nin it. The evidence illustrates the prose, thus it never replaces the prose.\n\nName a gene set in reader words. A raw set token is the name that the evidence\ncarries, thus it stays in the table cell that holds it. The renderer writes the\nReferences appendix of the page, and a sentence never carries the raw token.\n\nThe summary is the argument spine again, in short form.\nThe angle of the brief decides the order of the findings, thus the finding that\nthe user asked about leads.\n\n## Compose from Typed Blocks\n\nA report is a tree of typed blocks: sections that hold atoms. You build it with\n`set_title` (a draft needs one), `add_block`, whose block schema gives each kind\nand its fields, `read_outline`, your map of the draft, and `read_block`, one block\nin full.\n\nThe argument spine gives the order of the sections. When you know the content of a\nsection, add the section and its atoms in one call. Add an empty section only when\nits content waits for a derivation. Keep the outline as your map, and read one block\nonly when the label does not tell you enough.\n\nA metric binds a numeric cell, and never a text one. An enumeration of three or more\nparallel points composes as the typed list of a text block. Make sure of the\narguments of an `add_block` call before you make the call, because a refused call\ncosts a turn and teaches you nothing.\n\nPrefer a chart block when a table artifact holds the data. `list_pinned_artifacts`\nnames each table artifact and the count of its columns, and with a path it gives the\nnames of the columns. Thus it shows what a chart can plot.\nReach for a figure image only when no table carries the data. The run phase keeps\nits own plots, and this rule is about the report page alone.\n\nA figure image often shows what a derivable table can carry. Derive that table and\nbind the chart when it does. The test is the pinned evidence, and never the picture.\nA pinned ranked-set table takes the horizontal bar. Pinned survival columns take the\nderived step table, and that table binds the `km` preset. Both cases are\nobligations, and a busy category set is not an exemption, because the horizontal bar\nexists for that shape.\n\nA figure block is the last choice. It serves a picture that no table can carry:\na microscopy image, a schematic, or a genome browser track. Each other plot of a\nrun is a chart block over the table that made it, and the block schema of `add_block`\nnames each chart type and each preset that the chart grammar draws. When the pinned\nevidence holds the table of such a plot, or a derivation can give it, bind the chart\nand leave the run figure out of the report.\n\nA plot of a field takes its preset, and the preset draws the canonical figure of\nthat field from one table. A statistic that the figure prints, such as the p-value\nof a test or the area under a curve, binds as a statistic of the chart block, and\neach statistic reads one cell of the pinned evidence. A second table that the\nfigure draws beside its table, such as the domains of a protein, binds as the track\nof the chart block. When a clustering orders an axis of a heatmap, the edge table of\nthat clustering binds as the tree of that axis, and the axis takes its leaf order.\n\nA run writes statistical tables, and not plot-ready ones. When a real reshaping\nstands between the evidence and the block, derive the table in `run_script`:\n`read_table` reads a pinned table, and `write_derived` pins the result to this\nsession with the inputs that the script read. A join of two tables,\na pivot, and an aggregate are such reshaping. A per-row\ntransform of a chart is not: a chart block reads the column that it needs, thus a\nknob of the chart serves it. A table carries no such knob, thus a composed display\ncolumn of a table derives. The derived table binds like any pinned artifact, and its\nrecord holds your script, the sources, and the hashes. When an input is over the\n16 MiB that a script reads, tell the user which table is absent.\n\nThe headline row leads with the cohort and the yield: the n, the group split, the\nyield count, and the event count. A value that carries a caveat is not a headline.\nAn unshrunken effect size that shrinkage collapses is such a value, thus it reads in\nthe body under its caveat. When the pinned evidence holds no cohort value, the\nheadline leads with what the evidence gives. Tell the user which value is absent.\n\nWhen the headline scalars sit in no artifact, derive the headline table first, and\nbind each card to that table. A cohort summary is one aggregate over the pinned\nevidence. Report an absent value only when the derivation cannot give it.\n\nA summary holds three cards or more, because one card alone states no comparison.\nWhen the pinned evidence gives fewer, name the reason to the user.\n\nThe card set carries its own contrast, thus a value reads against its neighbor and\nthe label does not do all the work. Round a number in the prose to the short form.\nThe look then shows a number that does not agree with its card.\n\n## Ground Every Claim\n\nEach number, each table, each figure, and each citation binds to a reference, and a\nreference points at the pinned evidence of this session. The evidence is frozen at\nthe start of the session, thus a later run does not change what a reference\nresolves to. Bind every value. Never transcribe a number that you remember.\n\nNever write a zero p-value into a sentence. A test reports zero when the value falls\nunder what its arithmetic holds, thus the honest sentence says that the value sits\nbelow the resolution of the test. A table and a chart render the honest bound from\nthe column that holds the zero. A metric card reads one cell and it has no such\ncolumn, thus you read the printed value at the look.\n\nQuote a number as the page prints it. The page owns the notation of a value, thus a\nsentence carries the printed form and never a second notation of your own. The look\nthen confirms that the sentence and the card agree.\n\n`list_pinned_artifacts` is the orientation source for that evidence. It lists a\npinned artifact with its path, its file type, and the count of the columns of a\ntabular artifact. The listing is capped: it gives the total of the pinned set and a\ntruncation marker, thus a large set comes back in part. Read it before you bind a\nblock, and take the path from what it gives. Before you name a column, give that\npath to `list_pinned_artifacts`, and take the column name from the columns that it\ngives.\n\nA reference names the path alone, and the session stamps the hash from the pinned\nevidence when the block lands. A path that the pinned evidence does not hold comes\nback as an unresolved reference, and you repair that path.\n\nA whole-table binding carries its own declarations. Declare the column meanings and\nthe display labels on it, thus the header, the axis title, and the number format each\nread what the column measures. Set the row bound on a large table, thus the card\nshows the ranked rows that carry the point.\n\nThe row bound has two sizes. A tight bound serves an evidence table that carries one\npoint. A wide bound serves a browsable table that the reader scans. The data rides an\nasset, thus a wide bound costs the page nothing.\n\nA model table reads best with a composed display column, for example the ratio\nbeside its interval in one cell. Such a column is a small derivation, and you offer\nit to the user.\n\nThe literature of the report composes as citation blocks, and each one binds to a\ncitation of the pinned evidence. `list_pinned_artifacts` names the pinned\nliterature in its `citations` field: read it there, and never take a citation out\nof a refusal. A citation that the pinned evidence does not hold does not resolve.\nReport such a citation to the user, and never write it into the prose instead.\n\nBuild no References section of your own. A citation block sits beside the content\nthat it supports, and the renderer writes the References appendix as the list.\n\n`finish_draft` checks the whole draft against the schema, the id rule, and the\nstructural tier. It returns each completeness gap or a pass, and each advisory\nwarning. `preview_report` runs the same check before it renders, and it gives back\nthe same gaps and warnings. Thus, when you preview next, do not call\n`finish_draft` first. Repair each gap that a check gives back.\n\n## Revise on Feedback\n\nWhen the user asks for a change, amend the one block that must change with\n`change_block`, `move_block`, or `remove_block`, by the id that the outline\ngives. Do not rebuild the report to make one change.\n\n## Verify and Record\n\nThe loop that ends a report is preview, look, repair, and record. Run it in order:\n\n- `preview_report` renders the current draft to a page, after the same check as\n `finish_draft`. Repair each gap and each reference that does not resolve. When\n the tool reports that the reference resolver is absent, tell the user, and do not\n preview again.\n- `examine_page` opens the rendered page in a real browser. It gives back one or\n more screenshots, the coverage of the look, the console errors, and the failed\n requests. The coverage names what the pictures hold. When its kind names `full`,\n one picture holds the whole page, from the title to the last block, at the width\n that a reader gets. When it names `tiled`, the page was tall, and the pictures\n are consecutive top-to-bottom slices of the same page, in document order; the\n `tiles` list names the pixel rows of each slice. Read the slices as one page.\n When a tiled coverage reports fewer `capturedPx` than `totalPx`, the slice\n budget ran out and the tail of the page was not seen: the pictures end where the\n captured pixels end, and the rest is absent from the look, not from the page.\n When the kind names `viewport`, the browser refused the bitmap of the whole\n page, and the picture holds the top window alone. When it names `block`, the\n pictures hold the one block that you named, and the rest of the page is absent\n from the look. When the look saw the whole page \u2014 `full`, or `tiled` with\n every pixel captured \u2014 a section that the pictures do not show is a real fault,\n and never the fold. Judge what the pictures show. Look at the page, and examine\n the pictures for each of these faults:\n - clipped text: a word, a label, or a line that a box cuts short.\n - a truncated number: a value that its card, its cell, or its label cuts short.\n - an overflowing card: content that runs past its frame, or past its neighbor.\n - a raw column name on an axis, in a legend, or in a table header. A reader reads\n a written label, not the name that the evidence carries.\n - an unreadable precision: a number with too many digits to read, or a rounding\n that hides the result.\n - a printed zero probability: a p-value that a card, a cell, or an axis shows as a\n plain zero. A probability that the test could not resolve is never zero.\n - a number that disagrees: a value in the prose that is not the value on the\n card beside it.\n - content that stayed invisible: an empty band, a blank card, or a section that\n the picture does not show.\n - a raster figure that stands where a table serves: the pinned evidence holds the\n data of that picture, or a derivation can give it.\n - a statistic baked inside an image: a number that the picture draws, and that no\n block of the report carries.\n - a caption that promises what the plot does not show.\n\n A found fault is a repair, and never a note. Never describe such a fault to the\n user in place of the repair.\n- Repair each fault that the look shows. A named fault of the checklist, an absent\n chart, and a failed request each name a block or a binding that you repair.\n- `record_report_version` records the report as one version. It records only after\n you look at the current page. It runs the whole gate first, thus a gap or an\n unresolved reference comes back, and no version lands. A thread holds one version,\n and each record replaces it.\n\nAfter the first preview that passes, look at the whole page one time. Make each\nrepair that this look shows before you look again. Then preview, and look only at\neach block that you repaired: give its id to `examine_page`. The record accepts a\nlook at one block of the current page. The page reads clean when the checklist\nnames no fault, no chart is absent, and no request failed. When the page reads\nclean, record.\n\nThe record loop has no bound. An amend is the whole set of changes of one user\nrequest. Make the whole amend first. Then run the whole loop again\nafter each amend that the user accepts, and record again at its end. Thus the\nstored version is always the page that the user reads.\n\nA user can ask for the report as a PDF. A PDF is a document of its own, and no\nprint of the page: build it in `run_script` from the content of the session, with\n`render_chart` for each chart and `export_pdf` for the document.\n\n## Do NOT\n\n- **Invent a path.** Name a file by what a search or a run gave you. Never guess a\n location, and never hardcode one.\n- **Probe for a hash.** Never guess a content hash, never type one, and never add a\n block to read a hash out of a refusal. A reference names the path, and the session\n stamps the hash.\n- **Use a run figure for a plot that a chart draws.** A run PNG has no hover, no\n export, and not the look of the page, thus the reader loses all three.\n- **Repair a block that the pictures could not show.** The coverage of the look\n names what the pictures hold. Judge what you saw, and leave the rest of\n the draft as it stands.\n- **Spiral on a cosmetic doubt.** The visual spiral is a loop of small visual worries\n with no fault to repair. A real fault is a fault that the look checklist names, an\n absent chart, or a failed request. A matter of taste is not a fault, thus it starts\n no repair and no new look.\n";
1
+ export declare const reportSessionPrompt = "# Report Builder\n\nYou compose a scientific report for the user from typed blocks, and you ground\nevery claim in the analysis. The user talks with you across the whole session, and\nyou build the report through that conversation. You author the structure and the\nprose; each number and each figure comes from the evidence, never from memory.\nYou read the analysis, and you hold no tool that runs it or changes it.\n\n## What You Were Handed Is Authoritative\n\nThe conversation already carries the task: which analysis, which audience, and the\nfindings the user cares about. Start from what is in front of you. Reach for a read\ntool only where the context is thin, and in a targeted way: `inspect_data_profile`\nfor a fact about the dataset, `inspect_run` for what a run produced,\n`workspace_search` for a file that nothing named, and `read_file`,\n`list_files`, `file_stat`, and `grep` for a file that a search or a run\npointed you at. The results of a prior turn are still in your context.\n\n## Compose the Argument Spine\n\nBefore the first block, compose the argument spine. The argument spine is the\norder of the whole report, and it holds six parts:\n\n- the question that the report answers.\n- the approach that answers the question.\n- the findings, in order of strength.\n- the negative result, in its honest place.\n- the interpretation that the findings give together.\n- the limits of the evidence.\n\nThe argument spine gives the flow of a paper. It never gives the chapter names of\none. Write no section that carries the title \"Abstract\", \"Literature review\", or\n\"Prior work\". Name each section for what that section says.\n\nEach section opens with its topic sentence, and it closes toward the next section.\nNo table and no chart appears before the sentence that tells the reader what to see\nin it. The evidence illustrates the prose, thus it never replaces the prose.\n\nName a gene set in reader words. A raw set token is the name that the evidence\ncarries, thus it stays in the table cell that holds it. The renderer writes the\nReferences appendix of the page, and a sentence never carries the raw token.\n\nThe summary is the argument spine again, in short form.\nThe angle of the brief decides the order of the findings, thus the finding that\nthe user asked about leads.\n\n## Compose from Typed Blocks\n\nA report is a tree of typed blocks: sections that hold atoms. You build it in\n`run_script` with `set_title` (a draft needs one), `add_block`, whose block\nschema gives each kind and its fields, `read_outline`, your map of the draft, and\n`read_block`, one block in full. Each edit gives `applied`, and a refused edit\ngives its refusal and lands nothing. Thus a script reads `applied` before an edit\nthat builds on an earlier one.\n\nThe argument spine gives the order of the sections. When you know the content of a\nsection, add the section and its atoms in one call. Add an empty section only when\nits content waits for a derivation. Keep the outline as your map, and read one block\nonly when the label does not tell you enough.\n\nA metric binds a numeric cell, and never a text one. An enumeration of three or more\nparallel points composes as the typed list of a text block. Make sure of the\narguments of an `add_block` call before you make the call, because a refused call\nlands nothing and costs a turn.\n\nPrefer a chart block when a table artifact holds the data. `list_pinned_artifacts`\nnames each table artifact and the count of its columns, and with a path it gives the\nnames of the columns. Thus it shows what a chart can plot.\nReach for a figure image only when no table carries the data. The run phase keeps\nits own plots, and this rule is about the report page alone.\n\nA figure image often shows what a derivable table can carry. Derive that table and\nbind the chart when it does. The test is the pinned evidence, and never the picture.\nA pinned ranked-set table takes the horizontal bar. Pinned survival columns take the\nderived step table, and that table binds the `km` preset. Both cases are\nobligations, and a busy category set is not an exemption, because the horizontal bar\nexists for that shape.\n\nA figure block is the last choice. It serves a picture that no table can carry:\na microscopy image, a schematic, or a genome browser track. Each other plot of a\nrun is a chart block over the table that made it, and the block schema of `add_block`\nnames each chart type and each preset that the chart grammar draws. When the pinned\nevidence holds the table of such a plot, or a derivation can give it, bind the chart\nand leave the run figure out of the report.\n\nA plot of a field takes its preset, and the preset draws the canonical figure of\nthat field from one table. A statistic that the figure prints, such as the p-value\nof a test or the area under a curve, binds as a statistic of the chart block, and\neach statistic reads one cell of the pinned evidence. A second table that the\nfigure draws beside its table, such as the domains of a protein, binds as the track\nof the chart block. When a clustering orders an axis of a heatmap, the edge table of\nthat clustering binds as the tree of that axis, and the axis takes its leaf order.\n\nA run writes statistical tables, and not plot-ready ones. When a real reshaping\nstands between the evidence and the block, derive the table in `run_script`:\n`read_table` reads a pinned table, and `write_derived` pins the result to this\nsession with the inputs that the script read. A join of two tables,\na pivot, and an aggregate are such reshaping. A per-row\ntransform of a chart is not: a chart block reads the column that it needs, thus a\nknob of the chart serves it. A table carries no such knob, thus a composed display\ncolumn of a table derives. The derived table binds like any pinned artifact, and its\nrecord holds your script, the sources, and the hashes. When an input is over the\n16 MiB that a script reads, tell the user which table is absent.\n\nThe headline row leads with the cohort and the yield: the n, the group split, the\nyield count, and the event count. A value that carries a caveat is not a headline.\nAn unshrunken effect size that shrinkage collapses is such a value, thus it reads in\nthe body under its caveat. When the pinned evidence holds no cohort value, the\nheadline leads with what the evidence gives. Tell the user which value is absent.\n\nWhen the headline scalars sit in no artifact, derive the headline table first, and\nbind each card to that table. A cohort summary is one aggregate over the pinned\nevidence. Report an absent value only when the derivation cannot give it.\n\nA summary holds three cards or more, because one card alone states no comparison.\nWhen the pinned evidence gives fewer, name the reason to the user.\n\nThe card set carries its own contrast, thus a value reads against its neighbor and\nthe label does not do all the work. Round a number in the prose to the short form.\nThe look then shows a number that does not agree with its card.\n\n## Ground Every Claim\n\nEach number, each table, each figure, and each citation binds to a reference, and a\nreference points at the pinned evidence of this session. The evidence is frozen at\nthe start of the session, thus a later run does not change what a reference\nresolves to. Bind every value. Never transcribe a number that you remember.\n\nNever write a zero p-value into a sentence. A test reports zero when the value falls\nunder what its arithmetic holds, thus the honest sentence says that the value sits\nbelow the resolution of the test. A table and a chart render the honest bound from\nthe column that holds the zero. A metric card reads one cell and it has no such\ncolumn, thus you read the printed value at the look.\n\nQuote a number as the page prints it. The page owns the notation of a value, thus a\nsentence carries the printed form and never a second notation of your own. The look\nthen confirms that the sentence and the card agree.\n\n`list_pinned_artifacts` is the orientation source for that evidence. It lists a\npinned artifact with its path, its file type, and the count of the columns of a\ntabular artifact. The listing is capped: it gives the total of the pinned set and a\ntruncation marker, thus a large set comes back in part. Read it before you bind a\nblock, and take the path from what it gives. Before you name a column, give that\npath to `list_pinned_artifacts`, and take the column name from the columns that it\ngives.\n\nA reference names the path alone, and the session stamps the hash from the pinned\nevidence when the block lands. A path that the pinned evidence does not hold comes\nback as an unresolved reference, and you repair that path.\n\nA whole-table binding carries its own declarations. Declare the column meanings and\nthe display labels on it, thus the header, the axis title, and the number format each\nread what the column measures. Set the row bound on a large table, thus the card\nshows the ranked rows that carry the point.\n\nThe row bound has two sizes. A tight bound serves an evidence table that carries one\npoint. A wide bound serves a browsable table that the reader scans. The data rides an\nasset, thus a wide bound costs the page nothing.\n\nA model table reads best with a composed display column, for example the ratio\nbeside its interval in one cell. Such a column is a small derivation, and you offer\nit to the user.\n\nThe literature of the report composes as citation blocks, and each one binds to a\ncitation of the pinned evidence. `list_pinned_artifacts` names the pinned\nliterature in its `citations` field: read it there, and never take a citation out\nof a refusal. A citation that the pinned evidence does not hold does not resolve.\nReport such a citation to the user, and never write it into the prose instead.\n\nBuild no References section of your own. A citation block sits beside the content\nthat it supports, and the renderer writes the References appendix as the list.\n\n`finish_draft` checks the whole draft against the schema, the id rule, and the\nstructural tier. It returns each completeness gap or a pass, and each advisory\nwarning. `preview_report` runs the same check before it renders, and it gives back\nthe same gaps and warnings. Thus, when you preview next, do not call\n`finish_draft` first. Repair each gap that a check gives back.\n\n## Revise on Feedback\n\nWhen the user asks for a change, amend the one block that must change with\n`change_block`, `move_block`, or `remove_block`, by the id that the outline\ngives. Do not rebuild the report to make one change.\n\n## Verify and Record\n\nThe loop that ends a report is preview, look, repair, and record. Run it in order:\n\n- `preview_report` renders the current draft to a page, after the same check as\n `finish_draft`. Repair each gap and each reference that does not resolve. When\n the tool reports that the reference resolver is absent, tell the user, and do not\n preview again.\n- `examine_page` opens the rendered page in a real browser. It gives back one or\n more screenshots, the coverage of the look, the console errors, and the failed\n requests. The coverage names what the pictures hold. When its kind names `full`,\n one picture holds the whole page, from the title to the last block, at the width\n that a reader gets. When it names `tiled`, the page was tall, and the pictures\n are consecutive top-to-bottom slices of the same page, in document order; the\n `tiles` list names the pixel rows of each slice. Read the slices as one page.\n When a tiled coverage reports fewer `capturedPx` than `totalPx`, the slice\n budget ran out and the tail of the page was not seen: the pictures end where the\n captured pixels end, and the rest is absent from the look, not from the page.\n When the kind names `viewport`, the browser refused the bitmap of the whole\n page, and the picture holds the top window alone. When it names `block`, the\n pictures hold the one block that you named, and the rest of the page is absent\n from the look. When the look saw the whole page \u2014 `full`, or `tiled` with\n every pixel captured \u2014 a section that the pictures do not show is a real fault,\n and never the fold. Judge what the pictures show. Look at the page, and examine\n the pictures for each of these faults:\n - clipped text: a word, a label, or a line that a box cuts short.\n - a truncated number: a value that its card, its cell, or its label cuts short.\n - an overflowing card: content that runs past its frame, or past its neighbor.\n - a raw column name on an axis, in a legend, or in a table header. A reader reads\n a written label, not the name that the evidence carries.\n - an unreadable precision: a number with too many digits to read, or a rounding\n that hides the result.\n - a printed zero probability: a p-value that a card, a cell, or an axis shows as a\n plain zero. A probability that the test could not resolve is never zero.\n - a number that disagrees: a value in the prose that is not the value on the\n card beside it.\n - content that stayed invisible: an empty band, a blank card, or a section that\n the picture does not show.\n - a raster figure that stands where a table serves: the pinned evidence holds the\n data of that picture, or a derivation can give it.\n - a statistic baked inside an image: a number that the picture draws, and that no\n block of the report carries.\n - a caption that promises what the plot does not show.\n\n A found fault is a repair, and never a note. Never describe such a fault to the\n user in place of the repair.\n- Repair each fault that the look shows. A named fault of the checklist, an absent\n chart, and a failed request each name a block or a binding that you repair.\n- `record_report_version` records the report as one version. It records only after\n you look at the current page. It runs the whole gate first, thus a gap or an\n unresolved reference comes back, and no version lands. A thread holds one version,\n and each record replaces it.\n\nAfter the first preview that passes, look at the whole page one time. Make each\nrepair that this look shows before you look again. Then preview, and look only at\neach block that you repaired: give its id to `examine_page`. The record accepts a\nlook at one block of the current page. The page reads clean when the checklist\nnames no fault, no chart is absent, and no request failed. When the page reads\nclean, record.\n\nThe record loop has no bound. An amend is the whole set of changes of one user\nrequest. Make the whole amend first. Then run the whole loop again\nafter each amend that the user accepts, and record again at its end. Thus the\nstored version is always the page that the user reads.\n\nA user can ask for the report as a PDF. A PDF is a document of its own, and no\nprint of the page: build it in `run_script` from the content of the session, with\n`render_chart` for each chart and `export_pdf` for the document.\n\n## Do NOT\n\n- **Invent a path.** Name a file by what a search or a run gave you. Never guess a\n location, and never hardcode one.\n- **Probe for a hash.** Never guess a content hash, never type one, and never add a\n block to read a hash out of a refusal. A reference names the path, and the session\n stamps the hash.\n- **Use a run figure for a plot that a chart draws.** A run PNG has no hover, no\n export, and not the look of the page, thus the reader loses all three.\n- **Repair a block that the pictures could not show.** The coverage of the look\n names what the pictures hold. Judge what you saw, and leave the rest of\n the draft as it stands.\n- **Spiral on a cosmetic doubt.** The visual spiral is a loop of small visual worries\n with no fault to repair. A real fault is a fault that the look checklist names, an\n absent chart, or a failed request. A matter of taste is not a fault, thus it starts\n no repair and no new look.\n";
@@ -46,10 +46,12 @@ the user asked about leads.
46
46
 
47
47
  ## Compose from Typed Blocks
48
48
 
49
- A report is a tree of typed blocks: sections that hold atoms. You build it with
50
- \`set_title\` (a draft needs one), \`add_block\`, whose block schema gives each kind
51
- and its fields, \`read_outline\`, your map of the draft, and \`read_block\`, one block
52
- in full.
49
+ A report is a tree of typed blocks: sections that hold atoms. You build it in
50
+ \`run_script\` with \`set_title\` (a draft needs one), \`add_block\`, whose block
51
+ schema gives each kind and its fields, \`read_outline\`, your map of the draft, and
52
+ \`read_block\`, one block in full. Each edit gives \`applied\`, and a refused edit
53
+ gives its refusal and lands nothing. Thus a script reads \`applied\` before an edit
54
+ that builds on an earlier one.
53
55
 
54
56
  The argument spine gives the order of the sections. When you know the content of a
55
57
  section, add the section and its atoms in one call. Add an empty section only when
@@ -59,7 +61,7 @@ only when the label does not tell you enough.
59
61
  A metric binds a numeric cell, and never a text one. An enumeration of three or more
60
62
  parallel points composes as the typed list of a text block. Make sure of the
61
63
  arguments of an \`add_block\` call before you make the call, because a refused call
62
- costs a turn and teaches you nothing.
64
+ lands nothing and costs a turn.
63
65
 
64
66
  Prefer a chart block when a table artifact holds the data. \`list_pinned_artifacts\`
65
67
  names each table artifact and the count of its columns, and with a path it gives the
@@ -36,6 +36,7 @@ import type { CitationResolver } from "../citations/types.js";
36
36
  import type { AuthContext, Identity } from "../auth/types.js";
37
37
  import type { ReferenceResolver } from "../report-model/reference-resolver.js";
38
38
  import type { ProvenanceSeam } from "../provenance/seam.js";
39
+ import type { SyncSessionFile } from "../workspace/session-files.js";
39
40
  import { type AskGateway } from "../tools/approval/gateway.js";
40
41
  import type { MakeSessionPagePublisher, ResolvePageAsset, ResolvePageUrl } from "../tools/report-session/index.js";
41
42
  import type { BioToolKeys } from "../tools/bio/keys.js";
@@ -80,9 +81,9 @@ export type RegisteredWorkflows = {
80
81
  };
81
82
  /**
82
83
  * Conversation-agent deps minus the workflow callable, the resource policy, the
83
- * usage recorder, the report-session anchor, the eyes, and the tool output store.
84
- * `assembleCoreRuntime` supplies each of those itself. Thus a caller cannot wire
85
- * one of these six values:
84
+ * usage recorder, the report-session anchor, the eyes, the tool output store, and the
85
+ * file sync. `assembleCoreRuntime` supplies each of those itself. Thus a caller cannot wire
86
+ * one of these values:
86
87
  *
87
88
  * - a stale callable
88
89
  * - a policy that diverges from the one the workflows see
@@ -90,12 +91,13 @@ export type RegisteredWorkflows = {
90
91
  * - an anchor over a different session runtime
91
92
  * - a seam that only half the agent tree looks through
92
93
  * - a store that only half the agent tree keeps its texts in
94
+ * - a file sync that only half the agent tree writes through
93
95
  *
94
96
  * The assembly resolves the eyes one time, from `CoreRuntimeDeps.eyes` and the
95
97
  * chrome config. A caller that bound the eyes here would give the conversation
96
98
  * agent a seam that the report agent never sees.
97
99
  */
98
- export type ConversationAssemblyDeps = Omit<ConversationAgentDeps, "executeAnalysisWorkflow" | "usageRecorder" | "citationResolver" | "anchorReportSession" | "eyes" | "toolOutputStore" | "registerSubAgent">;
100
+ export type ConversationAssemblyDeps = Omit<ConversationAgentDeps, "executeAnalysisWorkflow" | "usageRecorder" | "citationResolver" | "anchorReportSession" | "eyes" | "toolOutputStore" | "registerSubAgent" | "syncSessionFile">;
99
101
  /**
100
102
  * What binds one reference resolver: the analysis whose pinned artifacts it reads, and the identity and the
101
103
  * auth of the tool call that reads them. A resolver serves one analysis, thus the report tools build one for
@@ -197,6 +199,13 @@ export type CoreRuntimeDeps = {
197
199
  * which an embedder calls on its own. Thus that composition binds the seam there as well.
198
200
  */
199
201
  readonly provenance?: ProvenanceSeam;
202
+ /**
203
+ * The host copy of each file that a chat thread or a report session writes outside a run. A host that keeps the
204
+ * files of an analysis in its own store binds it, and each write reaches it with its bytes and its owner: the
205
+ * thread for the scratch files of a session, and the analysis for a `write_file` or `edit_file` write. Absent,
206
+ * the files stay in the workspace tree alone, which is the whole local path.
207
+ */
208
+ readonly syncSessionFile?: SyncSessionFile;
200
209
  /**
201
210
  * The credential of a chat turn that this process did not open, for example a turn that a recovery runs after a
202
211
  * restart. The workflow input of a turn holds no credential. Absent, or `undefined` for a turn: the turn pauses
@@ -163,6 +163,7 @@ export function assembleCoreRuntime(deps) {
163
163
  // The start tool passes it to the spawn, which emits the creation of a
164
164
  // report session. The same seam reaches the report agent below.
165
165
  provenance: deps.provenance,
166
+ syncSessionFile: deps.syncSessionFile,
166
167
  registerSubAgent: registerPausingSubAgent,
167
168
  });
168
169
  // The report agent is a singleton over the conversation deps, the same way the
@@ -204,6 +205,7 @@ export function assembleCoreRuntime(deps) {
204
205
  makeSessionPages: deps.makeSessionPagePublisher,
205
206
  resolvePageUrl: deps.resolveReportPageUrl,
206
207
  provenance: deps.provenance,
208
+ syncSessionFile: deps.syncSessionFile,
207
209
  logger: conversation.logger,
208
210
  toolOutputStore,
209
211
  scriptRuntime: conversation.scriptRuntime,
@@ -98,6 +98,12 @@ export type WorkflowSchedule = {
98
98
  * schedule of this list. A second call with the same name replaces the first.
99
99
  */
100
100
  export declare function scheduleWorkflow(schedule: WorkflowSchedule): void;
101
+ /**
102
+ * Store each schedule of {@link scheduleWorkflow} in the system database, and delete each stored schedule of this
103
+ * application that no code registers. DBOS only upserts, thus a schedule of a removed workflow fires on each tick and
104
+ * finds no function. Each schedule of the harness goes through `scheduleWorkflow`.
105
+ */
106
+ export declare function applyWorkflowSchedules(): Promise<void>;
101
107
  /**
102
108
  * Launch DBOS. Idempotent — a second call is a no-op so tests that drive
103
109
  * the harness twice (or accidentally double-import) don't re-launch.
@@ -130,6 +130,19 @@ const pendingSchedules = new Map();
130
130
  export function scheduleWorkflow(schedule) {
131
131
  pendingSchedules.set(schedule.scheduleName, schedule);
132
132
  }
133
+ /**
134
+ * Store each schedule of {@link scheduleWorkflow} in the system database, and delete each stored schedule of this
135
+ * application that no code registers. DBOS only upserts, thus a schedule of a removed workflow fires on each tick and
136
+ * finds no function. Each schedule of the harness goes through `scheduleWorkflow`.
137
+ */
138
+ export async function applyWorkflowSchedules() {
139
+ if (pendingSchedules.size > 0)
140
+ await DBOS.applySchedules([...pendingSchedules.values()]);
141
+ for (const stored of await DBOS.listSchedules()) {
142
+ if (!pendingSchedules.has(stored.scheduleName))
143
+ await DBOS.deleteSchedule(stored.scheduleName);
144
+ }
145
+ }
133
146
  let resultRecipesRegistered = false;
134
147
  /**
135
148
  * DBOS refuses a registration after the launch, and the registry of SuperJSON
@@ -160,8 +173,7 @@ export async function launchDbos({ config, logger: injected }) {
160
173
  registerResultSerialization();
161
174
  const start = performance.now();
162
175
  await DBOS.launch();
163
- if (pendingSchedules.size > 0)
164
- await DBOS.applySchedules([...pendingSchedules.values()]);
176
+ await applyWorkflowSchedules();
165
177
  state.launched = true;
166
178
  state.recoveryStarted = true;
167
179
  logger.info("launched", {
@@ -86,6 +86,8 @@ export type ArtifactPageQuery = {
86
86
  q?: string;
87
87
  /** The whole path, with case. It selects the one row of that path. */
88
88
  path?: string;
89
+ /** The role of the row. `step_output` selects the rows that the grouped run count of {@link countArtifactsByRuns} counts. */
90
+ role?: ArtifactRole;
89
91
  /**
90
92
  * Extensions in lower case, with no dot. A row is selected when the extension of its path is a member. A row with
91
93
  * no extension never matches, and an empty list selects each row. The read compares the members as they are.
@@ -125,7 +127,8 @@ export type ArtifactPage = {
125
127
  * select, grouped by extension, and the page. The count is a separate statement because a page past the end has no
126
128
  * row to carry it. `total` is the sum of the groups.
127
129
  *
128
- * Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included. The cost
130
+ * Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included, unless the `role`
131
+ * filter selects one role. `total` and `extensionCounts` follow the filters. The cost
129
132
  * grows with the ledger of the one analysis, because the count reads each filtered row.
130
133
  */
131
134
  export declare function queryArtifactPage(pool: Querier, analysisId: string, query: ArtifactPageQuery): ResultAsync<ArtifactPage, DbError>;
@@ -193,7 +193,8 @@ const EXTENSION_SQL = `lower(substring(regexp_replace(path, '^.*/', '') from '^.
193
193
  * select, grouped by extension, and the page. The count is a separate statement because a page past the end has no
194
194
  * row to carry it. `total` is the sum of the groups.
195
195
  *
196
- * Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included. The cost
196
+ * Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included, unless the `role`
197
+ * filter selects one role. `total` and `extensionCounts` follow the filters. The cost
197
198
  * grows with the ledger of the one analysis, because the count reads each filtered row.
198
199
  */
199
200
  export function queryArtifactPage(pool, analysisId, query) {
@@ -215,6 +216,8 @@ export function queryArtifactPage(pool, analysisId, query) {
215
216
  conditions.push(`strpos(lower(path), lower(${bind(query.q)}::text)) > 0`);
216
217
  if (query.path !== undefined)
217
218
  conditions.push(`path = ${bind(query.path)}`);
219
+ if (query.role !== undefined)
220
+ conditions.push(`role = ${bind(query.role)}`);
218
221
  if (query.extensions !== undefined && query.extensions.length > 0) {
219
222
  conditions.push(`${EXTENSION_SQL} = ANY(${bind([...query.extensions])}::text[])`);
220
223
  }
@@ -14,10 +14,10 @@ export type { RegisterArtifactInput, ArtifactOrigin, InputArtifactMeta, StepArti
14
14
  export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, reconcileOrphanedRuns, ORPHANED_RUN_GRACE_MS, markRunCanceledIfActive, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
15
15
  setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, countRunsByAnalysis, queryRunsByThread, queryRunIdsByPlan, queryNewestPlanRun, } from "./runs.js";
16
16
  export type { InsertRunInput, RunPage, RunReservation } from "./runs.js";
17
- export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun } from "./step-executions.js";
18
- export type { InsertStepExecutionInput, SeedStepExecutionRow, UpdateStepExecutionInput } from "./step-executions.js";
19
- export { insertPlan, upsertPlan, loadPlan, loadPlanRecord } from "./plans.js";
20
- export type { InsertPlanInput, UpsertPlanInput, PlanRecord, PlanRowQuerier } from "./plans.js";
17
+ export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun, countStepsByRuns, } from "./step-executions.js";
18
+ export type { InsertStepExecutionInput, RunStepCounts, SeedStepExecutionRow, UpdateStepExecutionInput } from "./step-executions.js";
19
+ export { insertPlan, upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./plans.js";
20
+ export type { InsertPlanInput, UpsertPlanInput, PlanRecord, PlanRowQuerier, PlanPage, PlanPageEntry } from "./plans.js";
21
21
  export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
22
22
  export type { DataProfileStatus, DataProfileTerminalWrite, DataProfileResult, DataProfileChecked, DataProfileCompanionCompleteness, DataProfileDimension, DataProfileGroup, DataProfileGroupSlot, DataProfileFile, DataProfileMemberAnnotation, DataProfileObservation, DataProfilePartition, DataProfileProbeReport, DataProfileQualityAssessment, DataProfileQuarantine, DataProfileRecipeStep, DataProfileKind, DataProfileAxis, DataProfileInputSignature, DataProfileCoverage, } from "./data-profile.js";
23
23
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses, queryLatestRunsByAnalyses, queryRunProgressByRuns, queryRunsEndedSince, } from "./analyses-metrics.js";
@@ -11,8 +11,8 @@ export { upsertAnalysis, loadAnalysisStatus } from "./analyses.js";
11
11
  export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifactPaths, queryUnsyncedStepArtifacts, updateArtifactId, updateFileIds, queryArtifactPage, queryArtifactOrigins, countArtifactsByRuns, } from "./artifacts.js";
12
12
  export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, reconcileOrphanedRuns, ORPHANED_RUN_GRACE_MS, markRunCanceledIfActive, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
13
13
  setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, countRunsByAnalysis, queryRunsByThread, queryRunIdsByPlan, queryNewestPlanRun, } from "./runs.js";
14
- export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun } from "./step-executions.js";
15
- export { insertPlan, upsertPlan, loadPlan, loadPlanRecord } from "./plans.js";
14
+ export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun, countStepsByRuns, } from "./step-executions.js";
15
+ export { insertPlan, upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./plans.js";
16
16
  export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
17
17
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses, queryLatestRunsByAnalyses, queryRunProgressByRuns, queryRunsEndedSince, } from "./analyses-metrics.js";
18
18
  // Not per-entity: one analysis's whole Postgres footprint, reached from its id
@@ -94,3 +94,30 @@ export type PlanRecord = {
94
94
  export declare function loadPlanRecord(pool: PlanRowQuerier, planId: string, opts: {
95
95
  analysisId: string;
96
96
  }): ResultAsync<PlanRecord | null, DbError>;
97
+ /** One plan of {@link queryPlanPage}. */
98
+ export type PlanPageEntry = {
99
+ planId: string;
100
+ createdAt: string;
101
+ /** The `title` of the stored document when it is a string, as the document holds it, else `null`. */
102
+ title: string | null;
103
+ parentPlanId: string | null;
104
+ /** The newest plan of the same analysis that names this plan as its parent. */
105
+ supersededBy: string | null;
106
+ /** The newest run of the plan, in the order of {@link queryRunIdsByPlan}, or `null` for a plan with no run. */
107
+ newestRunId: string | null;
108
+ };
109
+ /** One page of the plans of an analysis, and the exact count of them. */
110
+ export type PlanPage = {
111
+ entries: PlanPageEntry[];
112
+ total: number;
113
+ };
114
+ /**
115
+ * One page of the plans of an analysis, newest first, in two statements: the count of the plans and the page. The
116
+ * count is a separate statement because a page past the end has no row to carry it. The order is the one of
117
+ * `supersededBy`: the creation time, then the highest plan id. The page gives the title of the document, not the
118
+ * document, because a page holds up to 200 plans.
119
+ */
120
+ export declare function queryPlanPage(pool: Querier, analysisId: string, opts: {
121
+ limit: number;
122
+ offset: number;
123
+ }): ResultAsync<PlanPage, DbError>;
@@ -117,6 +117,13 @@ const PlanRecordRowSchema = z.object({
117
117
  created_at: z.string(),
118
118
  superseded_by: z.string().nullable(),
119
119
  });
120
+ /**
121
+ * The `superseded_by` column of a plan read: the newest plan of the same analysis that names the plan as its parent.
122
+ * The text refers to the plan row of the outer query as `p`.
123
+ */
124
+ const SUPERSEDED_BY_SQL = `(SELECT c.plan_id FROM cortex_plans c
125
+ WHERE c.parent_plan_id = p.plan_id AND c.analysis_id = p.analysis_id
126
+ ORDER BY c.created_at DESC, c.plan_id DESC LIMIT 1) AS superseded_by`;
120
127
  /**
121
128
  * Analysis-scoped read of one plan with its parent and the newest plan that
122
129
  * replaced it, in one statement. Returns `ok(null)` for an unknown planId OR a
@@ -129,9 +136,7 @@ export function loadPlanRecord(pool, planId, opts) {
129
136
  return tryQuery("plans.loadPlanRecord", async () => {
130
137
  const result = await pool.query({
131
138
  text: `SELECT p.plan, p.parent_plan_id, p.created_at,
132
- (SELECT c.plan_id FROM cortex_plans c
133
- WHERE c.parent_plan_id = p.plan_id AND c.analysis_id = p.analysis_id
134
- ORDER BY c.created_at DESC, c.plan_id DESC LIMIT 1) AS superseded_by
139
+ ${SUPERSEDED_BY_SQL}
135
140
  FROM cortex_plans p
136
141
  WHERE p.plan_id = $1 AND p.analysis_id = $2`,
137
142
  values: [planId, opts.analysisId],
@@ -143,3 +148,52 @@ export function loadPlanRecord(pool, planId, opts) {
143
148
  return { planId, plan: row.plan, parentPlanId: row.parent_plan_id, supersededBy: row.superseded_by, createdAt: row.created_at };
144
149
  });
145
150
  }
151
+ const PlanPageRowSchema = z.object({
152
+ plan_id: z.string(),
153
+ created_at: z.string(),
154
+ title: z.string().nullable(),
155
+ parent_plan_id: z.string().nullable(),
156
+ superseded_by: z.string().nullable(),
157
+ newest_run_id: z.string().nullable(),
158
+ });
159
+ /**
160
+ * One page of the plans of an analysis, newest first, in two statements: the count of the plans and the page. The
161
+ * count is a separate statement because a page past the end has no row to carry it. The order is the one of
162
+ * `supersededBy`: the creation time, then the highest plan id. The page gives the title of the document, not the
163
+ * document, because a page holds up to 200 plans.
164
+ */
165
+ export function queryPlanPage(pool, analysisId, opts) {
166
+ return tryQuery("plans.queryPlanPage", async () => {
167
+ const countResult = await pool.query({
168
+ text: "SELECT COUNT(*)::int AS total FROM cortex_plans WHERE analysis_id = $1",
169
+ values: [analysisId],
170
+ });
171
+ const rowsResult = await pool.query({
172
+ text: `SELECT p.plan_id, p.created_at, p.parent_plan_id,
173
+ CASE WHEN jsonb_typeof(p.plan->'title') = 'string' THEN p.plan->>'title' END AS title,
174
+ ${SUPERSEDED_BY_SQL},
175
+ (SELECT r.run_id FROM cortex_runs r
176
+ WHERE r.analysis_id = p.analysis_id AND r.plan_id = p.plan_id
177
+ ORDER BY r.started_at DESC, r.run_id DESC LIMIT 1) AS newest_run_id
178
+ FROM cortex_plans p
179
+ WHERE p.analysis_id = $1
180
+ ORDER BY p.created_at DESC, p.plan_id DESC
181
+ LIMIT $2 OFFSET $3`,
182
+ values: [analysisId, opts.limit, opts.offset],
183
+ });
184
+ return {
185
+ entries: rowsResult.rows.map((raw) => {
186
+ const row = PlanPageRowSchema.parse(raw);
187
+ return {
188
+ planId: row.plan_id,
189
+ createdAt: row.created_at,
190
+ title: row.title,
191
+ parentPlanId: row.parent_plan_id,
192
+ supersededBy: row.superseded_by,
193
+ newestRunId: row.newest_run_id,
194
+ };
195
+ }),
196
+ total: countResult.rows[0]?.total ?? 0,
197
+ };
198
+ });
199
+ }
@@ -94,3 +94,16 @@ export declare function sweepPendingStepExecutions(pool: Querier, runId: string)
94
94
  */
95
95
  export declare function cancelRunningStepExecutions(pool: Querier, runId: string, reason: string): ResultAsync<void, DbError>;
96
96
  export declare function queryStepsByRun(pool: StepRowQuerier, runId: string): ResultAsync<StepExecutionRow[], DbError>;
97
+ /** The step counts of one run, from {@link countStepsByRuns}. */
98
+ export type RunStepCounts = {
99
+ runId: string;
100
+ doneSteps: number;
101
+ runningSteps: number;
102
+ totalSteps: number;
103
+ };
104
+ /**
105
+ * The step counts of each run of a list, in one statement. `totalSteps` counts each row, the `synthesis` row
106
+ * included, and `doneSteps` counts the `completed` rows: the rule of the analysis summary reads. A step that
107
+ * waits counts in `totalSteps` only. A run with no row has no entry, and a caller reads it as 0.
108
+ */
109
+ export declare function countStepsByRuns(pool: Querier, analysisId: string, runIds: readonly string[]): ResultAsync<RunStepCounts[], DbError>;