@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.
- package/dist/agents/report-session-agent.d.ts +1 -1
- package/dist/app/approve-plan.d.ts +48 -0
- package/dist/app/approve-plan.js +84 -0
- package/dist/app/data-profile-view.d.ts +1 -1
- package/dist/app/run-activity.js +4 -6
- package/dist/app/synthesize-run.js +1 -1
- package/dist/contracts/chat-events.d.ts +2 -0
- package/dist/contracts/chat-frame.d.ts +7 -0
- package/dist/contracts/chat-frame.js +18 -4
- package/dist/contracts/chat-parts.d.ts +73 -2
- package/dist/contracts/chat-parts.js +3 -1
- package/dist/contracts/index.d.ts +3 -1
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/part-registry.js +3 -0
- package/dist/contracts/schemas/chat-events.d.ts +13 -0
- package/dist/contracts/schemas/chat-events.js +1 -0
- package/dist/contracts/schemas/chat-parts.d.ts +200 -15
- package/dist/contracts/schemas/chat-parts.js +63 -12
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/contracts/subagent-activity.d.ts +12 -0
- package/dist/contracts/subagent-activity.js +76 -0
- package/dist/execution/artifact-metadata.js +1 -3
- package/dist/execution/post-step-pipeline.js +1 -1
- package/dist/execution/run-canceler.js +1 -1
- package/dist/execution/run-event-parts.d.ts +6 -5
- package/dist/execution/run-event-parts.js +6 -5
- package/dist/execution/run-event-stream.d.ts +18 -6
- package/dist/execution/run-event-stream.js +67 -17
- package/dist/execution/run-synthesis.d.ts +21 -0
- package/dist/execution/run-synthesis.js +77 -3
- package/dist/index.d.ts +24 -10
- package/dist/index.js +17 -3
- package/dist/lib/hooks.d.ts +1 -1
- package/dist/lib/metrics.d.ts +2 -2
- package/dist/loop/run-agent.js +1 -0
- package/dist/loop/types.d.ts +2 -0
- package/dist/memory/card-builders.d.ts +11 -0
- package/dist/memory/card-builders.js +35 -21
- package/dist/memory/conversation-display-recorder.d.ts +2 -0
- package/dist/memory/conversation-display-recorder.js +10 -1
- package/dist/memory/conversation-display-storage.d.ts +9 -2
- package/dist/memory/conversation-display-storage.js +18 -3
- package/dist/memory/thread-history.d.ts +8 -1
- package/dist/memory/thread-history.js +13 -3
- package/dist/memory/thread-store.d.ts +13 -0
- package/dist/memory/thread-store.js +15 -1
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +3 -1
- package/dist/provenance/seam.d.ts +16 -5
- package/dist/providers/ai-sdk.js +6 -4
- package/dist/reference-data/receipt.d.ts +16 -4
- package/dist/reference-data/receipt.js +20 -3
- package/dist/report-model/pin-snapshot.d.ts +1 -1
- package/dist/report-model/pin-snapshot.js +4 -52
- package/dist/runtime/assemble.d.ts +9 -2
- package/dist/runtime/assemble.js +13 -2
- package/dist/runtime/boot.d.ts +3 -0
- package/dist/runtime/boot.js +10 -1
- package/dist/sandbox/client.d.ts +1 -1
- package/dist/sandbox/create-sandbox.js +1 -1
- package/dist/sandbox/docker-client.js +1 -1
- package/dist/sandbox/k8s-client.js +5 -5
- package/dist/sandbox/sandbox-step-translate.d.ts +23 -0
- package/dist/sandbox/sandbox-step-translate.js +24 -0
- package/dist/schemas/artifact-manifest.d.ts +1 -1
- package/dist/schemas/artifact-manifest.js +11 -9
- package/dist/schemas/plan-schemas.d.ts +18 -18
- package/dist/schemas/run-synthesis.d.ts +2 -2
- package/dist/state/analyses-metrics.d.ts +79 -7
- package/dist/state/analyses-metrics.js +141 -6
- package/dist/state/artifacts.d.ts +50 -3
- package/dist/state/artifacts.js +67 -7
- package/dist/state/data-profile.d.ts +4 -2
- package/dist/state/data-profile.js +7 -5
- package/dist/state/index.d.ts +9 -8
- package/dist/state/index.js +5 -5
- package/dist/state/migrations/20261012120000_index_the_runs_by_start.d.ts +8 -0
- package/dist/state/migrations/20261012120000_index_the_runs_by_start.js +12 -0
- package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.d.ts +8 -0
- package/dist/state/migrations/20261013120000_index_the_ended_runs_and_the_recent_threads.js +13 -0
- package/dist/state/migrations/20261013180000_narrow_the_active_run_index.d.ts +8 -0
- package/dist/state/migrations/20261013180000_narrow_the_active_run_index.js +13 -0
- package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.d.ts +8 -0
- package/dist/state/migrations/20261014120000_index_the_plans_by_parent_and_the_runs_by_plan.js +13 -0
- package/dist/state/migrations/index.js +8 -0
- package/dist/state/plans.d.ts +21 -0
- package/dist/state/plans.js +25 -0
- package/dist/state/runs.d.ts +36 -5
- package/dist/state/runs.js +65 -19
- package/dist/state/schema.d.ts +2 -0
- package/dist/tasks/data-profile-activity.d.ts +7 -1
- package/dist/tasks/data-profile-activity.js +18 -9
- package/dist/tasks/data-profile.d.ts +15 -1
- package/dist/tasks/data-profile.js +18 -13
- package/dist/tools/approval/gateway.js +28 -5
- package/dist/tools/display/show-user.d.ts +1 -1
- package/dist/tools/execute-analysis.d.ts +51 -0
- package/dist/tools/execute-analysis.js +241 -109
- package/dist/tools/report-session/derive-table.js +1 -1
- package/dist/tools/report-session/examine-page.js +1 -1
- package/dist/tools/report-session/list-artifacts.js +2 -2
- package/dist/tools/report-session/preview-report.d.ts +1 -1
- package/dist/tools/report-session/preview-report.js +6 -6
- package/dist/tools/report-session/record-version.d.ts +1 -1
- package/dist/tools/report-session/record-version.js +4 -4
- package/dist/tools/research/inspect-data-profile.d.ts +1 -1
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/tools/research/inspect-run.js +7 -7
- package/dist/tools/workspace/mutator.js +2 -2
- package/dist/workflows/execute-analysis.d.ts +4 -3
- package/dist/workflows/execute-analysis.js +38 -25
- package/dist/workflows/plan/graph.d.ts +16 -0
- package/dist/workflows/plan/graph.js +36 -0
- package/dist/workflows/sandbox-step.d.ts +2 -2
- package/dist/workflows/sandbox-step.js +16 -14
- package/dist/workspace/filesystem.js +18 -22
- package/dist/workspace/paths.d.ts +7 -4
- package/package.json +4 -4
package/dist/state/runs.d.ts
CHANGED
|
@@ -77,11 +77,33 @@ export declare function queryActiveRun(pool: Querier, analysisId: string, planId
|
|
|
77
77
|
* analysis's live concurrency, not by its history, and a run leaves the set permanently when it
|
|
78
78
|
* finishes.
|
|
79
79
|
*
|
|
80
|
-
* The status set is the one {@link queryActiveRun} uses — non-terminal means
|
|
81
|
-
*
|
|
82
|
-
*
|
|
80
|
+
* The status set is the one {@link queryActiveRun} uses — non-terminal means `running`. Keep the two
|
|
81
|
+
* in step: a new non-terminal status added to one and not the other makes a live run invisible to
|
|
82
|
+
* exactly one of its readers.
|
|
83
83
|
*/
|
|
84
84
|
export declare function queryActiveRunsByAnalysis(pool: Querier, analysisId: string): ResultAsync<CortexRunRow[], DbError>;
|
|
85
|
+
/**
|
|
86
|
+
* How long a reserved run may wait for its workflow before {@link reconcileOrphanedRuns} fails it. A launch writes
|
|
87
|
+
* the workflow row seconds after the reservation, and the margin covers a slow authorization gate of a managed host.
|
|
88
|
+
*/
|
|
89
|
+
export declare const ORPHANED_RUN_GRACE_MS: number;
|
|
90
|
+
/**
|
|
91
|
+
* Fail each `running` run that started before the grace and that has no DBOS workflow row, and give the ids of
|
|
92
|
+
* the runs that it failed.
|
|
93
|
+
*
|
|
94
|
+
* A launch reserves the run row, then starts the workflow. A host that dies between the two, or a start that throws
|
|
95
|
+
* while the following status write also fails, leaves a `running` row that no workflow will ever settle. That row
|
|
96
|
+
* holds the active-plan slot, thus each later launch of the plan answers `started: false` for a run that never
|
|
97
|
+
* started. Recovery cannot help, because no workflow row exists to recover.
|
|
98
|
+
*
|
|
99
|
+
* Only a run with no workflow row at all is failed. A run whose workflow exists, also a terminal one, is the
|
|
100
|
+
* workflow's to settle: a failed mark could misname a run that completed. Call it after `DBOS.launch()`, thus a
|
|
101
|
+
* recovered workflow is visible to the predicate.
|
|
102
|
+
*/
|
|
103
|
+
export declare function reconcileOrphanedRuns(pool: Querier, opts?: {
|
|
104
|
+
now?: number;
|
|
105
|
+
graceMs?: number;
|
|
106
|
+
}): ResultAsync<string[], DbError>;
|
|
85
107
|
export declare function updateRunStatus(pool: Querier, runId: string, status: "running" | "completed" | "partial" | "failed" | "canceled" | "suspended_insufficient_funds", error?: string | null): ResultAsync<void, DbError>;
|
|
86
108
|
/**
|
|
87
109
|
* Conditionally transition an ACTIVE run to `canceled`, stamping `completed_at`
|
|
@@ -99,6 +121,16 @@ export declare function queryRunsByAnalysis(pool: Querier, analysisId: string, {
|
|
|
99
121
|
limit?: number;
|
|
100
122
|
offset?: number;
|
|
101
123
|
}): ResultAsync<CortexRunRow[], DbError>;
|
|
124
|
+
/**
|
|
125
|
+
* The run ids of one plan of one analysis, newest start first, at most `limit`.
|
|
126
|
+
* The run id breaks a tie of start times, highest first. A plan with no run gives
|
|
127
|
+
* an empty list.
|
|
128
|
+
*/
|
|
129
|
+
export declare function queryRunIdsByPlan(pool: Querier, opts: {
|
|
130
|
+
analysisId: string;
|
|
131
|
+
planId: string;
|
|
132
|
+
limit: number;
|
|
133
|
+
}): ResultAsync<string[], DbError>;
|
|
102
134
|
/**
|
|
103
135
|
* Read the analysis's non-terminal run activity without coupling it to a
|
|
104
136
|
* conversation thread. The total is counted before the detail limit so a
|
|
@@ -106,8 +138,7 @@ export declare function queryRunsByAnalysis(pool: Querier, analysisId: string, {
|
|
|
106
138
|
*/
|
|
107
139
|
export declare function queryNonTerminalRunsByAnalysis(pool: Querier, analysisId: string, limit?: number): ResultAsync<RunPage, DbError>;
|
|
108
140
|
/**
|
|
109
|
-
* Read one stable inspection page: running first, then
|
|
110
|
-
* terminal history. This is deliberately separate from
|
|
141
|
+
* Read one stable inspection page: running first, then each other run. This is deliberately separate from
|
|
111
142
|
* {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
|
|
112
143
|
* contract.
|
|
113
144
|
*/
|
package/dist/state/runs.js
CHANGED
|
@@ -132,7 +132,7 @@ export function queryActiveRun(pool, analysisId, planId) {
|
|
|
132
132
|
FROM cortex_runs
|
|
133
133
|
WHERE analysis_id = $1
|
|
134
134
|
AND plan_id IS NOT DISTINCT FROM $2
|
|
135
|
-
AND status
|
|
135
|
+
AND status = 'running'`,
|
|
136
136
|
values: [analysisId, planId],
|
|
137
137
|
});
|
|
138
138
|
const row = result.rows[0];
|
|
@@ -152,9 +152,9 @@ export function queryActiveRun(pool, analysisId, planId) {
|
|
|
152
152
|
* analysis's live concurrency, not by its history, and a run leaves the set permanently when it
|
|
153
153
|
* finishes.
|
|
154
154
|
*
|
|
155
|
-
* The status set is the one {@link queryActiveRun} uses — non-terminal means
|
|
156
|
-
*
|
|
157
|
-
*
|
|
155
|
+
* The status set is the one {@link queryActiveRun} uses — non-terminal means `running`. Keep the two
|
|
156
|
+
* in step: a new non-terminal status added to one and not the other makes a live run invisible to
|
|
157
|
+
* exactly one of its readers.
|
|
158
158
|
*/
|
|
159
159
|
export function queryActiveRunsByAnalysis(pool, analysisId) {
|
|
160
160
|
return tryQuery("runs.queryActiveRunsByAnalysis", async () => {
|
|
@@ -164,16 +164,52 @@ export function queryActiveRunsByAnalysis(pool, analysisId) {
|
|
|
164
164
|
mandate_jti, mandate_expires_at, plan_id -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
165
165
|
FROM cortex_runs
|
|
166
166
|
WHERE analysis_id = $1
|
|
167
|
-
AND status
|
|
167
|
+
AND status = 'running'
|
|
168
168
|
ORDER BY started_at DESC`,
|
|
169
169
|
values: [analysisId],
|
|
170
170
|
});
|
|
171
171
|
return result.rows.map(mapRunRow);
|
|
172
172
|
});
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* How long a reserved run may wait for its workflow before {@link reconcileOrphanedRuns} fails it. A launch writes
|
|
176
|
+
* the workflow row seconds after the reservation, and the margin covers a slow authorization gate of a managed host.
|
|
177
|
+
*/
|
|
178
|
+
export const ORPHANED_RUN_GRACE_MS = 5 * 60 * 1000;
|
|
179
|
+
/**
|
|
180
|
+
* Fail each `running` run that started before the grace and that has no DBOS workflow row, and give the ids of
|
|
181
|
+
* the runs that it failed.
|
|
182
|
+
*
|
|
183
|
+
* A launch reserves the run row, then starts the workflow. A host that dies between the two, or a start that throws
|
|
184
|
+
* while the following status write also fails, leaves a `running` row that no workflow will ever settle. That row
|
|
185
|
+
* holds the active-plan slot, thus each later launch of the plan answers `started: false` for a run that never
|
|
186
|
+
* started. Recovery cannot help, because no workflow row exists to recover.
|
|
187
|
+
*
|
|
188
|
+
* Only a run with no workflow row at all is failed. A run whose workflow exists, also a terminal one, is the
|
|
189
|
+
* workflow's to settle: a failed mark could misname a run that completed. Call it after `DBOS.launch()`, thus a
|
|
190
|
+
* recovered workflow is visible to the predicate.
|
|
191
|
+
*/
|
|
192
|
+
export function reconcileOrphanedRuns(pool, opts = {}) {
|
|
193
|
+
const now = opts.now ?? Date.now();
|
|
194
|
+
const before = new Date(now - (opts.graceMs ?? ORPHANED_RUN_GRACE_MS)).toISOString();
|
|
195
|
+
return tryMutation("runs.reconcileOrphanedRuns", async () => {
|
|
196
|
+
const result = await pool.query({
|
|
197
|
+
text: `UPDATE cortex_runs r
|
|
198
|
+
SET status = 'failed',
|
|
199
|
+
error = 'The workflow of the run never started.',
|
|
200
|
+
completed_at = $1
|
|
201
|
+
WHERE r.status = 'running'
|
|
202
|
+
AND r.started_at < $2
|
|
203
|
+
AND NOT EXISTS (SELECT 1 FROM dbos.workflow_status ws WHERE ws.workflow_uuid = r.run_id)
|
|
204
|
+
RETURNING r.run_id`,
|
|
205
|
+
values: [new Date(now).toISOString(), before],
|
|
206
|
+
});
|
|
207
|
+
return result.rows.map((row) => row.run_id);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
174
210
|
export function updateRunStatus(pool, runId, status, error) {
|
|
175
211
|
const now = new Date().toISOString();
|
|
176
|
-
const isTerminal = status === "completed" || status === "partial" || status === "failed" || status === "canceled";
|
|
212
|
+
const isTerminal = status === "completed" || status === "partial" || status === "failed" || status === "canceled" || status === "suspended_insufficient_funds";
|
|
177
213
|
return tryMutation("runs.updateRunStatus", async () => {
|
|
178
214
|
await pool.query({
|
|
179
215
|
text: `UPDATE cortex_runs
|
|
@@ -198,7 +234,7 @@ export function markRunCanceledIfActive(pool, runId, reason) {
|
|
|
198
234
|
const result = await pool.query({
|
|
199
235
|
text: `UPDATE cortex_runs
|
|
200
236
|
SET status = 'canceled', completed_at = $2, error = $3
|
|
201
|
-
WHERE run_id = $1 AND status
|
|
237
|
+
WHERE run_id = $1 AND status = 'running'`,
|
|
202
238
|
values: [runId, now, reason],
|
|
203
239
|
});
|
|
204
240
|
return (result.rowCount ?? 0) > 0;
|
|
@@ -242,6 +278,23 @@ export function queryRunsByAnalysis(pool, analysisId, { limit = 50, offset = 0 }
|
|
|
242
278
|
return result.rows.map(mapRunRow);
|
|
243
279
|
});
|
|
244
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* The run ids of one plan of one analysis, newest start first, at most `limit`.
|
|
283
|
+
* The run id breaks a tie of start times, highest first. A plan with no run gives
|
|
284
|
+
* an empty list.
|
|
285
|
+
*/
|
|
286
|
+
export function queryRunIdsByPlan(pool, opts) {
|
|
287
|
+
return tryQuery("runs.queryRunIdsByPlan", async () => {
|
|
288
|
+
const result = await pool.query({
|
|
289
|
+
text: `SELECT run_id FROM cortex_runs
|
|
290
|
+
WHERE analysis_id = $1 AND plan_id = $2
|
|
291
|
+
ORDER BY started_at DESC, run_id DESC
|
|
292
|
+
LIMIT $3`,
|
|
293
|
+
values: [opts.analysisId, opts.planId, opts.limit],
|
|
294
|
+
});
|
|
295
|
+
return result.rows.map((row) => row.run_id);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
245
298
|
/**
|
|
246
299
|
* Read the analysis's non-terminal run activity without coupling it to a
|
|
247
300
|
* conversation thread. The total is counted before the detail limit so a
|
|
@@ -253,7 +306,7 @@ export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
|
|
|
253
306
|
text: `SELECT COUNT(*)::int AS total
|
|
254
307
|
FROM cortex_runs
|
|
255
308
|
WHERE analysis_id = $1
|
|
256
|
-
AND status
|
|
309
|
+
AND status = 'running'`,
|
|
257
310
|
values: [analysisId],
|
|
258
311
|
});
|
|
259
312
|
const rowsResult = await pool.query({
|
|
@@ -262,13 +315,8 @@ export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
|
|
|
262
315
|
mandate_jti, mandate_expires_at, plan_id
|
|
263
316
|
FROM cortex_runs
|
|
264
317
|
WHERE analysis_id = $1
|
|
265
|
-
AND status
|
|
266
|
-
ORDER BY
|
|
267
|
-
WHEN 'running' THEN 0
|
|
268
|
-
WHEN 'suspended_insufficient_funds' THEN 1
|
|
269
|
-
ELSE 2
|
|
270
|
-
END,
|
|
271
|
-
started_at DESC,
|
|
318
|
+
AND status = 'running'
|
|
319
|
+
ORDER BY started_at DESC,
|
|
272
320
|
run_id DESC
|
|
273
321
|
LIMIT $2`,
|
|
274
322
|
values: [analysisId, limit],
|
|
@@ -280,8 +328,7 @@ export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
|
|
|
280
328
|
});
|
|
281
329
|
}
|
|
282
330
|
/**
|
|
283
|
-
* Read one stable inspection page: running first, then
|
|
284
|
-
* terminal history. This is deliberately separate from
|
|
331
|
+
* Read one stable inspection page: running first, then each other run. This is deliberately separate from
|
|
285
332
|
* {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
|
|
286
333
|
* contract.
|
|
287
334
|
*/
|
|
@@ -299,8 +346,7 @@ export function queryRunsForInspection(pool, analysisId, { limit = 50, offset =
|
|
|
299
346
|
WHERE analysis_id = $1
|
|
300
347
|
ORDER BY CASE status
|
|
301
348
|
WHEN 'running' THEN 0
|
|
302
|
-
|
|
303
|
-
ELSE 2
|
|
349
|
+
ELSE 1
|
|
304
350
|
END,
|
|
305
351
|
started_at DESC,
|
|
306
352
|
run_id DESC
|
package/dist/state/schema.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export declare const RunStatus: z.ZodEnum<{
|
|
|
54
54
|
suspended_insufficient_funds: "suspended_insufficient_funds";
|
|
55
55
|
}>;
|
|
56
56
|
export type RunStatus = z.infer<typeof RunStatus>;
|
|
57
|
+
/** A status that a run keeps once it ended: every status but `running`. */
|
|
58
|
+
export type TerminalRunStatus = Exclude<RunStatus, "running">;
|
|
57
59
|
export declare const StepExecutionStatus: z.ZodEnum<{
|
|
58
60
|
failed: "failed";
|
|
59
61
|
pending: "pending";
|
|
@@ -103,5 +103,11 @@ export interface ProfileActivityFrame {
|
|
|
103
103
|
* A failing write is swallowed and logged, never propagated: observation is a
|
|
104
104
|
* diagnostic channel, and a dropped frame must not fail a profile that is otherwise
|
|
105
105
|
* succeeding. This mirrors the sandbox-step producer's stance on the same write.
|
|
106
|
+
*
|
|
107
|
+
* Each emission writes the reconciling activity part, so a late subscriber folds to the
|
|
108
|
+
* current activity, and then one `data-activity-entry`, so it also gets each earlier line.
|
|
109
|
+
* The two parts carry one time from `now`, which the body binds to the checkpointed clock of
|
|
110
|
+
* its workflow. The two writes fail alone. The sequence number of the entry advances before
|
|
111
|
+
* the clock read and the writes, thus a failure cannot shift a later id.
|
|
106
112
|
*/
|
|
107
|
-
export declare function createProfileActivityEmitter(write: ActivityWrite, frame: ProfileActivityFrame, logger: Logger): ProfileActivityEmitter;
|
|
113
|
+
export declare function createProfileActivityEmitter(write: ActivityWrite, frame: ProfileActivityFrame, logger: Logger, now: () => Promise<number>): ProfileActivityEmitter;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* anything that throws on the way there reaches the failure path having emitted
|
|
20
20
|
* no terminal at all.
|
|
21
21
|
*/
|
|
22
|
-
import {
|
|
22
|
+
import { activityEmission, activityForTool } from "../sandbox/sandbox-step-translate.js";
|
|
23
23
|
/**
|
|
24
24
|
* Build the profile's activity channel over `write`.
|
|
25
25
|
*
|
|
@@ -30,21 +30,30 @@ import { activityForTool, stepPartId } from "../sandbox/sandbox-step-translate.j
|
|
|
30
30
|
* A failing write is swallowed and logged, never propagated: observation is a
|
|
31
31
|
* diagnostic channel, and a dropped frame must not fail a profile that is otherwise
|
|
32
32
|
* succeeding. This mirrors the sandbox-step producer's stance on the same write.
|
|
33
|
+
*
|
|
34
|
+
* Each emission writes the reconciling activity part, so a late subscriber folds to the
|
|
35
|
+
* current activity, and then one `data-activity-entry`, so it also gets each earlier line.
|
|
36
|
+
* The two parts carry one time from `now`, which the body binds to the checkpointed clock of
|
|
37
|
+
* its workflow. The two writes fail alone. The sequence number of the entry advances before
|
|
38
|
+
* the clock read and the writes, thus a failure cannot shift a later id.
|
|
33
39
|
*/
|
|
34
|
-
export function createProfileActivityEmitter(write, frame, logger) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// accumulating them. Minted through the shared construction because that
|
|
38
|
-
// sameness across producers is what makes the fold correct.
|
|
39
|
-
const id = stepPartId("step-activity", frame.runId, frame.stepId);
|
|
40
|
-
const emit = async (phase, activity) => {
|
|
40
|
+
export function createProfileActivityEmitter(write, frame, logger, now) {
|
|
41
|
+
let seq = 0;
|
|
42
|
+
const writeAlone = async (part, phase) => {
|
|
41
43
|
try {
|
|
42
|
-
await write(
|
|
44
|
+
await write(part);
|
|
43
45
|
}
|
|
44
46
|
catch (err) {
|
|
45
47
|
logger.warn("run-event write failed (non-fatal)", { phase, ...logger.errorFields(err) });
|
|
46
48
|
}
|
|
47
49
|
};
|
|
50
|
+
const emit = async (phase, activity) => {
|
|
51
|
+
const position = seq++;
|
|
52
|
+
const at = new Date(await now()).toISOString();
|
|
53
|
+
const parts = activityEmission(frame, position, phase, activity, at);
|
|
54
|
+
await writeAlone(parts.activity, phase);
|
|
55
|
+
await writeAlone(parts.entry, phase);
|
|
56
|
+
};
|
|
48
57
|
return {
|
|
49
58
|
sandboxInit: () => emit("sandbox-init", "Starting sandbox"),
|
|
50
59
|
scanning: () => emit("executing", "Scanning input files"),
|
|
@@ -28,7 +28,7 @@ import type { SandboxAgentDeps } from "../agents/sandbox/shared.js";
|
|
|
28
28
|
import type { BioToolKeys } from "../tools/bio/keys.js";
|
|
29
29
|
import type { ToolOutputStore } from "../loop/tool-output.js";
|
|
30
30
|
import type { DbError } from "../lib/db-result.js";
|
|
31
|
-
import { type GateRefusal } from "../lib/hooks.js";
|
|
31
|
+
import { type GateRefusal, type NoticeFailure } from "../lib/hooks.js";
|
|
32
32
|
import type { Logger } from "../lib/logger.js";
|
|
33
33
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
34
34
|
import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
|
|
@@ -38,6 +38,15 @@ import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
|
38
38
|
import { type ProfileSubmission } from "../schemas/data-profile-schemas.js";
|
|
39
39
|
import { type ProfileResolution } from "./data-profile-resolve.js";
|
|
40
40
|
import { type DataProfileResult } from "../state/index.js";
|
|
41
|
+
/** The settle of one profile claim: the ledger row left `running`, and the terminal write stamped it. */
|
|
42
|
+
export interface DataProfileSettlement {
|
|
43
|
+
readonly analysisId: string;
|
|
44
|
+
readonly status: "completed" | "failed";
|
|
45
|
+
/** The `data_profile_completed_at` that the terminal write stamped, as an ISO 8601 string. */
|
|
46
|
+
readonly completedAt: string;
|
|
47
|
+
}
|
|
48
|
+
/** The notice of a profile settle. The workflow body calls it one time for each settled claim. */
|
|
49
|
+
export type NotifyDataProfileSettled = (settlement: DataProfileSettlement) => ResultAsync<void, NoticeFailure>;
|
|
41
50
|
/** The body's construction-time deps — closed over at registration. */
|
|
42
51
|
export interface DataProfileDeps extends EnvironmentStorePaths {
|
|
43
52
|
/** Operational logging seam; omitted falls back to no-op. */
|
|
@@ -73,6 +82,11 @@ export interface DataProfileDeps extends EnvironmentStorePaths {
|
|
|
73
82
|
* Unbound keeps the current shape: no link tool, and no error.
|
|
74
83
|
*/
|
|
75
84
|
readonly extendAnalysisFarm?: ExtendAnalysisFarm;
|
|
85
|
+
/**
|
|
86
|
+
* Notice of each profile settle, for the records of the embedder. Unbound, the
|
|
87
|
+
* body calls nothing. It is not a provenance hook.
|
|
88
|
+
*/
|
|
89
|
+
readonly notifyDataProfileSettled?: NotifyDataProfileSettled;
|
|
76
90
|
}
|
|
77
91
|
/**
|
|
78
92
|
* Workflow input. JSON-serialisable — DBOS persists it as the workflow's
|
|
@@ -296,7 +296,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
296
296
|
// ledger has no suspended state and the retry path takes a `failed` row again.
|
|
297
297
|
const suspendProfile = async (suspension) => {
|
|
298
298
|
logger.warn("profile suspended", { reason: suspension.reason });
|
|
299
|
-
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, suspension.reason)), "failed");
|
|
299
|
+
await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, suspension.reason)), "failed", deps.notifyDataProfileSettled);
|
|
300
300
|
await revoke("data-profile-suspended");
|
|
301
301
|
unwrapOrThrow(await suspendAnalysis(deps.pool, analysisId));
|
|
302
302
|
await activity.failed(suspension.reason);
|
|
@@ -310,7 +310,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
310
310
|
// this capability, and a body that composed its own strings is a body they can drift from.
|
|
311
311
|
// The frame is the workflow's synthetic one: both values are constants shared by every analysis,
|
|
312
312
|
// so they identify nothing and consumers are required not to key on them.
|
|
313
|
-
const activity = createProfileActivityEmitter((part) => DBOS.writeStream("events", part), { runId: DATA_PROFILE_RUN_LITERAL, stepId: DATA_PROFILE_STEP_LITERAL }, logger);
|
|
313
|
+
const activity = createProfileActivityEmitter((part) => DBOS.writeStream("events", part), { runId: DATA_PROFILE_RUN_LITERAL, stepId: DATA_PROFILE_STEP_LITERAL }, logger, () => DBOS.now());
|
|
314
314
|
try {
|
|
315
315
|
// Record which workflow owns this profile before anything else, so a consumer that resolves
|
|
316
316
|
// the id from the ledger can subscribe from the earliest possible moment. Guarded on a real
|
|
@@ -337,7 +337,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
337
337
|
// catalogue's fit is measured, and a completion that emitted none would make the
|
|
338
338
|
// empty-manifest path invisible to the same monitoring that watches every other.
|
|
339
339
|
logProfileMonitoring(logger, { dimensions: [], probes: [], repairRounds: 0, absorb: "none" });
|
|
340
|
-
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId)), "completed");
|
|
340
|
+
await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId)), "completed", deps.notifyDataProfileSettled);
|
|
341
341
|
await revoke("data-profile-completed");
|
|
342
342
|
// This is a terminal completion like any other, so it reports one — a consumer watching
|
|
343
343
|
// an empty-manifest profile sees it settle rather than seeing the stream simply stop.
|
|
@@ -356,10 +356,10 @@ export async function runDataProfileBody(input, deps) {
|
|
|
356
356
|
const executionId = generateExecutionId(DATA_PROFILE_AGENT_ID);
|
|
357
357
|
const workflowId = DBOS.workflowID ?? `${DATA_PROFILE_RUN_LITERAL}:${executionId}`;
|
|
358
358
|
const childSession = forSubAgent(runSession, DATA_PROFILE_AGENT_ID);
|
|
359
|
-
// A resolver
|
|
359
|
+
// A resolver rejection fails the workflow loudly (workspace-root-resolution
|
|
360
360
|
// contract) — profiling an analysis whose root cannot be resolved is
|
|
361
361
|
// meaningless, so no softer handling is warranted.
|
|
362
|
-
const workspaceRoot = deps.resolveWorkspaceRoot(analysisId);
|
|
362
|
+
const workspaceRoot = await deps.resolveWorkspaceRoot(analysisId);
|
|
363
363
|
const profileWritePrefix = `${workspaceRoot}/runs/${DATA_PROFILE_RUN_LITERAL}/${DATA_PROFILE_STEP_LITERAL}`;
|
|
364
364
|
// 3. The deterministic scan, before any container and before the agent's first turn.
|
|
365
365
|
// It is always needed and its result does not depend on agent judgement, so spending
|
|
@@ -390,7 +390,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
390
390
|
await activity.indexing();
|
|
391
391
|
await indexProfile({ deps, analysisId, session: runSession, record, scan, logger });
|
|
392
392
|
logProfileMonitoring(logger, { resolution, dimensions: [], probes: [], repairRounds: 0, absorb: "none" });
|
|
393
|
-
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, record)), "completed");
|
|
393
|
+
await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, record)), "completed", deps.notifyDataProfileSettled);
|
|
394
394
|
await revoke("data-profile-completed");
|
|
395
395
|
await activity.complete();
|
|
396
396
|
return;
|
|
@@ -416,7 +416,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
416
416
|
repairRounds: 0,
|
|
417
417
|
absorb: absorb.kind,
|
|
418
418
|
});
|
|
419
|
-
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, absorbed)), "completed");
|
|
419
|
+
await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, absorbed)), "completed", deps.notifyDataProfileSettled);
|
|
420
420
|
await revoke("data-profile-completed");
|
|
421
421
|
await activity.complete();
|
|
422
422
|
return;
|
|
@@ -571,7 +571,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
571
571
|
});
|
|
572
572
|
// 6. Complete — store the FULL profiler finding plus the input signature for
|
|
573
573
|
// staleness detection. The scratch tree is gone; this row is all that survives.
|
|
574
|
-
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, profileRecord)), "completed");
|
|
574
|
+
await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, profileRecord)), "completed", deps.notifyDataProfileSettled);
|
|
575
575
|
await revoke("data-profile-completed");
|
|
576
576
|
// LAST statement of the success path, and that placement is the guarantee that exactly
|
|
577
577
|
// one terminal activity is ever emitted. Everything that could still throw — the ledger
|
|
@@ -598,7 +598,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
598
598
|
return await suspendProfile(suspension);
|
|
599
599
|
logger.error("profile failed", logger.errorFields(err));
|
|
600
600
|
const reason = profileFailureReason(err);
|
|
601
|
-
settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, reason)), "failed");
|
|
601
|
+
await settleTerminalWrite(logger, analysisId, unwrapOrThrow(await failDataProfile(deps.pool, analysisId, reason)), "failed", deps.notifyDataProfileSettled);
|
|
602
602
|
await revoke("data-profile-failed");
|
|
603
603
|
// The same bounded, user-safe line the ledger receives — never the raw error, whose paths and
|
|
604
604
|
// stack frames stay in the log record above. This is the only other terminal emission, so
|
|
@@ -617,18 +617,23 @@ function logTerminalNoop(logger, analysisId, write) {
|
|
|
617
617
|
}
|
|
618
618
|
/**
|
|
619
619
|
* Settle one terminal write of the profile ledger. A refused stamp is logged
|
|
620
|
-
* (see `logTerminalNoop`). An accepted stamp records the
|
|
621
|
-
*
|
|
620
|
+
* (see `logTerminalNoop`) and sends no notice. An accepted stamp records the
|
|
621
|
+
* profile's run outcome and then delivers the settle notice when the deps bind
|
|
622
|
+
* one: the CAS admits one write per `running` claim, so this is the exactly-once
|
|
622
623
|
* point of the transition — a recovery replay of the body finds the row no
|
|
623
|
-
* longer `running`, and
|
|
624
|
+
* longer `running`, and does neither again. The duration runs from the
|
|
624
625
|
* `data_profile_started_at` the claim stamped, never from a body-local clock.
|
|
626
|
+
* A crash between the stamp and the notice loses that one notice.
|
|
625
627
|
*/
|
|
626
|
-
function settleTerminalWrite(logger, analysisId, write, status) {
|
|
628
|
+
async function settleTerminalWrite(logger, analysisId, write, status, notify) {
|
|
627
629
|
if (!write.stamped) {
|
|
628
630
|
logTerminalNoop(logger, analysisId, status === "completed" ? "completion" : "failure");
|
|
629
631
|
return;
|
|
630
632
|
}
|
|
631
633
|
recordRunCompleted({ workflow: "data_profile", status, durationMs: elapsedSinceIso(write.startedAt) });
|
|
634
|
+
if (notify === undefined)
|
|
635
|
+
return;
|
|
636
|
+
await deliverNotice(logger, "notifyDataProfileSettled", notify({ analysisId, status, completedAt: write.completedAt }));
|
|
632
637
|
}
|
|
633
638
|
/** A one-line, user-facing description of a `DataProfileStartError`. */
|
|
634
639
|
export function describeDataProfileStartError(e) {
|
|
@@ -40,8 +40,11 @@ export function createAskGateway(deps) {
|
|
|
40
40
|
// A standing grant this user holds for this ask's grant key — the command
|
|
41
41
|
// unless the tool supplied a broader key — short-circuits the prompt: record
|
|
42
42
|
// the invocation as `resolved` for the audit trail and return without pausing.
|
|
43
|
+
// The one part it emits is already settled, so no surface prompts, and the
|
|
44
|
+
// transcript still shows that a standing grant approved the call.
|
|
43
45
|
if (unwrapOrThrow(await selectGrant(pool, ctx.analysisId, ctx.userId, request.grantKey ?? request.command))) {
|
|
44
46
|
unwrapOrThrow(await insertGrantedAsk(pool, row, now));
|
|
47
|
+
await emitAskPart(ctx.emit, id, request, "resolved", "always");
|
|
45
48
|
return { kind: "always" };
|
|
46
49
|
}
|
|
47
50
|
unwrapOrThrow(await insertPendingAsk(pool, row));
|
|
@@ -69,7 +72,7 @@ export function createAskGateway(deps) {
|
|
|
69
72
|
throw new AskRejectedError("approval request is no longer available");
|
|
70
73
|
}
|
|
71
74
|
if (resolution.status !== "pending") {
|
|
72
|
-
await emitAskPart(ctx.emit, id, request, resolution.status);
|
|
75
|
+
await emitAskPart(ctx.emit, id, request, resolution.status, replyKindOf(resolution.status, resolution.reply));
|
|
73
76
|
return settle(resolution.status, resolution.reply, ctx.signal);
|
|
74
77
|
}
|
|
75
78
|
await sleepUntil(POLL_INTERVAL_MS, ctx.signal);
|
|
@@ -88,7 +91,7 @@ export function createAskGateway(deps) {
|
|
|
88
91
|
return { ask, answer, pending, sweepExpired };
|
|
89
92
|
}
|
|
90
93
|
/** Emit (or reconcile, under the same id) the ask's `data-ask` chat part. */
|
|
91
|
-
function emitAskPart(emit, id, request, status) {
|
|
94
|
+
function emitAskPart(emit, id, request, status, reply) {
|
|
92
95
|
return emit({
|
|
93
96
|
type: "data-ask",
|
|
94
97
|
data: {
|
|
@@ -97,16 +100,36 @@ function emitAskPart(emit, id, request, status) {
|
|
|
97
100
|
command: request.command,
|
|
98
101
|
...(request.detail !== undefined ? { detail: request.detail } : {}),
|
|
99
102
|
status,
|
|
103
|
+
...(reply !== undefined ? { reply } : {}),
|
|
100
104
|
},
|
|
101
105
|
});
|
|
102
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* The kind of reply the terminal part shows: the kind that `settle` acts on for a `resolved` or `rejected` row,
|
|
109
|
+
* and none for any other status. It never reads the feedback of a rejection, which is for the model.
|
|
110
|
+
*/
|
|
111
|
+
function replyKindOf(status, reply) {
|
|
112
|
+
switch (status) {
|
|
113
|
+
case "resolved":
|
|
114
|
+
return approvalOf(reply).kind;
|
|
115
|
+
case "rejected":
|
|
116
|
+
return "reject";
|
|
117
|
+
case "expired":
|
|
118
|
+
case "aborted":
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** The approval that a `resolved` row grants. */
|
|
123
|
+
function approvalOf(reply) {
|
|
124
|
+
// Only `once` / `always` reach `resolved`; guard the JSONB round-trip and
|
|
125
|
+
// default to the narrowest approval if a malformed reply ever slipped in.
|
|
126
|
+
return reply && (reply.kind === "once" || reply.kind === "always") ? reply : { kind: "once" };
|
|
127
|
+
}
|
|
103
128
|
/** Resolve an approval or throw the terminal denial the ledger recorded. */
|
|
104
129
|
function settle(status, reply, signal) {
|
|
105
130
|
switch (status) {
|
|
106
131
|
case "resolved":
|
|
107
|
-
|
|
108
|
-
// default to the narrowest approval if a malformed reply ever slipped in.
|
|
109
|
-
return reply && (reply.kind === "once" || reply.kind === "always") ? reply : { kind: "once" };
|
|
132
|
+
return approvalOf(reply);
|
|
110
133
|
case "rejected":
|
|
111
134
|
throw new AskRejectedError(reply && reply.kind === "reject" ? reply.feedback : undefined);
|
|
112
135
|
case "expired":
|
|
@@ -26,7 +26,7 @@ type ShowUserOutput = {
|
|
|
26
26
|
id: string;
|
|
27
27
|
};
|
|
28
28
|
export declare const showUserTool: import("../define-tool.js").Tool<{
|
|
29
|
-
kind: "code" | "
|
|
29
|
+
kind: "code" | "echart" | "markdown" | "svg" | "table" | "structure";
|
|
30
30
|
title?: string | undefined;
|
|
31
31
|
spec?: Record<string, unknown> | undefined;
|
|
32
32
|
dataPath?: string | undefined;
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Unified async edge for approved plans and explicitly requested one-step ad
|
|
3
3
|
* hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
|
|
4
4
|
*/
|
|
5
|
+
import { ResultAsync } from "neverthrow";
|
|
5
6
|
import type { Pool } from "pg";
|
|
7
|
+
import type { AgentSession } from "../auth/types.js";
|
|
6
8
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
7
9
|
import type { EnvironmentStorePaths } from "../config/environment-stores.js";
|
|
8
10
|
import type { ResourcePolicy } from "../config/resource-limits.js";
|
|
@@ -54,7 +56,56 @@ export declare class PlanPackagesMissingError extends Error {
|
|
|
54
56
|
readonly unavailable?: string | undefined;
|
|
55
57
|
constructor(missing: string[], collisions: string[], unavailable?: string | undefined);
|
|
56
58
|
}
|
|
59
|
+
/** The deps that start the run of a stored plan. */
|
|
60
|
+
export type LaunchPlanRunDeps = Pick<ExecuteAnalysisToolDeps, "pool" | "executeAnalysisWorkflow" | "runAuthorizer" | "runLauncher" | "resourcePolicy" | "extendAnalysisFarm" | "logger">;
|
|
61
|
+
/** The run of a stored plan. `started` is false when a run of the plan was already active. */
|
|
62
|
+
export type PlanLaunch = {
|
|
63
|
+
runId: string;
|
|
64
|
+
started: boolean;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Why a stored plan did not start a run. `store_failed.cause` and `launch_failed.cause` are the
|
|
68
|
+
* values as the failing call gave them, thus a caller that must throw them keeps their class.
|
|
69
|
+
*/
|
|
70
|
+
export type LaunchPlanRunRefusal = {
|
|
71
|
+
type: "plan_not_found";
|
|
72
|
+
planId: string;
|
|
73
|
+
} | {
|
|
74
|
+
type: "plan_invalid";
|
|
75
|
+
planId: string;
|
|
76
|
+
malformed: boolean;
|
|
77
|
+
errors: string[];
|
|
78
|
+
} | {
|
|
79
|
+
type: "packages_missing";
|
|
80
|
+
missing: string[];
|
|
81
|
+
collisions: string[];
|
|
82
|
+
unavailable?: string;
|
|
83
|
+
} | {
|
|
84
|
+
type: "not_authorized";
|
|
85
|
+
reason: string;
|
|
86
|
+
} | {
|
|
87
|
+
type: "suspended";
|
|
88
|
+
reason: string;
|
|
89
|
+
} | {
|
|
90
|
+
type: "store_failed";
|
|
91
|
+
cause: unknown;
|
|
92
|
+
} | {
|
|
93
|
+
type: "launch_failed";
|
|
94
|
+
cause: unknown;
|
|
95
|
+
};
|
|
57
96
|
export declare function buildAdHocPlan(request: string, route: AdHocRoute, createdAt?: string): AnalysisPlan;
|
|
97
|
+
/**
|
|
98
|
+
* Start the run of a stored plan. It reads the plan under the analysis of the session, validates it,
|
|
99
|
+
* links its packages, reserves a run, authorizes it, and starts the workflow. It gives the run id, or
|
|
100
|
+
* the refusal, and it emits no chat part. `started` is false when a run of the plan was already active.
|
|
101
|
+
*
|
|
102
|
+
* This is an API boundary: a host calls it outside a tool body, thus no outcome above rejects the
|
|
103
|
+
* promise.
|
|
104
|
+
*/
|
|
105
|
+
export declare function launchPlanRun(deps: LaunchPlanRunDeps, input: {
|
|
106
|
+
session: AgentSession;
|
|
107
|
+
planId: string;
|
|
108
|
+
}): ResultAsync<PlanLaunch, LaunchPlanRunRefusal>;
|
|
58
109
|
export declare function createExecuteAnalysisTool(deps: ExecuteAnalysisToolDeps): import("./define-tool.js").Tool<{
|
|
59
110
|
mode: "plan" | "adhoc";
|
|
60
111
|
planId?: string | undefined;
|