@inflexa-ai/harness 0.48.0 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -190,8 +190,8 @@ export { planGraphOf } from "./workflows/plan/graph.js";
190
190
  export { planPartOf } from "./memory/card-builders.js";
191
191
  export { validatePlan } from "./schemas/validate-plan.js";
192
192
  export type { ValidationResult } from "./schemas/validate-plan.js";
193
- export { upsertPlan, loadPlan, loadPlanRecord } from "./state/plans.js";
194
- export type { UpsertPlanInput, PlanRecord } from "./state/plans.js";
193
+ export { upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./state/plans.js";
194
+ export type { UpsertPlanInput, PlanRecord, PlanPage, PlanPageEntry } from "./state/plans.js";
195
195
  export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, markRunCanceledIfActive, queryRun, queryRunsByAnalysis, countRunsByAnalysis, queryRunIdsByPlan, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
196
196
  export type { InsertRunInput, RunReservation } from "./state/runs.js";
197
197
  export { queryStepsByRun, countStepsByRuns } from "./state/step-executions.js";
package/dist/index.js CHANGED
@@ -293,7 +293,7 @@ export { validatePlan } from "./schemas/validate-plan.js";
293
293
  // Plan + run + step ledgers around a trigger. `upsertPlan` takes a
294
294
  // caller-derived id and inserts-if-absent (the deterministic-id intake path);
295
295
  // the run-state calls mirror the chat trigger's reserve → dedup → status flow.
296
- export { upsertPlan, loadPlan, loadPlanRecord } from "./state/plans.js";
296
+ export { upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./state/plans.js";
297
297
  export { insertRun, reserveRunById, queryActiveRun, queryActiveRunsByAnalysis, updateRunStatus, markRunCanceledIfActive, queryRun, queryRunsByAnalysis, countRunsByAnalysis, queryRunIdsByPlan, RunDedupCollisionError, RunIdentityCollisionError, } from "./state/runs.js";
298
298
  // The step ledger reads of a run, and the step counts of each run of a list in one statement.
299
299
  export { queryStepsByRun, countStepsByRuns } from "./state/step-executions.js";
@@ -98,6 +98,12 @@ export type WorkflowSchedule = {
98
98
  * schedule of this list. A second call with the same name replaces the first.
99
99
  */
100
100
  export declare function scheduleWorkflow(schedule: WorkflowSchedule): void;
101
+ /**
102
+ * Store each schedule of {@link scheduleWorkflow} in the system database, and delete each stored schedule of this
103
+ * application that no code registers. DBOS only upserts, thus a schedule of a removed workflow fires on each tick and
104
+ * finds no function. Each schedule of the harness goes through `scheduleWorkflow`.
105
+ */
106
+ export declare function applyWorkflowSchedules(): Promise<void>;
101
107
  /**
102
108
  * Launch DBOS. Idempotent — a second call is a no-op so tests that drive
103
109
  * the harness twice (or accidentally double-import) don't re-launch.
@@ -130,6 +130,19 @@ const pendingSchedules = new Map();
130
130
  export function scheduleWorkflow(schedule) {
131
131
  pendingSchedules.set(schedule.scheduleName, schedule);
132
132
  }
133
+ /**
134
+ * Store each schedule of {@link scheduleWorkflow} in the system database, and delete each stored schedule of this
135
+ * application that no code registers. DBOS only upserts, thus a schedule of a removed workflow fires on each tick and
136
+ * finds no function. Each schedule of the harness goes through `scheduleWorkflow`.
137
+ */
138
+ export async function applyWorkflowSchedules() {
139
+ if (pendingSchedules.size > 0)
140
+ await DBOS.applySchedules([...pendingSchedules.values()]);
141
+ for (const stored of await DBOS.listSchedules()) {
142
+ if (!pendingSchedules.has(stored.scheduleName))
143
+ await DBOS.deleteSchedule(stored.scheduleName);
144
+ }
145
+ }
133
146
  let resultRecipesRegistered = false;
134
147
  /**
135
148
  * DBOS refuses a registration after the launch, and the registry of SuperJSON
@@ -160,8 +173,7 @@ export async function launchDbos({ config, logger: injected }) {
160
173
  registerResultSerialization();
161
174
  const start = performance.now();
162
175
  await DBOS.launch();
163
- if (pendingSchedules.size > 0)
164
- await DBOS.applySchedules([...pendingSchedules.values()]);
176
+ await applyWorkflowSchedules();
165
177
  state.launched = true;
166
178
  state.recoveryStarted = true;
167
179
  logger.info("launched", {
@@ -86,6 +86,8 @@ export type ArtifactPageQuery = {
86
86
  q?: string;
87
87
  /** The whole path, with case. It selects the one row of that path. */
88
88
  path?: string;
89
+ /** The role of the row. `step_output` selects the rows that the grouped run count of {@link countArtifactsByRuns} counts. */
90
+ role?: ArtifactRole;
89
91
  /**
90
92
  * Extensions in lower case, with no dot. A row is selected when the extension of its path is a member. A row with
91
93
  * no extension never matches, and an empty list selects each row. The read compares the members as they are.
@@ -125,7 +127,8 @@ export type ArtifactPage = {
125
127
  * select, grouped by extension, and the page. The count is a separate statement because a page past the end has no
126
128
  * row to carry it. `total` is the sum of the groups.
127
129
  *
128
- * Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included. The cost
130
+ * Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included, unless the `role`
131
+ * filter selects one role. `total` and `extensionCounts` follow the filters. The cost
129
132
  * grows with the ledger of the one analysis, because the count reads each filtered row.
130
133
  */
131
134
  export declare function queryArtifactPage(pool: Querier, analysisId: string, query: ArtifactPageQuery): ResultAsync<ArtifactPage, DbError>;
@@ -193,7 +193,8 @@ const EXTENSION_SQL = `lower(substring(regexp_replace(path, '^.*/', '') from '^.
193
193
  * select, grouped by extension, and the page. The count is a separate statement because a page past the end has no
194
194
  * row to carry it. `total` is the sum of the groups.
195
195
  *
196
- * Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included. The cost
196
+ * Every row of the analysis is a member, an `input` row and a row with `unrecoverable_at` included, unless the `role`
197
+ * filter selects one role. `total` and `extensionCounts` follow the filters. The cost
197
198
  * grows with the ledger of the one analysis, because the count reads each filtered row.
198
199
  */
199
200
  export function queryArtifactPage(pool, analysisId, query) {
@@ -215,6 +216,8 @@ export function queryArtifactPage(pool, analysisId, query) {
215
216
  conditions.push(`strpos(lower(path), lower(${bind(query.q)}::text)) > 0`);
216
217
  if (query.path !== undefined)
217
218
  conditions.push(`path = ${bind(query.path)}`);
219
+ if (query.role !== undefined)
220
+ conditions.push(`role = ${bind(query.role)}`);
218
221
  if (query.extensions !== undefined && query.extensions.length > 0) {
219
222
  conditions.push(`${EXTENSION_SQL} = ANY(${bind([...query.extensions])}::text[])`);
220
223
  }
@@ -16,8 +16,8 @@ setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis
16
16
  export type { InsertRunInput, RunPage, RunReservation } from "./runs.js";
17
17
  export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun, countStepsByRuns, } from "./step-executions.js";
18
18
  export type { InsertStepExecutionInput, RunStepCounts, SeedStepExecutionRow, UpdateStepExecutionInput } from "./step-executions.js";
19
- export { insertPlan, upsertPlan, loadPlan, loadPlanRecord } from "./plans.js";
20
- export type { InsertPlanInput, UpsertPlanInput, PlanRecord, PlanRowQuerier } from "./plans.js";
19
+ export { insertPlan, upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./plans.js";
20
+ export type { InsertPlanInput, UpsertPlanInput, PlanRecord, PlanRowQuerier, PlanPage, PlanPageEntry } from "./plans.js";
21
21
  export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
22
22
  export type { DataProfileStatus, DataProfileTerminalWrite, DataProfileResult, DataProfileChecked, DataProfileCompanionCompleteness, DataProfileDimension, DataProfileGroup, DataProfileGroupSlot, DataProfileFile, DataProfileMemberAnnotation, DataProfileObservation, DataProfilePartition, DataProfileProbeReport, DataProfileQualityAssessment, DataProfileQuarantine, DataProfileRecipeStep, DataProfileKind, DataProfileAxis, DataProfileInputSignature, DataProfileCoverage, } from "./data-profile.js";
23
23
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses, queryLatestRunsByAnalyses, queryRunProgressByRuns, queryRunsEndedSince, } from "./analyses-metrics.js";
@@ -12,7 +12,7 @@ export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifact
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
14
  export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun, countStepsByRuns, } from "./step-executions.js";
15
- export { insertPlan, upsertPlan, loadPlan, loadPlanRecord } from "./plans.js";
15
+ export { insertPlan, upsertPlan, loadPlan, loadPlanRecord, queryPlanPage } from "./plans.js";
16
16
  export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, completeEmptyDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
17
17
  export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses, queryLatestRunsByAnalyses, queryRunProgressByRuns, queryRunsEndedSince, } from "./analyses-metrics.js";
18
18
  // Not per-entity: one analysis's whole Postgres footprint, reached from its id
@@ -94,3 +94,30 @@ export type PlanRecord = {
94
94
  export declare function loadPlanRecord(pool: PlanRowQuerier, planId: string, opts: {
95
95
  analysisId: string;
96
96
  }): ResultAsync<PlanRecord | null, DbError>;
97
+ /** One plan of {@link queryPlanPage}. */
98
+ export type PlanPageEntry = {
99
+ planId: string;
100
+ createdAt: string;
101
+ /** The `title` of the stored document when it is a string, as the document holds it, else `null`. */
102
+ title: string | null;
103
+ parentPlanId: string | null;
104
+ /** The newest plan of the same analysis that names this plan as its parent. */
105
+ supersededBy: string | null;
106
+ /** The newest run of the plan, in the order of {@link queryRunIdsByPlan}, or `null` for a plan with no run. */
107
+ newestRunId: string | null;
108
+ };
109
+ /** One page of the plans of an analysis, and the exact count of them. */
110
+ export type PlanPage = {
111
+ entries: PlanPageEntry[];
112
+ total: number;
113
+ };
114
+ /**
115
+ * One page of the plans of an analysis, newest first, in two statements: the count of the plans and the page. The
116
+ * count is a separate statement because a page past the end has no row to carry it. The order is the one of
117
+ * `supersededBy`: the creation time, then the highest plan id. The page gives the title of the document, not the
118
+ * document, because a page holds up to 200 plans.
119
+ */
120
+ export declare function queryPlanPage(pool: Querier, analysisId: string, opts: {
121
+ limit: number;
122
+ offset: number;
123
+ }): ResultAsync<PlanPage, DbError>;
@@ -117,6 +117,13 @@ const PlanRecordRowSchema = z.object({
117
117
  created_at: z.string(),
118
118
  superseded_by: z.string().nullable(),
119
119
  });
120
+ /**
121
+ * The `superseded_by` column of a plan read: the newest plan of the same analysis that names the plan as its parent.
122
+ * The text refers to the plan row of the outer query as `p`.
123
+ */
124
+ const SUPERSEDED_BY_SQL = `(SELECT c.plan_id FROM cortex_plans c
125
+ WHERE c.parent_plan_id = p.plan_id AND c.analysis_id = p.analysis_id
126
+ ORDER BY c.created_at DESC, c.plan_id DESC LIMIT 1) AS superseded_by`;
120
127
  /**
121
128
  * Analysis-scoped read of one plan with its parent and the newest plan that
122
129
  * replaced it, in one statement. Returns `ok(null)` for an unknown planId OR a
@@ -129,9 +136,7 @@ export function loadPlanRecord(pool, planId, opts) {
129
136
  return tryQuery("plans.loadPlanRecord", async () => {
130
137
  const result = await pool.query({
131
138
  text: `SELECT p.plan, p.parent_plan_id, p.created_at,
132
- (SELECT c.plan_id FROM cortex_plans c
133
- WHERE c.parent_plan_id = p.plan_id AND c.analysis_id = p.analysis_id
134
- ORDER BY c.created_at DESC, c.plan_id DESC LIMIT 1) AS superseded_by
139
+ ${SUPERSEDED_BY_SQL}
135
140
  FROM cortex_plans p
136
141
  WHERE p.plan_id = $1 AND p.analysis_id = $2`,
137
142
  values: [planId, opts.analysisId],
@@ -143,3 +148,52 @@ export function loadPlanRecord(pool, planId, opts) {
143
148
  return { planId, plan: row.plan, parentPlanId: row.parent_plan_id, supersededBy: row.superseded_by, createdAt: row.created_at };
144
149
  });
145
150
  }
151
+ const PlanPageRowSchema = z.object({
152
+ plan_id: z.string(),
153
+ created_at: z.string(),
154
+ title: z.string().nullable(),
155
+ parent_plan_id: z.string().nullable(),
156
+ superseded_by: z.string().nullable(),
157
+ newest_run_id: z.string().nullable(),
158
+ });
159
+ /**
160
+ * One page of the plans of an analysis, newest first, in two statements: the count of the plans and the page. The
161
+ * count is a separate statement because a page past the end has no row to carry it. The order is the one of
162
+ * `supersededBy`: the creation time, then the highest plan id. The page gives the title of the document, not the
163
+ * document, because a page holds up to 200 plans.
164
+ */
165
+ export function queryPlanPage(pool, analysisId, opts) {
166
+ return tryQuery("plans.queryPlanPage", async () => {
167
+ const countResult = await pool.query({
168
+ text: "SELECT COUNT(*)::int AS total FROM cortex_plans WHERE analysis_id = $1",
169
+ values: [analysisId],
170
+ });
171
+ const rowsResult = await pool.query({
172
+ text: `SELECT p.plan_id, p.created_at, p.parent_plan_id,
173
+ CASE WHEN jsonb_typeof(p.plan->'title') = 'string' THEN p.plan->>'title' END AS title,
174
+ ${SUPERSEDED_BY_SQL},
175
+ (SELECT r.run_id FROM cortex_runs r
176
+ WHERE r.analysis_id = p.analysis_id AND r.plan_id = p.plan_id
177
+ ORDER BY r.started_at DESC, r.run_id DESC LIMIT 1) AS newest_run_id
178
+ FROM cortex_plans p
179
+ WHERE p.analysis_id = $1
180
+ ORDER BY p.created_at DESC, p.plan_id DESC
181
+ LIMIT $2 OFFSET $3`,
182
+ values: [analysisId, opts.limit, opts.offset],
183
+ });
184
+ return {
185
+ entries: rowsResult.rows.map((raw) => {
186
+ const row = PlanPageRowSchema.parse(raw);
187
+ return {
188
+ planId: row.plan_id,
189
+ createdAt: row.created_at,
190
+ title: row.title,
191
+ parentPlanId: row.parent_plan_id,
192
+ supersededBy: row.superseded_by,
193
+ newestRunId: row.newest_run_id,
194
+ };
195
+ }),
196
+ total: countResult.rows[0]?.total ?? 0,
197
+ };
198
+ });
199
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.48.0",
3
+ "version": "0.48.1",
4
4
  "description": "Host-agnostic bioinformatics agent harness — an agent loop on the AI SDK, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",