@inflexa-ai/harness 0.43.1 → 0.45.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 (119) hide show
  1. package/dist/agents/report-session-agent.d.ts +1 -1
  2. package/dist/app/approve-plan.d.ts +48 -0
  3. package/dist/app/approve-plan.js +84 -0
  4. package/dist/app/data-profile-view.d.ts +1 -1
  5. package/dist/app/run-activity.js +4 -6
  6. package/dist/app/synthesize-run.js +1 -1
  7. package/dist/contracts/chat-events.d.ts +2 -0
  8. package/dist/contracts/chat-frame.d.ts +7 -0
  9. package/dist/contracts/chat-frame.js +18 -4
  10. package/dist/contracts/chat-parts.d.ts +73 -2
  11. package/dist/contracts/chat-parts.js +3 -1
  12. package/dist/contracts/index.d.ts +3 -1
  13. package/dist/contracts/index.js +1 -0
  14. package/dist/contracts/part-registry.js +3 -0
  15. package/dist/contracts/schemas/chat-events.d.ts +13 -0
  16. package/dist/contracts/schemas/chat-events.js +1 -0
  17. package/dist/contracts/schemas/chat-parts.d.ts +200 -15
  18. package/dist/contracts/schemas/chat-parts.js +63 -12
  19. package/dist/contracts/schemas/index.d.ts +1 -1
  20. package/dist/contracts/schemas/index.js +1 -1
  21. package/dist/contracts/subagent-activity.d.ts +12 -0
  22. package/dist/contracts/subagent-activity.js +76 -0
  23. package/dist/execution/artifact-metadata.js +1 -3
  24. package/dist/execution/post-step-pipeline.js +1 -1
  25. package/dist/execution/run-canceler.js +1 -1
  26. package/dist/execution/run-event-parts.d.ts +6 -5
  27. package/dist/execution/run-event-parts.js +6 -5
  28. package/dist/execution/run-event-stream.d.ts +18 -6
  29. package/dist/execution/run-event-stream.js +67 -17
  30. package/dist/execution/run-synthesis.d.ts +21 -0
  31. package/dist/execution/run-synthesis.js +77 -3
  32. package/dist/index.d.ts +24 -10
  33. package/dist/index.js +17 -3
  34. package/dist/lib/hooks.d.ts +1 -1
  35. package/dist/lib/metrics.d.ts +2 -2
  36. package/dist/loop/run-agent.js +1 -0
  37. package/dist/loop/types.d.ts +2 -0
  38. package/dist/memory/card-builders.d.ts +11 -0
  39. package/dist/memory/card-builders.js +35 -21
  40. package/dist/memory/conversation-display-recorder.d.ts +2 -0
  41. package/dist/memory/conversation-display-recorder.js +10 -1
  42. package/dist/memory/conversation-display-storage.d.ts +9 -2
  43. package/dist/memory/conversation-display-storage.js +18 -3
  44. package/dist/memory/thread-history.d.ts +8 -1
  45. package/dist/memory/thread-history.js +13 -3
  46. package/dist/memory/thread-store.d.ts +13 -0
  47. package/dist/memory/thread-store.js +15 -1
  48. package/dist/prompts/conversation.d.ts +1 -1
  49. package/dist/prompts/conversation.js +3 -1
  50. package/dist/provenance/seam.d.ts +16 -5
  51. package/dist/providers/ai-sdk.js +6 -4
  52. package/dist/reference-data/receipt.d.ts +16 -4
  53. package/dist/reference-data/receipt.js +20 -3
  54. package/dist/report-model/pin-snapshot.d.ts +1 -1
  55. package/dist/report-model/pin-snapshot.js +4 -52
  56. package/dist/runtime/assemble.d.ts +9 -2
  57. package/dist/runtime/assemble.js +13 -2
  58. package/dist/runtime/boot.d.ts +3 -0
  59. package/dist/runtime/boot.js +10 -1
  60. package/dist/sandbox/client.d.ts +1 -1
  61. package/dist/sandbox/create-sandbox.js +1 -1
  62. package/dist/sandbox/docker-client.js +1 -1
  63. package/dist/sandbox/k8s-client.js +5 -5
  64. package/dist/sandbox/sandbox-step-translate.d.ts +23 -0
  65. package/dist/sandbox/sandbox-step-translate.js +24 -0
  66. package/dist/schemas/artifact-manifest.d.ts +1 -1
  67. package/dist/schemas/artifact-manifest.js +11 -9
  68. package/dist/schemas/plan-schemas.d.ts +18 -18
  69. package/dist/schemas/run-synthesis.d.ts +2 -2
  70. package/dist/state/analyses-metrics.d.ts +79 -7
  71. package/dist/state/analyses-metrics.js +141 -6
  72. package/dist/state/artifacts.d.ts +50 -3
  73. package/dist/state/artifacts.js +67 -7
  74. package/dist/state/data-profile.d.ts +4 -2
  75. package/dist/state/data-profile.js +7 -5
  76. package/dist/state/index.d.ts +9 -8
  77. package/dist/state/index.js +5 -5
  78. package/dist/state/migrations/20261012120000_index_the_runs_by_start.d.ts +8 -0
  79. package/dist/state/migrations/20261012120000_index_the_runs_by_start.js +12 -0
  80. package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.d.ts +8 -0
  81. package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.js +13 -0
  82. package/dist/state/migrations/20261013180000_narrow_the_active_run_index.d.ts +8 -0
  83. package/dist/state/migrations/20261013180000_narrow_the_active_run_index.js +13 -0
  84. package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts +8 -0
  85. package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js +13 -0
  86. package/dist/state/migrations/index.js +8 -0
  87. package/dist/state/plans.d.ts +21 -0
  88. package/dist/state/plans.js +25 -0
  89. package/dist/state/runs.d.ts +36 -5
  90. package/dist/state/runs.js +65 -19
  91. package/dist/state/schema.d.ts +2 -0
  92. package/dist/tasks/data-profile-activity.d.ts +7 -1
  93. package/dist/tasks/data-profile-activity.js +18 -9
  94. package/dist/tasks/data-profile.d.ts +15 -1
  95. package/dist/tasks/data-profile.js +18 -13
  96. package/dist/tools/approval/gateway.js +28 -5
  97. package/dist/tools/display/show-user.d.ts +1 -1
  98. package/dist/tools/execute-analysis.d.ts +51 -0
  99. package/dist/tools/execute-analysis.js +241 -109
  100. package/dist/tools/report-session/derive-table.js +1 -1
  101. package/dist/tools/report-session/examine-page.js +1 -1
  102. package/dist/tools/report-session/list-artifacts.js +2 -2
  103. package/dist/tools/report-session/preview-report.d.ts +1 -1
  104. package/dist/tools/report-session/preview-report.js +6 -6
  105. package/dist/tools/report-session/record-version.d.ts +1 -1
  106. package/dist/tools/report-session/record-version.js +4 -4
  107. package/dist/tools/research/inspect-data-profile.d.ts +1 -1
  108. package/dist/tools/research/inspect-run.d.ts +1 -1
  109. package/dist/tools/research/inspect-run.js +7 -7
  110. package/dist/tools/workspace/mutator.js +2 -2
  111. package/dist/workflows/execute-analysis.d.ts +4 -3
  112. package/dist/workflows/execute-analysis.js +38 -25
  113. package/dist/workflows/plan/graph.d.ts +16 -0
  114. package/dist/workflows/plan/graph.js +36 -0
  115. package/dist/workflows/sandbox-step.d.ts +2 -2
  116. package/dist/workflows/sandbox-step.js +16 -14
  117. package/dist/workspace/filesystem.js +18 -22
  118. package/dist/workspace/paths.d.ts +7 -4
  119. package/package.json +4 -4
