@inflexa-ai/harness 0.47.0 → 0.48.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 (36) 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 +3 -1
  7. package/dist/index.js +2 -1
  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/state/index.d.ts +2 -2
  16. package/dist/state/index.js +1 -1
  17. package/dist/state/step-executions.d.ts +13 -0
  18. package/dist/state/step-executions.js +22 -0
  19. package/dist/tools/report-authoring/authoring-tools.d.ts +2 -2
  20. package/dist/tools/report-authoring/authoring-tools.js +31 -6
  21. package/dist/tools/report-session/script-tools.d.ts +3 -0
  22. package/dist/tools/report-session/script-tools.js +10 -1
  23. package/dist/tools/script/archive.d.ts +34 -0
  24. package/dist/tools/script/archive.js +182 -0
  25. package/dist/tools/script/file-tools.d.ts +10 -4
  26. package/dist/tools/script/file-tools.js +156 -42
  27. package/dist/tools/script/metrics.d.ts +10 -1
  28. package/dist/tools/script/metrics.js +17 -2
  29. package/dist/tools/script/run-script.js +91 -4
  30. package/dist/tools/script/scratch.d.ts +3 -0
  31. package/dist/tools/script/scratch.js +7 -4
  32. package/dist/tools/workspace/mutator.d.ts +3 -0
  33. package/dist/tools/workspace/mutator.js +11 -1
  34. package/dist/workspace/session-files.d.ts +35 -0
  35. package/dist/workspace/session-files.js +16 -0
  36. 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";
@@ -193,7 +194,8 @@ export { upsertPlan, loadPlan, loadPlanRecord } from "./state/plans.js";
193
194
  export type { UpsertPlanInput, PlanRecord } 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
@@ -295,7 +295,8 @@ export { validatePlan } from "./schemas/validate-plan.js";
295
295
  // the run-state calls mirror the chat trigger's reserve → dedup → status flow.
296
296
  export { upsertPlan, loadPlan, loadPlanRecord } 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,
@@ -14,8 +14,8 @@ 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";
17
+ export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun, countStepsByRuns, } from "./step-executions.js";
18
+ export type { InsertStepExecutionInput, RunStepCounts, SeedStepExecutionRow, UpdateStepExecutionInput } from "./step-executions.js";
19
19
  export { insertPlan, upsertPlan, loadPlan, loadPlanRecord } from "./plans.js";
20
20
  export type { InsertPlanInput, UpsertPlanInput, PlanRecord, PlanRowQuerier } from "./plans.js";
21
21
  export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
@@ -11,7 +11,7 @@ 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";
14
+ export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun, countStepsByRuns, } from "./step-executions.js";
15
15
  export { insertPlan, upsertPlan, loadPlan, loadPlanRecord } 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";
@@ -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>;
@@ -172,6 +172,28 @@ export function queryStepsByRun(pool, runId) {
172
172
  return result.rows.map(mapStepExecutionRow);
173
173
  });
174
174
  }
175
+ /**
176
+ * The step counts of each run of a list, in one statement. `totalSteps` counts each row, the `synthesis` row
177
+ * included, and `doneSteps` counts the `completed` rows: the rule of the analysis summary reads. A step that
178
+ * waits counts in `totalSteps` only. A run with no row has no entry, and a caller reads it as 0.
179
+ */
180
+ export function countStepsByRuns(pool, analysisId, runIds) {
181
+ return tryQuery("stepExecutions.countStepsByRuns", async () => {
182
+ if (runIds.length === 0)
183
+ return [];
184
+ const result = await pool.query({
185
+ text: `SELECT run_id,
186
+ COUNT(*) FILTER (WHERE status = 'completed')::int AS done,
187
+ COUNT(*) FILTER (WHERE status = 'running')::int AS running,
188
+ COUNT(*)::int AS total
189
+ FROM cortex_step_executions
190
+ WHERE analysis_id = $1 AND run_id = ANY($2::text[])
191
+ GROUP BY run_id`,
192
+ values: [analysisId, [...runIds]],
193
+ });
194
+ return result.rows.map((r) => ({ runId: r.run_id, doneSteps: r.done, runningSteps: r.running, totalSteps: r.total }));
195
+ });
196
+ }
175
197
  function mapStepExecutionRow(row) {
176
198
  return StepExecutionRowSchema.parse({
177
199
  runId: row.run_id,
@@ -16,8 +16,8 @@
16
16
  * The primitive admits a flat object input only. Thus a destination is four optional fields, and not a
17
17
  * nested union. A conflict between the flat fields refuses before the core runs.
18
18
  *
19
- * Inside a durable step, a mutation stores its result for the thread and the step, in the transaction of
20
- * the document and under the lock of the row. A rerun of the step after the landing, before its
19
+ * Inside a durable step, a mutation stores its result for the thread, the step, and the call, in the
20
+ * transaction of the document and under the lock of the row. A rerun of the step after the landing, before its
21
21
  * checkpoint, gives the stored result. Thus a rerun of `add_block` does not refuse its own block id, and a
22
22
  * rerun of `remove_block` does not refuse its own removed target.
23
23
  */
@@ -16,8 +16,8 @@
16
16
  * The primitive admits a flat object input only. Thus a destination is four optional fields, and not a
17
17
  * nested union. A conflict between the flat fields refuses before the core runs.
18
18
  *
19
- * Inside a durable step, a mutation stores its result for the thread and the step, in the transaction of
20
- * the document and under the lock of the row. A rerun of the step after the landing, before its
19
+ * Inside a durable step, a mutation stores its result for the thread, the step, and the call, in the
20
+ * transaction of the document and under the lock of the row. A rerun of the step after the landing, before its
21
21
  * checkpoint, gives the stored result. Thus a rerun of `add_block` does not refuse its own block id, and a
22
22
  * rerun of `remove_block` does not refuse its own removed target.
23
23
  */
@@ -133,6 +133,23 @@ const LandedResultSchema = z.object({
133
133
  })),
134
134
  })),
