@inflexa-ai/harness 0.39.0 → 0.39.2
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/agents/system-prompt.d.ts +3 -4
- package/dist/agents/system-prompt.js +1 -1
- package/dist/execution/artifact-registration.d.ts +12 -2
- package/dist/execution/artifact-registration.js +9 -4
- package/dist/execution/post-step-pipeline.d.ts +3 -6
- package/dist/execution/post-step-pipeline.js +11 -3
- package/dist/execution/step-summary.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/loop/metrics.js +1 -1
- package/dist/loop/run-agent.d.ts +2 -2
- package/dist/loop/run-agent.js +6 -8
- package/dist/loop/types.d.ts +1 -1
- package/dist/prompts/SOUL.d.ts +3 -3
- package/dist/prompts/SOUL.js +2 -16
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +14 -24
- package/dist/prompts/execute-analysis/step-summary.js +3 -4
- package/dist/prompts/planner.js +44 -45
- package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
- package/dist/prompts/sandbox/scientific-executor.js +5 -6
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +6 -7
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +9 -13
- package/dist/providers/types.d.ts +5 -4
- package/dist/report-model/pin-snapshot.js +4 -10
- package/dist/state/artifacts.d.ts +11 -9
- package/dist/state/artifacts.js +112 -93
- package/dist/state/data-profile.d.ts +2 -2
- package/dist/state/data-profile.js +2 -2
- package/dist/tasks/data-profile.d.ts +28 -10
- package/dist/tasks/data-profile.js +114 -95
- package/dist/tools/ad-hoc-router.js +2 -1
- package/dist/tools/research/generate-analogy-report.js +2 -4
- package/dist/tools/research/generate-plan.js +7 -8
- package/dist/tools/sandbox/list-available-refs.js +1 -2
- package/dist/workflows/execute-analysis.js +5 -3
- package/package.json +1 -1
package/dist/state/artifacts.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* `cortex_artifacts` operations — artifact upsert, lookup, and file-id
|
|
3
3
|
* reconciliation.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
import { tryMutation, tryQuery } from "../lib/db-result.js";
|
|
6
|
+
export function upsertArtifact(pool, entry) {
|
|
6
7
|
const now = new Date().toISOString();
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
return tryMutation("artifacts.upsertArtifact", async () => {
|
|
9
|
+
await pool.query({
|
|
10
|
+
text: `INSERT INTO cortex_artifacts
|
|
9
11
|
(analysis_id, path, hash, size, role, source_step, source_run, file_type, file_id, created_at)
|
|
10
12
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
11
13
|
ON CONFLICT (analysis_id, path) DO UPDATE SET
|
|
@@ -16,18 +18,19 @@ export async function upsertArtifact(pool, entry) {
|
|
|
16
18
|
source_run = EXCLUDED.source_run,
|
|
17
19
|
file_type = COALESCE(EXCLUDED.file_type, cortex_artifacts.file_type),
|
|
18
20
|
file_id = COALESCE(EXCLUDED.file_id, cortex_artifacts.file_id)`,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
values: [
|
|
22
|
+
entry.resourceId,
|
|
23
|
+
entry.path,
|
|
24
|
+
entry.hash,
|
|
25
|
+
entry.size,
|
|
26
|
+
entry.role,
|
|
27
|
+
entry.sourceStep ?? null,
|
|
28
|
+
entry.sourceRun ?? null,
|
|
29
|
+
entry.fileType ?? null,
|
|
30
|
+
entry.fileId ?? null,
|
|
31
|
+
now,
|
|
32
|
+
],
|
|
33
|
+
});
|
|
31
34
|
});
|
|
32
35
|
}
|
|
33
36
|
const COLS_PER_ROW = 10;
|
|
@@ -49,31 +52,32 @@ const ROWS_PER_STATEMENT = 1_000;
|
|
|
49
52
|
* idempotent per row (`ON CONFLICT ... DO UPDATE`), so a failure between
|
|
50
53
|
* chunks is healed by the caller's retry re-upserting the same manifest.
|
|
51
54
|
*/
|
|
52
|
-
export
|
|
55
|
+
export function upsertArtifacts(pool, entries) {
|
|
53
56
|
const now = new Date().toISOString();
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
e
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
57
|
+
return tryMutation("artifacts.upsertArtifacts", async () => {
|
|
58
|
+
for (let start = 0; start < entries.length; start += ROWS_PER_STATEMENT) {
|
|
59
|
+
const chunk = entries.slice(start, start + ROWS_PER_STATEMENT);
|
|
60
|
+
const placeholders = chunk
|
|
61
|
+
.map((_, i) => {
|
|
62
|
+
const base = i * COLS_PER_ROW;
|
|
63
|
+
return (`($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, ` +
|
|
64
|
+
`$${base + 5}, $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10})`);
|
|
65
|
+
})
|
|
66
|
+
.join(", ");
|
|
67
|
+
const values = chunk.flatMap((e) => [
|
|
68
|
+
e.resourceId,
|
|
69
|
+
e.path,
|
|
70
|
+
e.hash,
|
|
71
|
+
e.size,
|
|
72
|
+
e.role,
|
|
73
|
+
e.sourceStep ?? null,
|
|
74
|
+
e.sourceRun ?? null,
|
|
75
|
+
e.fileType ?? null,
|
|
76
|
+
e.fileId ?? null,
|
|
77
|
+
now,
|
|
78
|
+
]);
|
|
79
|
+
await pool.query({
|
|
80
|
+
text: `INSERT INTO cortex_artifacts
|
|
77
81
|
(analysis_id, path, hash, size, role, source_step, source_run, file_type, file_id, created_at)
|
|
78
82
|
VALUES ${placeholders}
|
|
79
83
|
ON CONFLICT (analysis_id, path) DO UPDATE SET
|
|
@@ -84,42 +88,47 @@ export async function upsertArtifacts(pool, entries) {
|
|
|
84
88
|
source_run = EXCLUDED.source_run,
|
|
85
89
|
file_type = COALESCE(EXCLUDED.file_type, cortex_artifacts.file_type),
|
|
86
90
|
file_id = COALESCE(EXCLUDED.file_id, cortex_artifacts.file_id)`,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
values,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
90
95
|
}
|
|
91
96
|
/**
|
|
92
97
|
* Query cortex_artifacts for input-role rows matching the given paths.
|
|
93
98
|
* Used at the registration boundary to resolve metadata for data-source inputs.
|
|
94
99
|
*/
|
|
95
|
-
export
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
export function queryInputArtifacts(pool, analysisId, paths) {
|
|
101
|
+
return tryQuery("artifacts.queryInputArtifacts", async () => {
|
|
102
|
+
if (paths.length === 0)
|
|
103
|
+
return [];
|
|
104
|
+
const result = await pool.query({
|
|
105
|
+
text: `SELECT path, hash, size, file_id
|
|
100
106
|
FROM cortex_artifacts
|
|
101
107
|
WHERE analysis_id = $1 AND role = 'input' AND path = ANY($2::text[])`,
|
|
102
|
-
|
|
108
|
+
values: [analysisId, paths],
|
|
109
|
+
});
|
|
110
|
+
return result.rows.map((r) => ({
|
|
111
|
+
path: r.path,
|
|
112
|
+
hash: r.hash,
|
|
113
|
+
size: Number(r.size),
|
|
114
|
+
fileId: r.file_id ?? null,
|
|
115
|
+
}));
|
|
103
116
|
});
|
|
104
|
-
return result.rows.map((r) => ({
|
|
105
|
-
path: r.path,
|
|
106
|
-
hash: r.hash,
|
|
107
|
-
size: Number(r.size),
|
|
108
|
-
fileId: r.file_id ?? null,
|
|
109
|
-
}));
|
|
110
117
|
}
|
|
111
|
-
export
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
export function queryUnsyncedStepArtifacts(pool, resourceId, runId, stepId) {
|
|
119
|
+
return tryQuery("artifacts.queryUnsyncedStepArtifacts", async () => {
|
|
120
|
+
const result = await pool.query({
|
|
121
|
+
text: `SELECT analysis_id, path, artifact_id, file_id, hash, size, role,
|
|
114
122
|
source_step, source_run, created_at, file_type
|
|
115
123
|
FROM cortex_artifacts
|
|
116
124
|
WHERE analysis_id = $1 AND source_run = $2 AND source_step = $3
|
|
117
125
|
AND artifact_id IS NOT NULL AND file_id IS NULL
|
|
118
126
|
AND role = 'step_output'
|
|
119
127
|
ORDER BY created_at`,
|
|
120
|
-
|
|
128
|
+
values: [resourceId, runId, stepId],
|
|
129
|
+
});
|
|
130
|
+
return result.rows.map(mapArtifactRow);
|
|
121
131
|
});
|
|
122
|
-
return result.rows.map(mapArtifactRow);
|
|
123
132
|
}
|
|
124
133
|
/**
|
|
125
134
|
* The artifacts a completed step registered, most consequential first. Ordered
|
|
@@ -127,11 +136,12 @@ export async function queryUnsyncedStepArtifacts(pool, resourceId, runId, stepId
|
|
|
127
136
|
* path, so a `limit` truncates the tail of the list rather than an arbitrary
|
|
128
137
|
* slice of it — and so the same call replays to the same rows.
|
|
129
138
|
*/
|
|
130
|
-
export
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
export function queryStepArtifactPaths(pool, resourceId, runId, stepId, limit) {
|
|
140
|
+
return tryQuery("artifacts.queryStepArtifactPaths", async () => {
|
|
141
|
+
if (limit <= 0)
|
|
142
|
+
return [];
|
|
143
|
+
const result = await pool.query({
|
|
144
|
+
text: `SELECT path, file_type
|
|
135
145
|
FROM cortex_artifacts
|
|
136
146
|
WHERE analysis_id = $1 AND source_run = $2 AND source_step = $3
|
|
137
147
|
AND role = 'step_output'
|
|
@@ -144,9 +154,10 @@ export async function queryStepArtifactPaths(pool, resourceId, runId, stepId, li
|
|
|
144
154
|
END,
|
|
145
155
|
path
|
|
146
156
|
LIMIT $4`,
|
|
147
|
-
|
|
157
|
+
values: [resourceId, runId, stepId, limit],
|
|
158
|
+
});
|
|
159
|
+
return result.rows.map((r) => ({ path: r.path, fileType: r.file_type ?? null }));
|
|
148
160
|
});
|
|
149
|
-
return result.rows.map((r) => ({ path: r.path, fileType: r.file_type ?? null }));
|
|
150
161
|
}
|
|
151
162
|
/**
|
|
152
163
|
* Every artifact of one analysis, ordered by path so the same call gives the same rows.
|
|
@@ -156,50 +167,58 @@ export async function queryStepArtifactPaths(pool, resourceId, runId, stepId, li
|
|
|
156
167
|
* query drops such a row, a later refusal says that the artifact never existed, and that reason is
|
|
157
168
|
* false.
|
|
158
169
|
*/
|
|
159
|
-
export
|
|
160
|
-
|
|
161
|
-
|
|
170
|
+
export function queryAnalysisArtifacts(pool, analysisId) {
|
|
171
|
+
return tryQuery("artifacts.queryAnalysisArtifacts", async () => {
|
|
172
|
+
const result = await pool.query({
|
|
173
|
+
text: `SELECT path, hash, file_type
|
|
162
174
|
FROM cortex_artifacts
|
|
163
175
|
WHERE analysis_id = $1
|
|
164
176
|
ORDER BY path`,
|
|
165
|
-
|
|
177
|
+
values: [analysisId],
|
|
178
|
+
});
|
|
179
|
+
return result.rows.map((r) => ({ path: r.path, hash: r.hash, fileType: r.file_type ?? null }));
|
|
166
180
|
});
|
|
167
|
-
return result.rows.map((r) => ({ path: r.path, hash: r.hash, fileType: r.file_type ?? null }));
|
|
168
181
|
}
|
|
169
182
|
/** Count step-output artifacts produced by a run — for the run-completed card. */
|
|
170
|
-
export
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
export function countArtifactsForRun(pool, analysisId, runId) {
|
|
184
|
+
return tryQuery("artifacts.countArtifactsForRun", async () => {
|
|
185
|
+
const result = await pool.query({
|
|
186
|
+
text: `SELECT COUNT(*)::text AS n
|
|
173
187
|
FROM cortex_artifacts
|
|
174
188
|
WHERE analysis_id = $1 AND source_run = $2 AND role = 'step_output'`,
|
|
175
|
-
|
|
189
|
+
values: [analysisId, runId],
|
|
190
|
+
});
|
|
191
|
+
return Number(result.rows[0]?.n ?? 0);
|
|
176
192
|
});
|
|
177
|
-
return Number(result.rows[0]?.n ?? 0);
|
|
178
193
|
}
|
|
179
|
-
export
|
|
180
|
-
|
|
181
|
-
|
|
194
|
+
export function updateArtifactId(pool, resourceId, path, artifactId, fileType) {
|
|
195
|
+
return tryMutation("artifacts.updateArtifactId", async () => {
|
|
196
|
+
await pool.query({
|
|
197
|
+
text: `UPDATE cortex_artifacts SET artifact_id = $1,
|
|
182
198
|
file_type = COALESCE($4, file_type)
|
|
183
199
|
WHERE analysis_id = $2 AND path = $3`,
|
|
184
|
-
|
|
200
|
+
values: [artifactId, resourceId, path, fileType ?? null],
|
|
201
|
+
});
|
|
185
202
|
});
|
|
186
203
|
}
|
|
187
|
-
export
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
204
|
+
export function updateFileIds(pool, pairs) {
|
|
205
|
+
return tryMutation("artifacts.updateFileIds", async () => {
|
|
206
|
+
if (pairs.length === 0)
|
|
207
|
+
return;
|
|
208
|
+
const values = [];
|
|
209
|
+
const tuples = [];
|
|
210
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
211
|
+
const base = i * 2;
|
|
212
|
+
tuples.push(`($${base + 1}::text, $${base + 2}::text)`);
|
|
213
|
+
values.push(pairs[i].artifactId, pairs[i].fileId);
|
|
214
|
+
}
|
|
215
|
+
await pool.query({
|
|
216
|
+
text: `UPDATE cortex_artifacts AS a
|
|
199
217
|
SET file_id = c.file_id
|
|
200
218
|
FROM (VALUES ${tuples.join(", ")}) AS c(artifact_id, file_id)
|
|
201
219
|
WHERE a.artifact_id = c.artifact_id`,
|
|
202
|
-
|
|
220
|
+
values,
|
|
221
|
+
});
|
|
203
222
|
});
|
|
204
223
|
}
|
|
205
224
|
function mapArtifactRow(row) {
|
|
@@ -146,9 +146,9 @@ export declare function expireStaleDataProfile(pool: Querier, analysisId: string
|
|
|
146
146
|
* Reset a ledger row wedged at `running` with no workflow behind it back to
|
|
147
147
|
* `failed`, so the normal retry path can re-profile it.
|
|
148
148
|
*
|
|
149
|
-
* A start that
|
|
149
|
+
* A start that fails after the CAS already flipped the row to `running`
|
|
150
150
|
* compensates itself (`triggerDataProfile`/`runDataProfile` fail the ledger in
|
|
151
|
-
* their
|
|
151
|
+
* their dispatch). This covers the residual case that compensation cannot: a host
|
|
152
152
|
* that dies in the window between the CAS and the `DBOS.startWorkflow` insert
|
|
153
153
|
* leaves a `running` row with no workflow for recovery to resume — nothing
|
|
154
154
|
* would ever move it off `running`, and every later trigger reports
|
|
@@ -225,9 +225,9 @@ export function expireStaleDataProfile(pool, analysisId, timeoutMs) {
|
|
|
225
225
|
* Reset a ledger row wedged at `running` with no workflow behind it back to
|
|
226
226
|
* `failed`, so the normal retry path can re-profile it.
|
|
227
227
|
*
|
|
228
|
-
* A start that
|
|
228
|
+
* A start that fails after the CAS already flipped the row to `running`
|
|
229
229
|
* compensates itself (`triggerDataProfile`/`runDataProfile` fail the ledger in
|
|
230
|
-
* their
|
|
230
|
+
* their dispatch). This covers the residual case that compensation cannot: a host
|
|
231
231
|
* that dies in the window between the CAS and the `DBOS.startWorkflow` insert
|
|
232
232
|
* leaves a `running` row with no workflow for recovery to resume — nothing
|
|
233
233
|
* would ever move it off `running`, and every later trigger reports
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* triggers dedup via the ledger CAS (only one `tryStart/tryRerun/tryRetry`
|
|
18
18
|
* UPDATE wins); each winning attempt starts a workflow under a per-attempt id.
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
20
|
+
import { ResultAsync } from "neverthrow";
|
|
21
21
|
import type { Pool } from "pg";
|
|
22
22
|
import { type AuthContext, type RunSession } from "../auth/types.js";
|
|
23
23
|
import type { EnvironmentStorePaths } from "../config/environment-stores.js";
|
|
@@ -26,6 +26,7 @@ import type { RunAuthorization, RunAuthorizer } from "../execution/run-authorize
|
|
|
26
26
|
import type { StagedInput } from "../execution/staged-input.js";
|
|
27
27
|
import type { SandboxAgentDeps } from "../agents/sandbox/shared.js";
|
|
28
28
|
import type { BioToolKeys } from "../tools/bio/keys.js";
|
|
29
|
+
import type { DbError } from "../lib/db-result.js";
|
|
29
30
|
import { type GateRefusal } from "../lib/hooks.js";
|
|
30
31
|
import type { Logger } from "../lib/logger.js";
|
|
31
32
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
@@ -149,9 +150,25 @@ export declare function runDataProfileBody(input: DataProfileWorkflowInput, deps
|
|
|
149
150
|
* - `"already_running"`: a workflow already owns the row, and nothing new ran.
|
|
150
151
|
* - `"completed"`: the analysis has no input files, so the row is `completed` at once
|
|
151
152
|
* with no result. No workflow ran (see {@link completeEmptyDataProfile}).
|
|
152
|
-
* - `"failed"`: the trigger refused the call
|
|
153
|
+
* - `"failed"`: the trigger refused the call, and the row is unchanged.
|
|
153
154
|
*/
|
|
154
155
|
export type DataProfileTriggerResult = "started" | "restarted" | "already_running" | "completed" | "failed";
|
|
156
|
+
/**
|
|
157
|
+
* Why the dispatch of a claimed profile started no workflow. The dispatch writes `failed` to the claimed
|
|
158
|
+
* row before a caller reads the `err`:
|
|
159
|
+
* - `refused`: `RunAuthorizer.authorize` gave an `err`. `reason` and `suspend` are the values of the host.
|
|
160
|
+
* - `start_failed`: DBOS did not start the workflow. `cause` is the throw of DBOS.
|
|
161
|
+
*/
|
|
162
|
+
export type DataProfileStartError = {
|
|
163
|
+
readonly type: "refused";
|
|
164
|
+
readonly reason: string;
|
|
165
|
+
readonly suspend: boolean;
|
|
166
|
+
} | {
|
|
167
|
+
readonly type: "start_failed";
|
|
168
|
+
readonly cause: unknown;
|
|
169
|
+
};
|
|
170
|
+
/** A one-line, user-facing description of a `DataProfileStartError`. */
|
|
171
|
+
export declare function describeDataProfileStartError(e: DataProfileStartError): string;
|
|
155
172
|
/**
|
|
156
173
|
* Route-side deps for triggering the data-profile workflow: the ledger pool,
|
|
157
174
|
* the run authorizer, and the registered workflow callable. The body's
|
|
@@ -213,16 +230,17 @@ export declare function dataProfileWorkflowId(analysisId: string, nonce: string)
|
|
|
213
230
|
* `"completed"`. An empty manifest against a seed that names files is a divergence
|
|
214
231
|
* between the caller and the ledger, and the trigger refuses it.
|
|
215
232
|
*
|
|
216
|
-
* Returns what happened, so the caller can surface it (e.g. in the seed response).
|
|
233
|
+
* Returns what happened, so the caller can surface it (e.g. in the seed response). A
|
|
234
|
+
* failed ledger read or claim is the `err`. The dispatch settles its own row, thus a
|
|
235
|
+
* refusal or a failed start after a claim shows on the ledger only.
|
|
217
236
|
*/
|
|
218
|
-
export declare function triggerDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams):
|
|
237
|
+
export declare function triggerDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams): ResultAsync<DataProfileTriggerResult, DbError>;
|
|
219
238
|
/**
|
|
220
239
|
* Start the data-profile workflow for an already-claimed analysis (the retry
|
|
221
240
|
* route claims via `tryRetryDataProfile`, then calls this). The caller does not
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* promise resolves: the row is where a caller reads the outcome.
|
|
241
|
+
* wait for the completion of the workflow. A refused authorization and a failed
|
|
242
|
+
* start each settle the row as `failed` before the `err` (see
|
|
243
|
+
* {@link dispatchClaimedProfile}), thus a caller that minted an authorization
|
|
244
|
+
* can revoke it.
|
|
227
245
|
*/
|
|
228
|
-
export declare function runDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams):
|
|
246
|
+
export declare function runDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams): ResultAsync<void, DataProfileStartError>;
|