@@ -17,54 +17,11 @@
17
17
  * keeps the membership role, thus the record map is optional and it changes no resolution.
18
18
  */
19
19
  import { err, ok } from "neverthrow";
20
- import { open } from "node:fs/promises";
21
- import { join } from "node:path";
22
- import { tryFs } from "../lib/fs-result.js";
20
+ import { readSynthesisText } from "../execution/run-synthesis.js";
23
21
  import { queryAnalysisArtifacts } from "../state/artifacts.js";
24
22
  import { queryRunsByAnalysis } from "../state/runs.js";
25
- import { isSafeId, runDir } from "../workspace/paths.js";
26
- /** The file name of the synthesis record inside the run directory. */
27
- const SYNTHESIS_FILE = "synthesis.json";
28
- /**
29
- * The cap of one synthesis read. A synthesis record is small by construction, and the cap bounds the
30
- * cost of a file that is not. A file over the cap gives no key, because a cut JSON text cannot parse.
31
- */
32
- const SYNTHESIS_CAP_BYTES = 1024 * 1024;
33
23
  /** The page size of the run listing. The listing walks the pages until a short page ends the walk. */
34
24
  const RUN_PAGE_SIZE = 200;
35
- /**
36
- * Read a synthesis record under the cap, or give `undefined` when the bytes do not come back.
37
- *
38
- * The read takes a bounded byte window, thus a file of any size costs the cap. An absent file, a genuine
39
- * read fault, and a file over the cap each give `undefined`, because the collection reports no error for
40
- * one record.
41
- */
42
- async function readSynthesisText(absolute) {
43
- return tryFs("pinReportSnapshot.readSynthesis", async () => {
44
- const handle = await open(absolute, "r");
45
- try {
46
- // The window takes one byte more than the cap. A read that fills it states that the file
47
- // holds more bytes than the cap admits, thus the two conditions stay apart.
48
- const buffer = Buffer.alloc(SYNTHESIS_CAP_BYTES + 1);
49
- // One read of a network-backed file can give fewer bytes than the window holds. Such a
50
- // short read cuts the text in the middle of the JSON, and the record then parses to no
51
- // key at all. Thus the loop reads again from the offset that the last read reached, and
52
- // it stops when the window fills or when a read gives no byte.
53
- let filled = 0;
54
- while (filled < buffer.length) {
55
- const { bytesRead } = await handle.read(buffer, filled, buffer.length - filled, filled);
56
- if (bytesRead === 0) {
57
- break;
58
- }
59
- filled += bytesRead;
60
- }
61
- return filled > SYNTHESIS_CAP_BYTES ? undefined : buffer.subarray(0, filled).toString("utf8");
62
- }
63
- finally {
64
- await handle.close();
65
- }
66
- }, { path: absolute, onAbsent: () => undefined }).unwrapOr(undefined);
67
- }
68
25
  /** The trimmed text of a field, or `undefined` when the field is not a non-empty string. */