135
135
  });
136
+ /** A refusal as a script reads it: the closed reason and the prose detail. */
137
+ const RefusalOutputSchema = z.object({ reason: z.string(), detail: z.string() });
138
+ const OutlineEntryOutputSchema = z.object({ id: z.string(), kind: z.string(), depth: z.number(), label: z.string() });
139
+ /** The result of an edit, as `run_script` shows it to a script. A refused edit lands nothing. */
140
+ const MutationOutputSchema = z.union([
141
+ z.object({
142
+ applied: z.literal(true),
143
+ changed: z.array(z.object({ parentId: z.string().optional(), children: z.array(OutlineEntryOutputSchema) })),
144
+ }),
145
+ z.object({ applied: z.literal(false), refusal: RefusalOutputSchema }),
146
+ ]);
147
+ const OutlineOutputSchema = z.union([z.object({ outline: z.array(OutlineEntryOutputSchema) }), z.object({ refused: RefusalOutputSchema })]);
148
+ const ReadBlockOutputSchema = z.union([
149
+ z.object({ found: z.literal(true), block: z.record(z.string(), z.unknown()) }),
150
+ z.object({ found: z.literal(false) }),
151
+ z.object({ refused: RefusalOutputSchema }),
152
+ ]);
136
153
  /**
137
154
  * Resolve the report thread of a call, and load its state through the gateway.
138
155
  *
@@ -439,7 +456,9 @@ export function createReportAuthoringTools(gateway, deps = {}) {
439
456
  * the landing of its own earlier run.
440
457
  */