69
26
  function trimmedText(value) {
70
27
  if (typeof value !== "string") {
@@ -166,10 +123,10 @@ function citationsOf(text) {
166
123
  async function collectCitations(pool, resolveWorkspaceRoot, analysisId) {
167
124
  let root;
168
125
  try {
169
- root = resolveWorkspaceRoot(analysisId);
126
+ root = await resolveWorkspaceRoot(analysisId);
170
127
  }
171
128
  catch {
172
- // The seam signals an unresolvable resource by a throw. The artifact map still states the
129
+ // The seam signals an unresolvable resource by a rejection. The artifact map still states the
173
130
  // membership of the session, thus the fault costs the citation list alone.
174
131
  return ok({ keys: [], records: new Map() });
175
132
  }
@@ -181,12 +138,7 @@ async function collectCitations(pool, resolveWorkspaceRoot, analysisId) {
181
138
  return err({ kind: "run-listing-failed", cause: page.error.cause });
182
139
  }
183
140
  for (const run of page.value) {
184
- // The path builder refuses an id that can escape the run tree. A refusal is a throw, thus
185
- // the guard keeps the collection on the value channel.
186
- if (!isSafeId(run.runId)) {
187
- continue;
188
- }
189
- const text = await readSynthesisText(join(root, runDir(run.runId), SYNTHESIS_FILE));
141
+ const text = await readSynthesisText({ workspaceRoot: root, runId: run.runId });
190
142
  if (text === undefined) {
191
143
  continue;
192
144
  }
@@ -14,8 +14,9 @@
14
14
  * `applicationVersion` stamp because all registration happens in this one call,
15
15
  * before launch — a blue/green drain treats them as a single cohort.
16
16
  */
17
- import { type Result } from "neverthrow";
17
+ import { type Result, type ResultAsync } from "neverthrow";
18
18
  import { type ConversationAgentDeps } from "../agents/conversation-agent.js";
19
+ import { type ApprovePlanInput, type ApprovePlanRefusal, type PlanApproval } from "../app/approve-plan.js";
19
20
  import { type ReportSessionRuntime } from "../app/report-session-runtime.js";
20
21
  import type { UsageRecorder } from "../billing/usage-recorder.js";
21
22
  import type { ResourcePolicy } from "../config/resource-limits.js";
@@ -97,7 +98,7 @@ export interface ReportResolverScope {
97
98
  * The factory that gives the reference resolver of one report tool call. The harness wires the production
98
99
  * realization, and an embedder can bind its own at its composition root.
99
100
  */
100
- export type MakeReportReferenceResolver = (scope: ReportResolverScope) => ReferenceResolver;
101
+ export type MakeReportReferenceResolver = (scope: ReportResolverScope) => Promise<ReferenceResolver>;
101
102
  export interface CoreRuntimeDeps {
102
103
  readonly conversation: ConversationAssemblyDeps;
103
104
  readonly workflows: CoreWorkflowDeps;
@@ -235,6 +236,12 @@ export interface CoreRuntime {
235
236
  readonly reportSession: Pick<ReportSessionRuntime, "ensureSessionState">;
236
237
  readonly workflows: RegisteredWorkflows;
237
238
  readonly citationResolver: CitationResolver;
239
+ /**
240
+ * Approve a stored plan by its id and start its run with no model call. It runs over the deps that
241
+ * reach the `execute_analysis` tool of the conversation agent, thus the approval and the tool launch
242
+ * a plan the same way.
243
+ */
244
+ readonly approvePlan: (input: ApprovePlanInput) => ResultAsync<PlanApproval, ApprovePlanRefusal>;
238
245
  }
239
246
  /**
240
247
  * The eyes of the composition, or none.
@@ -17,6 +17,7 @@
17
17
  import { err, ok } from "neverthrow";
18
18
  import { createConversationAgent } from "../agents/conversation-agent.js";
19
19
  import { createReportSessionAgent } from "../agents/report-session-agent.js";
20
+ import { approvePlan } from "../app/approve-plan.js";
20
21
  import { createReportSessionRuntime } from "../app/report-session-runtime.js";
21
22
  import { createNoopUsageRecorder } from "../billing/noop-usage-recorder.js";
22
23
  import { hasBrowserUrl } from "../lib/chrome.js";
@@ -152,10 +153,10 @@ export function assembleCoreRuntime(deps) {
152
153
  // for each tool call. The extraction arm falls through to the registered extract-values
153
154
  // callable for an over-cap file or a host parse fault. The cap gates the host read, and
154
155
  // the resolver defaults it to 16 MiB.
155
- const makeProductionReportResolver = (scope) => {
156
+ const makeProductionReportResolver = async (scope) => {
156
157
  const arm = createExtractionArm(bindExtractionTrigger({ runAuthorizer: conversation.runAuthorizer, workflow: extractValues }, { auth: scope.auth, analysisId: scope.analysisId }));
157
158
  return createProductionResolver({
158
- workspaceRoot: conversation.resolveWorkspaceRoot(scope.analysisId),
159
+ workspaceRoot: await conversation.resolveWorkspaceRoot(scope.analysisId),
159
160
  analysisId: scope.analysisId,
160
161
  extractionArm: arm,
161
162
  readCache: reportArtifactReads.forAnalysis(scope.analysisId),
@@ -210,5 +211,15 @@ export function assembleCoreRuntime(deps) {
210
211
  deriveTableExec,
211
212
  },
212
213
  citationResolver,
214
+ approvePlan: (input) => approvePlan({
215
+ pool: conversation.pool,
216
+ executeAnalysisWorkflow: executeAnalysis,
217
+ runAuthorizer: conversation.runAuthorizer,
218
+ runLauncher: conversation.runLauncher,
219
+ ...(resourcePolicy ? { resourcePolicy } : {}),
220
+ ...(conversation.extendAnalysisFarm ? { extendAnalysisFarm: conversation.extendAnalysisFarm } : {}),
221
+ ...(conversation.logger ? { logger: conversation.logger } : {}),
222
+ ...(deps.provenance ? { provenance: deps.provenance } : {}),
223
+ }, input),
213
224
  };
214
225
  }
@@ -25,6 +25,9 @@
25
25
  * legacy-workflow reap, an agent-switch install). Runs after registration
26
26
  * so it may close over the registered callables.
27
27
  * 7. `launchDbos(...)` — the last registration-dependent step.
28
+ * 8. `reconcileOrphanedRuns(pool)` — fails each `running` run whose workflow never started. It runs
29
+ * after launch, thus a workflow that recovery re-queued counts as started. A failure is logged and does
30
+ * not stop the boot, because the repair only frees the slot of a plan.
28
31
  *
29
32
  * No step reads or rewrites stored conversation display. A boot that validated
30
33
  * every `messages` row is what let a single retired part key fail startup for a
@@ -25,6 +25,9 @@
25
25
  * legacy-workflow reap, an agent-switch install). Runs after registration
26
26
  * so it may close over the registered callables.
27
27
  * 7. `launchDbos(...)` — the last registration-dependent step.
28
+ * 8. `reconcileOrphanedRuns(pool)` — fails each `running` run whose workflow never started. It runs
29
+ * after launch, thus a workflow that recovery re-queued counts as started. A failure is logged and does
30
+ * not stop the boot, because the repair only frees the slot of a plan.
28
31
  *
29
32
  * No step reads or rewrites stored conversation display. A boot that validated
30
33
  * every `messages` row is what let a single retired part key fail startup for a
@@ -38,6 +41,7 @@
38
41
  import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
39
42
  import { validateAgentSkills } from "../agents/sandbox/validate-skills.js";
40
43
  import { initCortexState } from "../state/init.js";
44
+ import { reconcileOrphanedRuns } from "../state/runs.js";
41
45
  import { assembleCoreRuntime } from "./assemble.js";
42
46
  import { assertConnectionBudget } from "./connection-budget.js";
43
47
  import { launchDbos, shutdownDbos } from "./dbos.js";
@@ -56,7 +60,12 @@ export async function bootHarness(deps) {
56
60
  const runtime = assembleCoreRuntime(core);
57
61
  await deps.beforeLaunch?.();
58
62
  await launchDbos({ config: deps.dbos, logger });
59
- logger.named("boot").info("harness booted", { executorId: deps.dbos.executorId });
63
+ const bootLog = logger.named("boot");
64
+ (await reconcileOrphanedRuns(pool)).match((runIds) => {
65
+ if (runIds.length > 0)
66
+ bootLog.warn("failed the runs whose workflow never started", { runIds });
67
+ }, (error) => bootLog.warn("the repair of the orphaned runs failed", bootLog.errorFields(error)));
68
+ bootLog.info("harness booted", { executorId: deps.dbos.executorId });
60
69
  const shutdown = (signal) => runShutdownSequence({
61
70
  signal,
62
71
  logger,
@@ -43,7 +43,7 @@ export interface SandboxClient {
43
43
  * Each spawn failure is an `err` value, the refusal of the label hook
44
44
  * (`labels_refused`) included: a spawn path splits the result with
45
45
  * `keepSuspendingRefusal`. Only a defect of the deployment config, or a
46
- * throw of the workspace-root resolver, throws, and it fails the workflow.
46
+ * rejection of the workspace-root resolver, throws, and it fails the workflow.
47
47
  */
48
48
  createSandbox(session: SpawnSession, spec: SandboxSpec, identity: SandboxIdentity): ResultAsync<SandboxRef, SandboxError>;
49
49
  /**
@@ -57,7 +57,7 @@ export async function precreateStepTree(deps, coords) {
57
57
  // `tailWritePrefix` (not a raw `join`) so every segment runs through the same
58
58
  // id validation the mount builders apply — a crafted tail cannot escape the
59
59
  // resolved root.
60
- const writeDir = tailWritePrefix({ workspaceRoot: deps.resolveWorkspaceRoot(coords.analysisId), tail: write.tail });
60
+ const writeDir = tailWritePrefix({ workspaceRoot: await deps.resolveWorkspaceRoot(coords.analysisId), tail: write.tail });
61
61
  await mkdir(writeDir, { recursive: true });
62
62
  await Promise.all(write.subdirs.map((sub) => mkdir(join(writeDir, sub), { recursive: true })));
63
63
  if (deps.stepTreeAccess === "world-writable") {
@@ -287,7 +287,7 @@ export function createDockerSandboxOps(config) {
287
287
  toolchainSource: config.toolchainSource,
288
288
  cache: cacheBindable,
289
289
  });
290
- const hostTreePath = config.resolveWorkspaceRoot(analysisId);
290
+ const hostTreePath = await config.resolveWorkspaceRoot(analysisId);
291
291
  // `tailWritePrefix` (not a raw `join`) so the RW bind source runs through
292
292
  // the same validation as the pre-created write tree — a crafted stepId or
293
293
  // a crafted tail cannot escape the resolved root into the container. The
@@ -88,11 +88,11 @@ function buildKubeApi() {
88
88
  * mount a directory the harness never wrote to — fail loudly instead. `createSandbox` runs
89
89
  * inside a DBOS workflow body, where a throw is the durable failure signal.
90
90
  */
91
- function workspaceSubPathFor(config, analysisId) {
91
+ async function workspaceSubPathFor(config, analysisId) {
92
92
  if (!config.sessionPvcRoot) {
93
93
  throw new Error("k8s sandbox: sessionPvc is set but sessionPvcRoot is not — the PVC subPath of a workspace root cannot be derived");
94
94
  }
95
- const rel = relativePath(config.sessionPvcRoot, config.resolveWorkspaceRoot(analysisId));
95
+ const rel = relativePath(config.sessionPvcRoot, await config.resolveWorkspaceRoot(analysisId));
96
96
  const posix = rel.split(sep).join("/");
97
97
  if (posix.length === 0 || posix.startsWith("..")) {
98
98
  throw new Error(`k8s sandbox: workspace root for ${analysisId} does not live under sessionPvcRoot (${config.sessionPvcRoot})`);
@@ -111,7 +111,7 @@ function pvcSubPath(value, what) {
111
111
  }
112
112
  return value;
113
113
  }
114
- function buildJobSpec(session, sandboxSpec, hostLabels, config, identity, farm, libsMounted) {
114
+ async function buildJobSpec(session, sandboxSpec, hostLabels, config, identity, farm, libsMounted) {
115
115
  const { sandboxId } = identity;
116
116
  // The lock gate of the package-store spec: `farmPath` is PVC-relative,
117
117
  // thus this host can prove the lock only through `libStorePvcRoot`. The
@@ -148,7 +148,7 @@ function buildJobSpec(session, sandboxSpec, hostLabels, config, identity, farm,
148
148
  const volumes = [];
149
149
  const volumeMounts = [];
150
150
  if (config.sessionPvc) {
151
- const subPaths = buildSessionSubPaths(coords, workspaceSubPathFor(config, session.scope.analysisId));
151
+ const subPaths = buildSessionSubPaths(coords, await workspaceSubPathFor(config, session.scope.analysisId));
152
152
  volumes.push({
153
153
  name: SESSION_VOLUME_NAME,
154
154
  persistentVolumeClaim: { claimName: config.sessionPvc },
@@ -491,7 +491,7 @@ export function createK8sSandboxOps(config) {
491
491
  .warn("the farm of the analysis has no usable inflexa.lock at sandbox creation — mounting no package store (sandbox degrades to available:false)", { libStorePvc: config.libStorePvc, farmPath: farm.farmPath, lockError: lock.error.type, sandboxId });
492
492
  }
493
493
  }
494
- const job = buildJobSpec(session, spec, hostLabels, config, identity, farm, libsMounted);
494
+ const job = await buildJobSpec(session, spec, hostLabels, config, identity, farm, libsMounted);
495
495
  const adopted = await createOrAdoptJob(batchApi, coreApi, config.namespace, sandboxId, spec.childWorkflowId, job);
496
496
  if (adopted.isErr())
497
497
  return err(adopted.error);
@@ -5,6 +5,7 @@
5
5
  * extraction/fold, and the chat-data-part narrowing a workflow body composes
6
6
  * into its emitters.
7
7
  */
8
+ import type { ActivityEntryPart, StepActivityPart, StepPhase, SynthesisPhase, SynthesisProgressPart } from "../contracts/chat-parts.js";
8
9
  import type { ChatDataPart, EmitFn } from "../loop/types.js";
9
10
  import type { ToolDetailResolver } from "../tools/detail-resolver.js";
10
11
  /**
@@ -19,6 +20,28 @@ import type { ToolDetailResolver } from "../tools/detail-resolver.js";
19
20
  * producers construct it identically.
20
21
  */
21
22
  export declare function stepPartId(kind: string, runId: string, stepId: string): string;
23
+ /**
24
+ * The two parts of one activity emission: the reconciling `data-step-activity` that holds the current activity,
25
+ * and the `data-activity-entry` that keeps the emission in the run stream. Both carry the same phase, activity,
26
+ * and time. `seq` is the position of the emission in the body of its workflow, so each entry has its own id, and a
27
+ * recovered body counts the same emissions again and gives the same ids. Every producer mints the two parts here.
28
+ */
29
+ export declare function activityEmission(frame: {
30
+ readonly runId: string;
31
+ readonly stepId: string;
32
+ }, seq: number, phase: StepPhase, activity: string, at: string): {
33
+ readonly activity: StepActivityPart;
34
+ readonly entry: ActivityEntryPart;
35
+ };
36
+ /**
37
+ * The two parts of one synthesis progress report: the reconciling `data-synthesis-progress` that holds the current
38
+ * phase, and the `data-activity-entry` that keeps the report in the run stream under the synthesis step. `seq` is the
39
+ * position of the report in the synthesis, so each entry has its own id. The `extra` fields stay on the progress part.
40
+ */
41
+ export declare function synthesisEmission(runId: string, stepId: string, seq: number, phase: SynthesisPhase, activity: string, at: string, extra: Pick<SynthesisProgressPart, "delegationCount" | "reason" | "error">): {
42
+ readonly progress: SynthesisProgressPart;
43
+ readonly entry: ActivityEntryPart;
44
+ };
22
45
  /**
23
46
  * The live-activity phrase for one tool call: the tool's name, then its own
24
47
  * description of this call when it has one.
@@ -19,6 +19,30 @@
19
19
  export function stepPartId(kind, runId, stepId) {
20
20
  return `${kind}-${runId}-${stepId}`;
21
21
  }
22
+ /**
23
+ * The two parts of one activity emission: the reconciling `data-step-activity` that holds the current activity,
24
+ * and the `data-activity-entry` that keeps the emission in the run stream. Both carry the same phase, activity,
25
+ * and time. `seq` is the position of the emission in the body of its workflow, so each entry has its own id, and a
26
+ * recovered body counts the same emissions again and gives the same ids. Every producer mints the two parts here.
27
+ */
28
+ export function activityEmission(frame, seq, phase, activity, at) {
29
+ const { runId, stepId } = frame;
30
+ return {
31
+ activity: { type: "data-step-activity", id: stepPartId("step-activity", runId, stepId), runId, stepId, phase, activity, at },
32
+ entry: { type: "data-activity-entry", id: `${stepPartId("activity-entry", runId, stepId)}-${seq}`, runId, stepId, phase, activity, at },
33
+ };
34
+ }
35
+ /**
36
+ * The two parts of one synthesis progress report: the reconciling `data-synthesis-progress` that holds the current
37
+ * phase, and the `data-activity-entry` that keeps the report in the run stream under the synthesis step. `seq` is the
38
+ * position of the report in the synthesis, so each entry has its own id. The `extra` fields stay on the progress part.
39
+ */
40
+ export function synthesisEmission(runId, stepId, seq, phase, activity, at, extra) {
41
+ return {
42
+ progress: { type: "data-synthesis-progress", id: `synthesis-progress-${runId}`, runId, phase, activity, at, ...extra },
43
+ entry: { type: "data-activity-entry", id: `${stepPartId("activity-entry", runId, stepId)}-${seq}`, runId, stepId, phase, activity, at },
44
+ };
45
+ }
22
46
  /**
23
47
  * The live-activity phrase for one tool call: the tool's name, then its own
24
48
  * description of this call when it has one.
@@ -30,5 +30,5 @@ export declare const ArtifactManifestEntrySchema: z.ZodObject<{
30
30
  hash: z.ZodOptional<z.ZodString>;
31
31
  }, z.core.$strip>;
32
32
  export type ArtifactManifestEntry = z.infer<typeof ArtifactManifestEntrySchema>;
33
- /** Infer artifact type from a file path based on its parent subdirectory. */
33
+ /** Infer artifact type from a file path. A folder segment at the start of the path or after a `/` gives its type. */
34
34
  export declare function inferArtifactType(filePath: string): ArtifactType;
@@ -22,15 +22,17 @@ export const ArtifactManifestEntrySchema = z.object({
22
22
  /** SHA-256 content hash for integrity verification (e.g., sha256:abcdef...). */
23
23
  hash: z.string().optional(),
24
24
  });
25
- /** Infer artifact type from a file path based on its parent subdirectory. */
25
+ const FOLDER_TYPES = [
26
+ ["figures", "figure"],
27
+ ["scripts", "script"],
28
+ ["logs", "log"],
29
+ ["notebooks", "notebook"],
30
+ ];
31
+ /** Infer artifact type from a file path. A folder segment at the start of the path or after a `/` gives its type. */
26
32
  export function inferArtifactType(filePath) {
27
- if (filePath.includes("/figures/"))
28
- return "figure";
29
- if (filePath.includes("/scripts/"))
30
- return "script";
31
- if (filePath.includes("/logs/"))
32
- return "log";
33
- if (filePath.includes("/notebooks/"))
34
- return "notebook";
33
+ for (const [folder, type] of FOLDER_TYPES) {
34
+ if (filePath.startsWith(`${folder}/`) || filePath.includes(`/${folder}/`))
35
+ return type;
36
+ }
35
37
  return "output";
36
38
  }
@@ -11,13 +11,8 @@ export declare const PlanStepSchema: z.ZodObject<{
11
11
  context: z.ZodOptional<z.ZodString>;
12
12
  description: z.ZodOptional<z.ZodString>;
13
13
  id: z.ZodString;
14
- constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
15
- track: z.ZodString;
16
- step_type: z.ZodString;
17
- question: z.ZodString;
18
- acceptance_criteria: z.ZodArray<z.ZodString>;
19
14
  caveats: z.ZodOptional<z.ZodArray<z.ZodString>>;
20
- packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
15
+ question: z.ZodString;
21
16
  depends_on: z.ZodArray<z.ZodString>;
22
17
  resources: z.ZodNonOptional<z.ZodOptional<z.ZodObject<{
23
18
  cpu: z.ZodNumber;
@@ -26,6 +21,11 @@ export declare const PlanStepSchema: z.ZodObject<{
26
21
  count: z.ZodNumber;
27
22
  }, z.core.$strip>>;
28
23
  }, z.core.$strip>>>;
24
+ track: z.ZodString;
25
+ step_type: z.ZodString;
26
+ acceptance_criteria: z.ZodArray<z.ZodString>;
27
+ constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
28
+ packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
29
29
  agent: z.ZodEnum<{
30
30
  [x: string]: string;
31
31
  }>;
@@ -44,13 +44,8 @@ export declare const PlannerPlanSchema: z.ZodObject<{
44
44
  context: z.ZodOptional<z.ZodString>;
45
45
  description: z.ZodOptional<z.ZodString>;
46
46
  id: z.ZodString;
47
- constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
48
- track: z.ZodString;
49
- step_type: z.ZodString;
50
- question: z.ZodString;
51
- acceptance_criteria: z.ZodArray<z.ZodString>;
52
47
  caveats: z.ZodOptional<z.ZodArray<z.ZodString>>;
53
- packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
48
+ question: z.ZodString;
54
49
  depends_on: z.ZodArray<z.ZodString>;
55
50
  resources: z.ZodNonOptional<z.ZodOptional<z.ZodObject<{
56
51
  cpu: z.ZodNumber;
@@ -59,6 +54,11 @@ export declare const PlannerPlanSchema: z.ZodObject<{
59
54
  count: z.ZodNumber;
60
55
  }, z.core.$strip>>;
61
56
  }, z.core.$strip>>>;
57
+ track: z.ZodString;
58
+ step_type: z.ZodString;
59
+ acceptance_criteria: z.ZodArray<z.ZodString>;
60
+ constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
61
+ packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
62
62
  agent: z.ZodEnum<{
63
63
  [x: string]: string;
64
64
  }>;
@@ -86,13 +86,8 @@ export declare const PlanningAgentOutputSchema: z.ZodObject<{
86
86
  context: z.ZodOptional<z.ZodString>;
87
87
  description: z.ZodOptional<z.ZodString>;
88
88
  id: z.ZodString;
89
- constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
90
- track: z.ZodString;
91
- step_type: z.ZodString;
92
- question: z.ZodString;
93
- acceptance_criteria: z.ZodArray<z.ZodString>;
94
89
  caveats: z.ZodOptional<z.ZodArray<z.ZodString>>;
95
- packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
90
+ question: z.ZodString;
96
91
  depends_on: z.ZodArray<z.ZodString>;
97
92
  resources: z.ZodNonOptional<z.ZodOptional<z.ZodObject<{
98
93
  cpu: z.ZodNumber;
@@ -101,6 +96,11 @@ export declare const PlanningAgentOutputSchema: z.ZodObject<{
101
96
  count: z.ZodNumber;
102
97
  }, z.core.$strip>>;
103
98
  }, z.core.$strip>>>;
99
+ track: z.ZodString;
100
+ step_type: z.ZodString;
101
+ acceptance_criteria: z.ZodArray<z.ZodString>;
102
+ constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
103
+ packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
104
104
  agent: z.ZodEnum<{
105
105
  [x: string]: string;
106
106
  }>;
@@ -31,11 +31,11 @@ export declare const SynthesizedFindingSchema: z.ZodObject<{
31
31
  high: "high";
32
32
  }>;
33
33
  noveltyStatus: z.ZodEnum<{
34
- expected: "expected";
35
34
  novel: "novel";
36
35
  confirmed: "confirmed";
37
36
  partially_confirmed: "partially_confirmed";
38
37
  contradicted: "contradicted";
38
+ expected: "expected";
39
39
  }>;
40
40
  literatureInterpretation: z.ZodString;
41
41
  references: z.ZodArray<z.ZodObject<{
@@ -97,11 +97,11 @@ export declare const RunSynthesisSchema: z.ZodObject<{
97
97
  high: "high";
98
98
  }>;
99
99
  noveltyStatus: z.ZodEnum<{
100
- expected: "expected";
101
100
  novel: "novel";
102
101
  confirmed: "confirmed";
103
102
  partially_confirmed: "partially_confirmed";
104
103
  contradicted: "contradicted";
104
+ expected: "expected";
105
105
  }>;
106
106
  literatureInterpretation: z.ZodString;
107
107
  references: z.ZodArray<z.ZodObject<{
@@ -1,11 +1,44 @@
1
1
  /**
2
- * Cross-entity aggregate reads used by `routes/analyses-metrics.ts` to build
3
- * the per-analysis metrics view. Each query touches one table; the view is
4
- * the batch shape callers want.
2
+ * Cross-entity aggregate reads for a list of analyses. Each read takes the ids
3
+ * of a page and sends one statement, whatever the length of the list: an
4
+ * embedder builds the per-analysis summary of a list from these reads, never
5
+ * from one read for each analysis.
5
6
  */
6
7
  import { type ResultAsync } from "neverthrow";
8
+ import type { DataProfileLifecycleStatus } from "../contracts/data-profile.js";
7
9
  import { type DbError } from "../lib/db-result.js";
8
10
  import type { Querier } from "./db.js";
11
+ import type { RunStatus, TerminalRunStatus } from "./schema.js";
12
+ /** The newest run of one analysis, with the title of its plan and the counts of its step rows. */
13
+ export interface LatestRunSummary {
14
+ readonly analysisId: string;
15
+ readonly runId: string;
16
+ readonly status: RunStatus;
17
+ readonly planTitle: string | null;
18
+ readonly doneSteps: number;
19
+ readonly totalSteps: number;
20
+ readonly startedAt: string;
21
+ readonly completedAt: string | null;
22
+ }
23
+ /** A run of a list of run ids, with the fields of the newest-run read and the thread of the run. */
24
+ export interface RunProgressSummary extends LatestRunSummary {
25
+ /** The thread of the run as the ledger stores it, or `null`. */
26
+ readonly threadId: string | null;
27
+ }
28
+ /** A terminal run that ended at or after a time. */
29
+ export interface EndedRunSummary {
30
+ readonly analysisId: string;
31
+ readonly runId: string;
32
+ readonly status: TerminalRunStatus;
33
+ readonly completedAt: string;
34
+ }
35
+ /** The data profile state of one analysis that has a profile status. */
36
+ export interface DataProfileSummary {
37
+ readonly analysisId: string;
38
+ readonly status: DataProfileLifecycleStatus;
39
+ readonly completedAt: string | null;
40
+ readonly profiledFiles: number | null;
41
+ }
9
42
  export declare function queryRunCountsByAnalyses(pool: Querier, analysisIds: string[]): ResultAsync<Array<{
10
43
  analysisId: string;
11
44
  status: string;
@@ -15,7 +48,46 @@ export declare function queryThreadCountsByAnalyses(pool: Querier, analysisIds:
15
48
  analysisId: string;
16
49
  count: number;
17
50
  }>, DbError>;
18
- export declare function queryDataProfileStatusByAnalyses(pool: Querier, analysisIds: string[]): ResultAsync<Array<{
19
- analysisId: string;
20
- status: string | null;
21
- }>, DbError>;
51
+ /**
52
+ * The newest run of each analysis of a list, in one statement. An analysis with
53
+ * no run has no entry. The newest run has the latest `started_at`, and `run_id`
54
+ * breaks a tie in descending order. The `LATERAL` subquery reads
55
+ * `idx_cortex_runs_analysis_started`, thus one index entry for each analysis
56
+ * and not a sort of the run history.
57
+ *
58
+ * The plan joins on `plan_id` and `analysis_id`: a plan of a different analysis
59
+ * gives no title, the scope rule of `loadPlan`. The statement gives the title
60
+ * only when it is a string, and the row mapping trims it with `trim()`, the
61
+ * rule of the cli `planTitleOf`. A blank title or a title that is not a string
62
+ * reads as `null`. The step counts
63
+ * cover the ledger rows of the selected run only, the reserved synthesis row
64
+ * included.
65
+ */
66
+ export declare function queryLatestRunsByAnalyses(pool: Querier, analysisIds: string[]): ResultAsync<LatestRunSummary[], DbError>;
67
+ /**
68
+ * The progress of each run of a list of run ids, in one statement. An id with
69
+ * no run row has no entry. The entries come newest first by `started_at`, and
70
+ * `run_id` breaks a tie in descending order. The plan title and the step counts
71
+ * follow the rules of `queryLatestRunsByAnalyses`, and each entry adds the
72
+ * thread of the run.
73
+ */
74
+ export declare function queryRunProgressByRuns(pool: Querier, runIds: string[]): ResultAsync<RunProgressSummary[], DbError>;
75
+ /**
76
+ * The newest `limit` terminal runs of every analysis that ended at or after
77
+ * `since`, newest completion first, with `run_id` breaking a tie in descending
78
+ * order. `since` is in the form of `toISOString()`, the form of each
79
+ * `completed_at` stamp, thus the text comparison is the time comparison. The
80
+ * statement walks `idx_cortex_runs_completed`.
81
+ */
82
+ export declare function queryRunsEndedSince(pool: Querier, input: {
83
+ since: string;
84
+ limit: number;
85
+ }): ResultAsync<EndedRunSummary[], DbError>;
86
+ /**
87
+ * The profile state of each analysis of a list, in one statement. An analysis
88
+ * with no ledger row, or with a cleared row whose status is null, has no entry:
89
+ * the same collapsed absence as `loadDataProfileStatus`. The statement selects
90
+ * only the three parts of the stored result that the file count reads, and the
91
+ * count itself is `profileDatasetFileCount`.
92
+ */
93
+ export declare function queryDataProfileStatusByAnalyses(pool: Querier, analysisIds: string[]): ResultAsync<DataProfileSummary[], DbError>;