441
458
  const openMutation = async (ctx) => {
442
- const callKey = stepKey();
459
+ // The calls of one script share the step of `run_script`, thus the key names the call inside its step.
460
+ const step = stepKey();
461
+ const callKey = step === undefined ? undefined : `${step}:${ctx.invocationId}`;
443
462
  const opened = await openReportThread(gateway, ctx.session.scope, callKey);
444
463
  if (opened.isErr())
445
464
  return { done: refuse(opened.error) };
@@ -504,6 +523,7 @@ export function createReportAuthoringTools(gateway, deps = {}) {
504
523
  "The root admits a section only, and an atom needs a section as its parent. " +
505
524
  "A reference names the path of a pinned artifact, and the session stamps the hash from the pinned evidence.",
506
525
  inputSchema: addBlockInput,
526
+ outputSchema: MutationOutputSchema,
507
527
  describeCall: (input) => {
508
528
  const block = decodeBlockPayload(input.block);
509
529
  const kind = readKind(block);
@@ -534,10 +554,11 @@ export function createReportAuthoringTools(gateway, deps = {}) {
534
554
  });
535
555
  const change_block = defineTool({
536
556
  id: "change_block",
537
- description: "Change one block by its id. Give a `title` to retitle a section, or a `block` to replace an atom, and never both. " +
557
+ description: "Change one block by its id: a `title` retitles a section, or a `block` replaces an atom, and never both. " +
538
558
  "The change keeps the id of the target, thus the `id` of the payload does not matter. A kind change is permitted. " +
539
559
  "A reference names the path of a pinned artifact, and the session stamps the hash from the pinned evidence.",
540
560
  inputSchema: changeBlockInput,
561
+ outputSchema: MutationOutputSchema,
541
562
  describeCall: (input) => `change ${input.targetId}`,
542
563
  execute: async (input, ctx) => {
543
564
  const opening = await openMutation(ctx);
@@ -560,6 +581,7 @@ export function createReportAuthoringTools(gateway, deps = {}) {
560
581
  id: "remove_block",
561
582
  description: "Remove one block by its id. A removed section takes its whole subtree with it, and the id is free for a later add.",
562
583
  inputSchema: removeBlockInput,
584
+ outputSchema: MutationOutputSchema,
563
585
  describeCall: (input) => `remove ${input.targetId}`,
564
586
  execute: async (input, ctx) => {
565
587
  const opening = await openMutation(ctx);
@@ -579,6 +601,7 @@ export function createReportAuthoringTools(gateway, deps = {}) {
579
601
  description: "Move one block by its id to a new destination. Name the destination with `parentId` and one of `place`, `before`, or `after`. " +
580
602
  "A section cannot move into its own subtree.",
581
603
  inputSchema: moveBlockInput,
604
+ outputSchema: MutationOutputSchema,
582
605
  describeCall: (input) => `move ${input.targetId}`,
583
606
  execute: async (input, ctx) => {
584
607
  const opening = await openMutation(ctx);
@@ -596,9 +619,9 @@ export function createReportAuthoringTools(gateway, deps = {}) {
596
619
  });
597
620
  const set_title = defineTool({
598
621
  id: "set_title",
599
- description: "Set the title of the report document. A draft starts with no title, and a document needs one. " +
600
- "The title names the whole report, and it is not the title of a section.",
622
+ description: "Set the title of the whole report, not the title of a section. A draft starts with no title, and a document needs one.",
601
623
  inputSchema: setTitleInput,
624
+ outputSchema: MutationOutputSchema,
602
625
  describeCall: (input) => `title the report ${input.title}`,
603
626
  // The title sits on the document, thus no container changes its child order.
604
627
  execute: async (input, ctx) => {
@@ -624,6 +647,7 @@ export function createReportAuthoringTools(gateway, deps = {}) {
624
647
  "A label that ends with the character … is clipped. Use the outline as the primary view of the draft, " +
625
648
  "and read one block when the label is not enough.",
626
649
  inputSchema: readOutlineInput,
650
+ outputSchema: OutlineOutputSchema,
627
651
  // The input carries no field, thus a hook can only restate the name of the tool.
628
652
  describeCall: "none",
629
653
  execute: async (_input, ctx) => {
@@ -640,6 +664,7 @@ export function createReportAuthoringTools(gateway, deps = {}) {
640
664
  "because the session owns the hash. A section gives its title and the id of each child, " +
641
665
  "because the outline already names every block under it. Use it when the outline label is not enough.",
642
666
  inputSchema: readBlockInput,
667
+ outputSchema: ReadBlockOutputSchema,
643
668
  describeCall: (input) => `read ${input.targetId}`,
644
669
  execute: async (input, ctx) => {
645
670
  const opened = await openThread(ctx);
@@ -18,12 +18,15 @@ import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
18
18
  import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
19
19
  import { type Tool } from "../define-tool.js";
20
20
  import { type ReportSessionStateGateway } from "../report-authoring/authoring-tools.js";
21
+ import { type SyncSessionFile } from "../../workspace/session-files.js";
21
22
  export type ReportScriptToolsDeps = {
22
23
  readonly gateway: ReportSessionStateGateway;
23
24
  readonly workspaceFs: WorkspaceFilesystem;
24
25
  readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
25
26
  readonly derivations: Pick<ReportSessionStateStore, "appendDerivation">;
26
27
  readonly provenance?: ProvenanceSeam;
28
+ /** The host copy of each derived table, owned by the thread of the session. */
29
+ readonly syncSessionFile?: SyncSessionFile;
27
30
  readonly logger?: Logger;
28
31
  };
29
32
  /** The script tools of a report session. */
@@ -24,6 +24,7 @@ import { openReportThread } from "../report-authoring/authoring-tools.js";
24
24
  import { ReadTableOutputSchema, readTableRows, rowsText } from "../script/file-tools.js";
25
25
  import { DERIVED_FILE_CAP_BYTES } from "../script/scratch.js";
26
26
  import { confinedWrite } from "../workspace/mutator.js";
27
+ import { syncWrittenFile } from "../../workspace/session-files.js";
27
28
  function refused(error) {
28
29
  return err({ error, retryable: false });
29
30
  }
@@ -57,7 +58,7 @@ function createReadTableTool(deps) {
57
58
  });
58
59
  if (table.isErr())
59
60
  return refused(table.error);
60
- const hash = computeSha256(table.value.content);
61
+ const hash = computeSha256(table.value.source);
61
62
  if (hash !== entry.hash)
62
63
  return refused(`${path} changed after the session pinned it, thus its bytes are not the evidence of this session`);
63
64
  const { rows } = table.value;
@@ -167,6 +168,14 @@ function createWriteDerivedTool(deps) {
167
168
  if (appended.value === "absent")
168
169
  return refused("no report session state row exists to hold the derivation");
169
170
  observeDerived(analysisId, threadId, record);
171
+ await syncWrittenFile(deps.syncSessionFile, logger, {
172
+ session: ctx.session,
173
+ owner: { kind: "thread", threadId },
174
+ path: outputPath,
175
+ hash: record.outputHash,
176
+ size: landed.bytes.length,
177
+ content: landed.bytes,
178
+ });
170
179
  return ok(derivedOutput(record, landed.bytes.length, columns));
171
180
  },
172
181
  });