@inflexa-ai/harness 0.13.0 → 0.14.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/agents/conversation-agent.js +23 -16
- package/dist/agents/sandbox/cheminformatics-agent.js +1 -1
- package/dist/agents/sandbox/chromatin-agent.js +1 -1
- package/dist/agents/sandbox/dna-methylation-agent.js +1 -1
- package/dist/agents/sandbox/drug-repurposing-agent.js +5 -10
- package/dist/agents/sandbox/enrichment-agent.js +1 -1
- package/dist/agents/sandbox/genomic-variant-agent.js +1 -11
- package/dist/agents/sandbox/immune-profiling-agent.js +1 -1
- package/dist/agents/sandbox/multi-omics-integration-agent.js +1 -1
- package/dist/agents/sandbox/network-agent.js +1 -1
- package/dist/agents/sandbox/pkpd-clinical-response-agent.js +3 -6
- package/dist/agents/sandbox/scientific-executor.js +1 -1
- package/dist/agents/sandbox/shared.js +7 -13
- package/dist/agents/sandbox/translational-safety-agent.js +4 -7
- package/dist/agents/sandbox/types.d.ts +5 -5
- package/dist/agents/sandbox/types.js +4 -4
- package/dist/app/chat-turn.js +4 -1
- package/dist/app/message-assembly.d.ts +5 -2
- package/dist/app/message-assembly.js +7 -2
- package/dist/app/run-activity.d.ts +13 -0
- package/dist/app/run-activity.js +51 -0
- package/dist/contracts/target-dossier.d.ts +10 -10
- package/dist/execution/report-runner.js +1 -1
- package/dist/execution/run-event-parts.d.ts +44 -0
- package/dist/execution/run-event-parts.js +0 -0
- package/dist/execution/run-event-stream.d.ts +91 -0
- package/dist/execution/run-event-stream.js +212 -0
- package/dist/execution/run-synthesis.js +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +20 -1
- package/dist/input-sanitization.d.ts +14 -4
- package/dist/input-sanitization.js +27 -4
- package/dist/lib/storage.js +4 -1
- package/dist/loop/budget-exceeded.d.ts +18 -2
- package/dist/loop/budget-exceeded.js +49 -2
- package/dist/loop/run-agent.d.ts +22 -1
- package/dist/loop/run-agent.js +91 -25
- package/dist/loop/run-to-terminal.d.ts +22 -4
- package/dist/loop/run-to-terminal.js +20 -4
- package/dist/memory/ai-sdk-message-storage.d.ts +37 -4
- package/dist/memory/ai-sdk-message-storage.js +45 -4
- package/dist/memory/content-to-cortex.js +19 -3
- package/dist/memory/message-backfill.js +1 -1
- package/dist/memory/thread-history.js +34 -8
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +15 -6
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +16 -14
- package/dist/prompts/planner.js +15 -20
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +31 -22
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -10
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +8 -7
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +20 -14
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +7 -7
- package/dist/providers/errors.d.ts +7 -0
- package/dist/providers/errors.js +88 -3
- package/dist/runtime/pools.d.ts +15 -1
- package/dist/runtime/pools.js +15 -1
- package/dist/sandbox/sandbox-step-translate.d.ts +15 -2
- package/dist/sandbox/sandbox-step-translate.js +17 -2
- package/dist/schemas/workflow-state.d.ts +2 -2
- package/dist/schemas/workflow-state.js +2 -2
- package/dist/state/data-profile.d.ts +53 -1
- package/dist/state/data-profile.js +66 -6
- package/dist/state/index.d.ts +3 -3
- package/dist/state/index.js +2 -2
- package/dist/state/init.js +37 -2
- package/dist/state/runs.d.ts +39 -0
- package/dist/state/runs.js +103 -0
- package/dist/tasks/data-profile-activity.d.ts +87 -0
- package/dist/tasks/data-profile-activity.js +56 -0
- package/dist/tasks/data-profile.js +66 -4
- package/dist/tools/bio/chembl.d.ts +1 -1
- package/dist/tools/bio/chembl.js +51 -54
- package/dist/tools/bio/comptox.js +35 -33
- package/dist/tools/bio/drug-gene-interactions.d.ts +76 -0
- package/dist/tools/bio/drug-gene-interactions.js +175 -0
- package/dist/tools/bio/gene-disease-evidence.d.ts +61 -0
- package/dist/tools/bio/gene-disease-evidence.js +187 -0
- package/dist/tools/bio/gene-preclinical-profile.d.ts +58 -0
- package/dist/tools/bio/gene-preclinical-profile.js +125 -0
- package/dist/tools/bio/index.d.ts +23 -16
- package/dist/tools/bio/index.js +29 -22
- package/dist/tools/bio/keys.d.ts +16 -9
- package/dist/tools/bio/keys.js +22 -13
- package/dist/tools/bio/lookup-annotation.d.ts +48 -0
- package/dist/tools/bio/lookup-annotation.js +173 -0
- package/dist/tools/bio/opentargets.d.ts +7 -11
- package/dist/tools/bio/opentargets.js +24 -35
- package/dist/tools/bio/pubchem.js +6 -4
- package/dist/tools/bio/pubmed.d.ts +15 -7
- package/dist/tools/bio/pubmed.js +79 -20
- package/dist/tools/bio/search-clinical-trials.d.ts +20 -1
- package/dist/tools/bio/search-clinical-trials.js +26 -5
- package/dist/tools/bio/search-faers.js +9 -3
- package/dist/tools/bio/search-gene.d.ts +0 -1
- package/dist/tools/bio/search-gene.js +4 -5
- package/dist/tools/bio/search-geo-datasets.js +10 -4
- package/dist/tools/bio/search-interactions.d.ts +8 -4
- package/dist/tools/bio/search-interactions.js +28 -12
- package/dist/tools/bio/target-safety.d.ts +59 -0
- package/dist/tools/bio/target-safety.js +191 -0
- package/dist/tools/execute-analysis.d.ts +1 -0
- package/dist/tools/execute-analysis.js +5 -5
- package/dist/tools/lib/dgidb-client.d.ts +49 -0
- package/dist/tools/lib/dgidb-client.js +168 -0
- package/dist/tools/lib/disgenet-client.d.ts +56 -0
- package/dist/tools/lib/disgenet-client.js +59 -0
- package/dist/tools/lib/drugbank-client.d.ts +83 -0
- package/dist/tools/lib/drugbank-client.js +95 -0
- package/dist/tools/lib/ensembl-client.d.ts +4 -1
- package/dist/tools/lib/ensembl-client.js +5 -3
- package/dist/tools/lib/gwas-catalog-client.d.ts +60 -0
- package/dist/tools/lib/gwas-catalog-client.js +136 -0
- package/dist/tools/lib/pharmgkb-client.d.ts +22 -0
- package/dist/tools/lib/pharmgkb-client.js +32 -0
- package/dist/tools/lib/pubmed-client.d.ts +57 -4
- package/dist/tools/lib/pubmed-client.js +57 -3
- package/dist/tools/lib/string-client.d.ts +25 -4
- package/dist/tools/lib/string-client.js +22 -5
- package/dist/tools/research/generate-plan.d.ts +16 -9
- package/dist/tools/research/generate-plan.js +405 -85
- package/dist/tools/research/inspect-data-profile.d.ts +31 -6
- package/dist/tools/research/inspect-data-profile.js +23 -7
- package/dist/tools/research/inspect-run.d.ts +33 -15
- package/dist/tools/research/inspect-run.js +155 -66
- package/dist/tools/research/literature-reviewer.js +7 -11
- package/dist/workflows/execute-analysis.d.ts +63 -0
- package/dist/workflows/execute-analysis.js +73 -2
- package/dist/workflows/sandbox-step.js +1 -11
- package/dist/workflows/target-assessment/decisions/index.d.ts +1 -1
- package/dist/workflows/target-assessment/phase4-assemble.d.ts +1 -1
- package/dist/workflows/target-assessment/steps/phase2-aggregate.d.ts +2 -2
- package/dist/workflows/target-assessment/steps/phase3-aggregate.d.ts +1 -1
- package/package.json +1 -1
- package/dist/tools/bio/check-safety-panel.d.ts +0 -35
- package/dist/tools/bio/check-safety-panel.js +0 -76
- package/dist/tools/bio/get-impc-ko-profile.d.ts +0 -9
- package/dist/tools/bio/get-impc-ko-profile.js +0 -23
- package/dist/tools/bio/lookup-go-term.d.ts +0 -31
- package/dist/tools/bio/lookup-go-term.js +0 -116
- package/dist/tools/bio/search-bgee-expression.d.ts +0 -10
- package/dist/tools/bio/search-bgee-expression.js +0 -30
- package/dist/tools/bio/search-clinvar.d.ts +0 -13
- package/dist/tools/bio/search-clinvar.js +0 -28
- package/dist/tools/bio/search-dgidb.d.ts +0 -53
- package/dist/tools/bio/search-dgidb.js +0 -213
- package/dist/tools/bio/search-disgenet.d.ts +0 -30
- package/dist/tools/bio/search-disgenet.js +0 -84
- package/dist/tools/bio/search-drugbank.d.ts +0 -38
- package/dist/tools/bio/search-drugbank.js +0 -109
- package/dist/tools/bio/search-gwas-catalog.d.ts +0 -33
- package/dist/tools/bio/search-gwas-catalog.js +0 -172
- package/dist/tools/bio/search-pathway.d.ts +0 -12
- package/dist/tools/bio/search-pathway.js +0 -32
- package/dist/tools/bio/search-pharmgkb.d.ts +0 -17
- package/dist/tools/bio/search-pharmgkb.js +0 -52
package/dist/state/index.js
CHANGED
|
@@ -10,11 +10,11 @@ export { initCortexState } from "./init.js";
|
|
|
10
10
|
export { upsertAnalysis, resolveAnalysisBilling, suspendAnalysis, resumeAnalysis, loadAnalysisStatus } from "./analyses.js";
|
|
11
11
|
export { upsertArtifact, upsertArtifacts, queryInputArtifacts, queryStepArtifactPaths, queryUnsyncedStepArtifacts, updateArtifactId, updateFileIds, countArtifactsForRun, } from "./artifacts.js";
|
|
12
12
|
export { insertRun, reserveRunById, RunDedupCollisionError, RunIdentityCollisionError, updateRunStatus, promoteFailedToPartial, setRunMandate, // oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
13
|
-
setRunSynthesisOutcome, queryRun, queryActiveRun, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
|
|
13
|
+
setRunSynthesisOutcome, queryRun, queryActiveRun, queryNonTerminalRunsByAnalysis, queryRunsForInspection, queryRunsByAnalysis, queryRunsByThread, } from "./runs.js";
|
|
14
14
|
export { insertStepExecution, seedStepExecutions, sweepPendingStepExecutions, updateStepExecution, queryStepsByRun } from "./step-executions.js";
|
|
15
15
|
export { setSandboxRef, setActiveExecId, clearSandboxRef, queryActiveSandboxes, reconcileReapedSandbox } from "./active-sandboxes.js";
|
|
16
16
|
export { insertPlan, upsertPlan, loadPlan } from "./plans.js";
|
|
17
|
-
export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, } from "./data-profile.js";
|
|
17
|
+
export { tryStartDataProfile, tryRetryDataProfile, tryRerunDataProfile, completeDataProfile, failDataProfile, expireStaleDataProfile, reconcileOrphanedDataProfile, clearDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, } from "./data-profile.js";
|
|
18
18
|
export { queryRunCountsByAnalyses, queryThreadCountsByAnalyses, queryDataProfileStatusByAnalyses } from "./analyses-metrics.js";
|
|
19
19
|
export { AnalysisStateRowSchema, ArtifactRole, ArtifactRowSchema, RunStatus, StepExecutionStatus, SynthesisStatus, CortexRunRowSchema, StepExecutionRowSchema, CortexPlanRowSchema, } from "./schema.js";
|
|
20
20
|
export { insertAssessment, updateProgress, setDossier, markFailed, getAssessment, listAssessmentsByOrg, softDeleteAssessment, TargetAssessmentStatusSchema, TargetAssessmentErrorSchema, TargetAssessmentRowSchema, } from "./target-assessments.js";
|
package/dist/state/init.js
CHANGED
|
@@ -27,10 +27,22 @@ CREATE TABLE IF NOT EXISTS cortex_analysis_state (
|
|
|
27
27
|
data_profile_started_at TEXT,
|
|
28
28
|
data_profile_completed_at TEXT,
|
|
29
29
|
data_profile_result JSONB,
|
|
30
|
+
data_profile_workflow_id TEXT,
|
|
30
31
|
created_at TEXT NOT NULL,
|
|
31
32
|
updated_at TEXT NOT NULL
|
|
32
33
|
);
|
|
33
34
|
|
|
35
|
+
-- DBOS workflow id of the profile attempt that owns the row — what a consumer
|
|
36
|
+
-- subscribes to in order to observe that profile's activity. Written by the
|
|
37
|
+
-- workflow body, not the trigger: the ledger claim happens before the workflow id
|
|
38
|
+
-- is minted, so only the body knows the id of the attempt that actually started.
|
|
39
|
+
-- Nullable, and absence is a normal state rather than a defect, with two ordinary
|
|
40
|
+
-- causes: a row claimed whose body has not yet recorded its id, and a row written
|
|
41
|
+
-- before this column existed. Both mean "this profile's stream is not
|
|
42
|
+
-- addressable", which is true in each case — hence no backfill.
|
|
43
|
+
ALTER TABLE cortex_analysis_state
|
|
44
|
+
ADD COLUMN IF NOT EXISTS data_profile_workflow_id TEXT;
|
|
45
|
+
|
|
34
46
|
-- File registry — artifact_id (provenance) + file_id (S3), cross-run.
|
|
35
47
|
CREATE TABLE IF NOT EXISTS cortex_artifacts (
|
|
36
48
|
analysis_id TEXT NOT NULL,
|
|
@@ -207,12 +219,20 @@ CREATE INDEX IF NOT EXISTS idx_cortex_regulatory_chunks_metadata
|
|
|
207
219
|
-- workflow and sandbox agent loops use the DBOS step cache, never this
|
|
208
220
|
-- table (see the harness-thread-store spec). The (thread_id, seq) primary key already serves
|
|
209
221
|
-- (thread_id, seq DESC) reads, so no separate index is needed.
|
|
222
|
+
--
|
|
223
|
+
-- message_envelope is JSON, not JSONB, and must stay that way. JSONB stores a
|
|
224
|
+
-- re-sorted parse tree rather than the document it was given, so a tool call's
|
|
225
|
+
-- input — and every other free-form payload, which has no schema to be
|
|
226
|
+
-- normalized back against on read — comes back key-reordered. Those bytes are
|
|
227
|
+
-- re-sent to the provider on the next turn, where a reordered prefix misses the
|
|
228
|
+
-- prompt cache. JSON stores the text verbatim. Nothing indexes this column, and
|
|
229
|
+
-- the JSON operators retractLastTurn uses work the same on either type.
|
|
210
230
|
CREATE TABLE IF NOT EXISTS messages (
|
|
211
231
|
thread_id TEXT NOT NULL,
|
|
212
232
|
seq BIGINT NOT NULL,
|
|
213
233
|
role TEXT,
|
|
214
234
|
content_jsonb JSONB,
|
|
215
|
-
message_envelope
|
|
235
|
+
message_envelope JSON,
|
|
216
236
|
tokens INTEGER NOT NULL,
|
|
217
237
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
218
238
|
PRIMARY KEY (thread_id, seq)
|
|
@@ -411,7 +431,22 @@ export async function initCortexState(pool, injected) {
|
|
|
411
431
|
// carry the NOT NULL floor, which would reject a clear. Idempotent:
|
|
412
432
|
// dropping an absent NOT NULL no-ops, so this is a no-op on fresh DBs.
|
|
413
433
|
"ALTER TABLE cortex_analysis_state ALTER COLUMN data_profile_status DROP NOT NULL",
|
|
414
|
-
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS message_envelope
|
|
434
|
+
"ALTER TABLE messages ADD COLUMN IF NOT EXISTS message_envelope JSON",
|
|
435
|
+
// Demote an existing JSONB envelope column to JSON (see the CREATE
|
|
436
|
+
// TABLE comment). Type-guarded because ALTER ... TYPE rewrites the
|
|
437
|
+
// whole table and this runs at every boot; scoped to
|
|
438
|
+
// current_schema() so parallel test schemas never see each other.
|
|
439
|
+
// Rows already written as JSONB carry over as they stand — their
|
|
440
|
+
// original key order is not recoverable.
|
|
441
|
+
`DO $$ BEGIN
|
|
442
|
+
IF EXISTS (
|
|
443
|
+
SELECT 1 FROM information_schema.columns
|
|
444
|
+
WHERE table_schema = current_schema() AND table_name = 'messages'
|
|
445
|
+
AND column_name = 'message_envelope' AND data_type = 'jsonb'
|
|
446
|
+
) THEN
|
|
447
|
+
ALTER TABLE messages ALTER COLUMN message_envelope TYPE JSON USING message_envelope::text::json;
|
|
448
|
+
END IF;
|
|
449
|
+
END $$`,
|
|
415
450
|
// Grant key an 'always' records the standing grant under. Nullable
|
|
416
451
|
// and unbackfilled: every new row writes it (command when absent), and
|
|
417
452
|
// orphaned legacy pending rows are swept to 'expired' before any answer
|
package/dist/state/runs.d.ts
CHANGED
|
@@ -26,6 +26,11 @@ export interface RunReservation {
|
|
|
26
26
|
readonly row: CortexRunRow;
|
|
27
27
|
readonly inserted: boolean;
|
|
28
28
|
}
|
|
29
|
+
/** Bounded run rows paired with the true count before limit/offset. */
|
|
30
|
+
export interface RunPage {
|
|
31
|
+
readonly runs: CortexRunRow[];
|
|
32
|
+
readonly total: number;
|
|
33
|
+
}
|
|
29
34
|
export declare class RunIdentityCollisionError extends Error {
|
|
30
35
|
readonly runId: string;
|
|
31
36
|
constructor(runId: string);
|
|
@@ -59,6 +64,24 @@ export declare function reserveRunById(pool: Querier, input: InsertRunInput): Re
|
|
|
59
64
|
* most one match.
|
|
60
65
|
*/
|
|
61
66
|
export declare function queryActiveRun(pool: Querier, analysisId: string, planId: string | null): ResultAsync<CortexRunRow | null, DbError>;
|
|
67
|
+
/**
|
|
68
|
+
* Every NON-TERMINAL run of an analysis, newest-first and deliberately unbounded.
|
|
69
|
+
*
|
|
70
|
+
* The counterpart to {@link queryRunsByAnalysis}, which answers "the newest N runs" — a windowed
|
|
71
|
+
* question, and the wrong one for a host driving live surfaces. Ordering by `started_at DESC` and
|
|
72
|
+
* capping means the run most worth watching is the first to fall out: a long analysis still going
|
|
73
|
+
* while ten short ones start and finish after it leaves the window entirely, so a host reading only
|
|
74
|
+
* that window loses its progress AND never learns it terminated.
|
|
75
|
+
*
|
|
76
|
+
* Unbounded is safe here in a way it is not for the general listing: this result is capped by the
|
|
77
|
+
* analysis's live concurrency, not by its history, and a run leaves the set permanently when it
|
|
78
|
+
* finishes.
|
|
79
|
+
*
|
|
80
|
+
* The status set is the one {@link queryActiveRun} uses — non-terminal means still running, or
|
|
81
|
+
* suspended awaiting funds. Keep the two in step: a new non-terminal status added to one and not the
|
|
82
|
+
* other makes a live run invisible to exactly one of its readers.
|
|
83
|
+
*/
|
|
84
|
+
export declare function queryActiveRunsByAnalysis(pool: Querier, analysisId: string): ResultAsync<CortexRunRow[], DbError>;
|
|
62
85
|
export declare function updateRunStatus(pool: Querier, runId: string, status: "running" | "completed" | "partial" | "failed" | "canceled" | "suspended_insufficient_funds", error?: string | null): ResultAsync<void, DbError>;
|
|
63
86
|
export declare function promoteFailedToPartial(pool: Querier, runId: string): ResultAsync<boolean, DbError>;
|
|
64
87
|
export declare function queryRun(pool: Querier, runId: string): ResultAsync<CortexRunRow | null, DbError>;
|
|
@@ -66,6 +89,22 @@ export declare function queryRunsByAnalysis(pool: Querier, analysisId: string, {
|
|
|
66
89
|
limit?: number;
|
|
67
90
|
offset?: number;
|
|
68
91
|
}): ResultAsync<CortexRunRow[], DbError>;
|
|
92
|
+
/**
|
|
93
|
+
* Read the analysis's non-terminal run activity without coupling it to a
|
|
94
|
+
* conversation thread. The total is counted before the detail limit so a
|
|
95
|
+
* bounded prompt projection can disclose omitted rows.
|
|
96
|
+
*/
|
|
97
|
+
export declare function queryNonTerminalRunsByAnalysis(pool: Querier, analysisId: string, limit?: number): ResultAsync<RunPage, DbError>;
|
|
98
|
+
/**
|
|
99
|
+
* Read one stable inspection page: running first, then suspended, then
|
|
100
|
+
* terminal history. This is deliberately separate from
|
|
101
|
+
* {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
|
|
102
|
+
* contract.
|
|
103
|
+
*/
|
|
104
|
+
export declare function queryRunsForInspection(pool: Querier, analysisId: string, { limit, offset }?: {
|
|
105
|
+
limit?: number;
|
|
106
|
+
offset?: number;
|
|
107
|
+
}): ResultAsync<RunPage, DbError>;
|
|
69
108
|
export declare function queryRunsByThread(pool: Querier, analysisId: string, threadId: string, { limit, offset }?: {
|
|
70
109
|
limit?: number;
|
|
71
110
|
offset?: number;
|
package/dist/state/runs.js
CHANGED
|
@@ -139,6 +139,38 @@ export function queryActiveRun(pool, analysisId, planId) {
|
|
|
139
139
|
return row ? mapRunRow(row) : null;
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Every NON-TERMINAL run of an analysis, newest-first and deliberately unbounded.
|
|
144
|
+
*
|
|
145
|
+
* The counterpart to {@link queryRunsByAnalysis}, which answers "the newest N runs" — a windowed
|
|
146
|
+
* question, and the wrong one for a host driving live surfaces. Ordering by `started_at DESC` and
|
|
147
|
+
* capping means the run most worth watching is the first to fall out: a long analysis still going
|
|
148
|
+
* while ten short ones start and finish after it leaves the window entirely, so a host reading only
|
|
149
|
+
* that window loses its progress AND never learns it terminated.
|
|
150
|
+
*
|
|
151
|
+
* Unbounded is safe here in a way it is not for the general listing: this result is capped by the
|
|
152
|
+
* analysis's live concurrency, not by its history, and a run leaves the set permanently when it
|
|
153
|
+
* finishes.
|
|
154
|
+
*
|
|
155
|
+
* The status set is the one {@link queryActiveRun} uses — non-terminal means still running, or
|
|
156
|
+
* suspended awaiting funds. Keep the two in step: a new non-terminal status added to one and not the
|
|
157
|
+
* other makes a live run invisible to exactly one of its readers.
|
|
158
|
+
*/
|
|
159
|
+
export function queryActiveRunsByAnalysis(pool, analysisId) {
|
|
160
|
+
return tryQuery("runs.queryActiveRunsByAnalysis", async () => {
|
|
161
|
+
const result = await pool.query({
|
|
162
|
+
text: `SELECT run_id, analysis_id, thread_id, workflow_name,
|
|
163
|
+
status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
|
|
164
|
+
mandate_jti, mandate_expires_at, plan_id -- oss-core-managed-ok: run-mandate ledger (nullable; OSS leaves null)
|
|
165
|
+
FROM cortex_runs
|
|
166
|
+
WHERE analysis_id = $1
|
|
167
|
+
AND status IN ('running','suspended_insufficient_funds')
|
|
168
|
+
ORDER BY started_at DESC`,
|
|
169
|
+
values: [analysisId],
|
|
170
|
+
});
|
|
171
|
+
return result.rows.map(mapRunRow);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
142
174
|
export function updateRunStatus(pool, runId, status, error) {
|
|
143
175
|
const now = new Date().toISOString();
|
|
144
176
|
const isTerminal = status === "completed" || status === "partial" || status === "failed" || status === "canceled";
|
|
@@ -189,6 +221,77 @@ export function queryRunsByAnalysis(pool, analysisId, { limit = 50, offset = 0 }
|
|
|
189
221
|
return result.rows.map(mapRunRow);
|
|
190
222
|
});
|
|
191
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Read the analysis's non-terminal run activity without coupling it to a
|
|
226
|
+
* conversation thread. The total is counted before the detail limit so a
|
|
227
|
+
* bounded prompt projection can disclose omitted rows.
|
|
228
|
+
*/
|
|
229
|
+
export function queryNonTerminalRunsByAnalysis(pool, analysisId, limit = 20) {
|
|
230
|
+
return tryQuery("runs.queryNonTerminalRunsByAnalysis", async () => {
|
|
231
|
+
const countResult = await pool.query({
|
|
232
|
+
text: `SELECT COUNT(*)::int AS total
|
|
233
|
+
FROM cortex_runs
|
|
234
|
+
WHERE analysis_id = $1
|
|
235
|
+
AND status IN ('running','suspended_insufficient_funds')`,
|
|
236
|
+
values: [analysisId],
|
|
237
|
+
});
|
|
238
|
+
const rowsResult = await pool.query({
|
|
239
|
+
text: `SELECT run_id, analysis_id, thread_id, workflow_name,
|
|
240
|
+
status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
|
|
241
|
+
mandate_jti, mandate_expires_at, plan_id
|
|
242
|
+
FROM cortex_runs
|
|
243
|
+
WHERE analysis_id = $1
|
|
244
|
+
AND status IN ('running','suspended_insufficient_funds')
|
|
245
|
+
ORDER BY CASE status
|
|
246
|
+
WHEN 'running' THEN 0
|
|
247
|
+
WHEN 'suspended_insufficient_funds' THEN 1
|
|
248
|
+
ELSE 2
|
|
249
|
+
END,
|
|
250
|
+
started_at DESC,
|
|
251
|
+
run_id DESC
|
|
252
|
+
LIMIT $2`,
|
|
253
|
+
values: [analysisId, limit],
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
runs: rowsResult.rows.map(mapRunRow),
|
|
257
|
+
total: Number(countResult.rows[0]?.total ?? 0),
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Read one stable inspection page: running first, then suspended, then
|
|
263
|
+
* terminal history. This is deliberately separate from
|
|
264
|
+
* {@link queryRunsByAnalysis}, whose chronological order is an embedder/UI
|
|
265
|
+
* contract.
|
|
266
|
+
*/
|
|
267
|
+
export function queryRunsForInspection(pool, analysisId, { limit = 50, offset = 0 } = {}) {
|
|
268
|
+
return tryQuery("runs.queryRunsForInspection", async () => {
|
|
269
|
+
const countResult = await pool.query({
|
|
270
|
+
text: "SELECT COUNT(*)::int AS total FROM cortex_runs WHERE analysis_id = $1",
|
|
271
|
+
values: [analysisId],
|
|
272
|
+
});
|
|
273
|
+
const rowsResult = await pool.query({
|
|
274
|
+
text: `SELECT run_id, analysis_id, thread_id, workflow_name,
|
|
275
|
+
status, started_at, completed_at, error, synthesis_status, synthesis_reason, parts,
|
|
276
|
+
mandate_jti, mandate_expires_at, plan_id
|
|
277
|
+
FROM cortex_runs
|
|
278
|
+
WHERE analysis_id = $1
|
|
279
|
+
ORDER BY CASE status
|
|
280
|
+
WHEN 'running' THEN 0
|
|
281
|
+
WHEN 'suspended_insufficient_funds' THEN 1
|
|
282
|
+
ELSE 2
|
|
283
|
+
END,
|
|
284
|
+
started_at DESC,
|
|
285
|
+
run_id DESC
|
|
286
|
+
LIMIT $2 OFFSET $3`,
|
|
287
|
+
values: [analysisId, limit, offset],
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
runs: rowsResult.rows.map(mapRunRow),
|
|
291
|
+
total: Number(countResult.rows[0]?.total ?? 0),
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
}
|
|
192
295
|
export function queryRunsByThread(pool, analysisId, threadId, { limit = 50, offset = 0 } = {}) {
|
|
193
296
|
return tryQuery("runs.queryRunsByThread", async () => {
|
|
194
297
|
const result = await pool.query({
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data-profile workflow's activity emissions, as a pure factory over a raw
|
|
3
|
+
* stream write.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the workflow body for the same reason `createLineageCollector`
|
|
6
|
+
* and `buildDriftSignature` are: a DBOS workflow body is not reachable from a
|
|
7
|
+
* unit test — it needs a launched engine, a sandbox, a scripted model, and an
|
|
8
|
+
* embedder — so the decidable part is lifted out and tested on its own. What is
|
|
9
|
+
* decidable here is the whole observable contract of this capability: which
|
|
10
|
+
* phase each moment of a profile reports, and in what words. Those phrases are
|
|
11
|
+
* what a user reads, so they are the one part of the work that must not be left
|
|
12
|
+
* to drift.
|
|
13
|
+
*
|
|
14
|
+
* What stays in the body is only control flow — where each call sits relative to
|
|
15
|
+
* the ledger writes. That is not extractable, and the ordering property it
|
|
16
|
+
* carries (exactly one terminal emission) is guaranteed structurally instead:
|
|
17
|
+
* there is exactly one `complete` call site and it is the last statement of the
|
|
18
|
+
* success path, after the terminal ledger write and the authorization revoke, so
|
|
19
|
+
* anything that throws on the way there reaches the failure path having emitted
|
|
20
|
+
* no terminal at all.
|
|
21
|
+
*/
|
|
22
|
+
import type { Logger } from "../lib/logger.js";
|
|
23
|
+
/**
|
|
24
|
+
* Writes one part to the run-event stream. Injected rather than imported so this
|
|
25
|
+
* module stays free of the durability engine and so a test can record what a
|
|
26
|
+
* profile would have reported without launching one.
|
|
27
|
+
*/
|
|
28
|
+
export type ActivityWrite = (part: unknown) => Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* The profile's activity channel. One method per moment the body reports, rather
|
|
31
|
+
* than a generic `emit(phase, text)`: the phrases are normative, and a call site
|
|
32
|
+
* that passes its own string is a call site that can drift from the spec.
|
|
33
|
+
*
|
|
34
|
+
* Every method resolves even when the underlying write fails — see
|
|
35
|
+
* {@link createProfileActivityEmitter}.
|
|
36
|
+
*
|
|
37
|
+
* Five of the contract's phases have no method here, and their absence is a
|
|
38
|
+
* decision rather than an oversight: `generating-metadata` and
|
|
39
|
+
* `generating-summary` describe the sandbox-step's post-agent pipeline, which a
|
|
40
|
+
* profile does not run (its agent delivers everything through `submit_profile`);
|
|
41
|
+
* `persisting` is defined as step bytes uploading to an artifact store, and a
|
|
42
|
+
* profile's durable products are the vector index and the ledger row, so it would
|
|
43
|
+
* describe nothing; `retrying` needs a retry loop, and a profile failure is
|
|
44
|
+
* terminal with recovery being a fresh attempt; `warning` needs a non-fatal
|
|
45
|
+
* user-facing warning channel, and the body's two soft conditions (an empty
|
|
46
|
+
* manifest, a fallback file description) are logged because neither warrants
|
|
47
|
+
* interrupting the activity line.
|
|
48
|
+
*/
|
|
49
|
+
export interface ProfileActivityEmitter {
|
|
50
|
+
/** Reported before the sandbox is created — the longest wait in a profile, and the one that precedes the agent entirely. */
|
|
51
|
+
sandboxInit(): Promise<void>;
|
|
52
|
+
/** Reported once the sandbox is ready, covering the gap before the agent's first tool call. */
|
|
53
|
+
agentStarting(): Promise<void>;
|
|
54
|
+
/** Reported per tool call the profiler agent makes, phrased by the shared translator. */
|
|
55
|
+
forTool(name: string, input: unknown): Promise<void>;
|
|
56
|
+
/** Reported for the vector-store pass that indexes per-file descriptions. */
|
|
57
|
+
indexing(): Promise<void>;
|
|
58
|
+
/** The success terminal. Emitted only after the terminal ledger write has landed. */
|
|
59
|
+
complete(): Promise<void>;
|
|
60
|
+
/** The failure terminal, carrying the same user-safe reason the ledger receives. */
|
|
61
|
+
failed(reason: string): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
/** Identity the emitted parts carry — the workflow's synthetic frame, not a real run. */
|
|
64
|
+
export interface ProfileActivityFrame {
|
|
65
|
+
/**
|
|
66
|
+
* The constant literal the data-profile workflow uses as its run id. It is the
|
|
67
|
+
* same string for every analysis, so it identifies nothing; consumers are
|
|
68
|
+
* required not to key on it (see the data-profile-observation spec). It is
|
|
69
|
+
* carried because the part contract requires the field and because this is the
|
|
70
|
+
* frame the workflow already uses for its sandbox identity and scratch path.
|
|
71
|
+
*/
|
|
72
|
+
readonly runId: string;
|
|
73
|
+
/** The constant literal step id of the profile's single synthetic step. */
|
|
74
|
+
readonly stepId: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the profile's activity channel over `write`.
|
|
78
|
+
*
|
|
79
|
+
* Every emission is awaited by its caller in body order, because `DBOS.writeStream`
|
|
80
|
+
* allocates a function id and an unawaited write would race the next real operation
|
|
81
|
+
* for the counter, desynchronising the recorded sequence on replay.
|
|
82
|
+
*
|
|
83
|
+
* A failing write is swallowed and logged, never propagated: observation is a
|
|
84
|
+
* diagnostic channel, and a dropped frame must not fail a profile that is otherwise
|
|
85
|
+
* succeeding. This mirrors the sandbox-step producer's stance on the same write.
|
|
86
|
+
*/
|
|
87
|
+
export declare function createProfileActivityEmitter(write: ActivityWrite, frame: ProfileActivityFrame, logger: Logger): ProfileActivityEmitter;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The data-profile workflow's activity emissions, as a pure factory over a raw
|
|
3
|
+
* stream write.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from the workflow body for the same reason `createLineageCollector`
|
|
6
|
+
* and `buildDriftSignature` are: a DBOS workflow body is not reachable from a
|
|
7
|
+
* unit test — it needs a launched engine, a sandbox, a scripted model, and an
|
|
8
|
+
* embedder — so the decidable part is lifted out and tested on its own. What is
|
|
9
|
+
* decidable here is the whole observable contract of this capability: which
|
|
10
|
+
* phase each moment of a profile reports, and in what words. Those phrases are
|
|
11
|
+
* what a user reads, so they are the one part of the work that must not be left
|
|
12
|
+
* to drift.
|
|
13
|
+
*
|
|
14
|
+
* What stays in the body is only control flow — where each call sits relative to
|
|
15
|
+
* the ledger writes. That is not extractable, and the ordering property it
|
|
16
|
+
* carries (exactly one terminal emission) is guaranteed structurally instead:
|
|
17
|
+
* there is exactly one `complete` call site and it is the last statement of the
|
|
18
|
+
* success path, after the terminal ledger write and the authorization revoke, so
|
|
19
|
+
* anything that throws on the way there reaches the failure path having emitted
|
|
20
|
+
* no terminal at all.
|
|
21
|
+
*/
|
|
22
|
+
import { activityForTool, stepPartId } from "../sandbox/sandbox-step-translate.js";
|
|
23
|
+
/**
|
|
24
|
+
* Build the profile's activity channel over `write`.
|
|
25
|
+
*
|
|
26
|
+
* Every emission is awaited by its caller in body order, because `DBOS.writeStream`
|
|
27
|
+
* allocates a function id and an unawaited write would race the next real operation
|
|
28
|
+
* for the counter, desynchronising the recorded sequence on replay.
|
|
29
|
+
*
|
|
30
|
+
* A failing write is swallowed and logged, never propagated: observation is a
|
|
31
|
+
* diagnostic channel, and a dropped frame must not fail a profile that is otherwise
|
|
32
|
+
* succeeding. This mirrors the sandbox-step producer's stance on the same write.
|
|
33
|
+
*/
|
|
34
|
+
export function createProfileActivityEmitter(write, frame, logger) {
|
|
35
|
+
// One reconciling id for the profile's single step, so the run-stream fold
|
|
36
|
+
// collapses every phase transition latest-wins onto one frame rather than
|
|
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) => {
|
|
41
|
+
try {
|
|
42
|
+
await write({ type: "data-step-activity", id, runId: frame.runId, stepId: frame.stepId, phase, activity });
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
logger.warn("run-event write failed (non-fatal)", { phase, ...logger.errorFields(err) });
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
sandboxInit: () => emit("sandbox-init", "Starting sandbox"),
|
|
50
|
+
agentStarting: () => emit("executing", "Running data-profiler"),
|
|
51
|
+
forTool: (name, input) => emit("executing", activityForTool(name, input)),
|
|
52
|
+
indexing: () => emit("indexing", "Indexing input descriptions for search"),
|
|
53
|
+
complete: () => emit("complete", "Profile complete"),
|
|
54
|
+
failed: (reason) => emit("failed", reason),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -33,7 +33,8 @@ import { generateExecutionId } from "../sandbox/execution-id.js";
|
|
|
33
33
|
import { mintSandboxIdentity } from "../sandbox/identity.js";
|
|
34
34
|
import { createVectorStore } from "../state/vector-store.js";
|
|
35
35
|
import { ProfilerOutputSchema } from "../schemas/data-profile-schemas.js";
|
|
36
|
-
import { completeDataProfile, failDataProfile, loadDataProfileStatus, loadSeedInputFileIds, tryRerunDataProfile, tryStartDataProfile, upsertArtifacts, } from "../state/index.js";
|
|
36
|
+
import { completeDataProfile, failDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, tryRerunDataProfile, tryStartDataProfile, upsertArtifacts, } from "../state/index.js";
|
|
37
|
+
import { createProfileActivityEmitter } from "./data-profile-activity.js";
|
|
37
38
|
import { ensureSearchIndex, searchIndexName } from "../workspace/search-config.js";
|
|
38
39
|
/** Synthetic run/step literal for the data-profile workflow. */
|
|
39
40
|
const DATA_PROFILE_RUN_LITERAL = "data-profile";
|
|
@@ -135,7 +136,29 @@ export async function runDataProfileBody(input, deps) {
|
|
|
135
136
|
// Cortex-owned and must be revoked here.
|
|
136
137
|
const { analysisId, runSession, ownsMandate = true, stagedInputs } = input; // oss-core-managed-ok
|
|
137
138
|
const authorization = { runSession, ownsMandate }; // oss-core-managed-ok
|
|
139
|
+
// The profile's activity channel. `DBOS.writeStream` is body-only, so the write is bound here
|
|
140
|
+
// while every phase and phrase lives in the emitter — the phrases are the observable contract of
|
|
141
|
+
// this capability, and a body that composed its own strings is a body they can drift from.
|
|
142
|
+
// The frame is the workflow's synthetic one: both values are constants shared by every analysis,
|
|
143
|
+
// so they identify nothing and consumers are required not to key on them.
|
|
144
|
+
const activity = createProfileActivityEmitter((part) => DBOS.writeStream("events", part), { runId: DATA_PROFILE_RUN_LITERAL, stepId: DATA_PROFILE_STEP_LITERAL }, logger);
|
|
138
145
|
try {
|
|
146
|
+
// Record which workflow owns this profile before anything else, so a consumer that resolves
|
|
147
|
+
// the id from the ledger can subscribe from the earliest possible moment. Guarded on a real
|
|
148
|
+
// ambient id: this body is also driven directly (outside a workflow), where the synthetic
|
|
149
|
+
// fallback used further down names no workflow at all and must never reach the ledger.
|
|
150
|
+
const ownWorkflowId = DBOS.workflowID;
|
|
151
|
+
if (ownWorkflowId !== undefined) {
|
|
152
|
+
await DBOS.runStep(async () => {
|
|
153
|
+
// A refused CAS is a normal in-band outcome, not a failure: the row left
|
|
154
|
+
// `running` underneath us (cleared, or stale-expired and re-claimed), which is
|
|
155
|
+
// the ledger correctly moving on. There is nothing to repair, and the profile's
|
|
156
|
+
// own work is unaffected — only its observability is.
|
|
157
|
+
if (!unwrapOrThrow(await recordDataProfileWorkflowId(deps.pool, analysisId, ownWorkflowId))) {
|
|
158
|
+
logger.warn("workflow-id record skipped: ledger row not running (cleared or re-claimed concurrently)");
|
|
159
|
+
}
|
|
160
|
+
}, { name: "record-workflow-id" });
|
|
161
|
+
}
|
|
139
162
|
// 1. Input files are already staged under data/inputs/ (see the data-profile-init spec); the
|
|
140
163
|
// embedder populated the tree and handed us this manifest in the workflow
|
|
141
164
|
// input. The body never downloads.
|
|
@@ -144,6 +167,9 @@ export async function runDataProfileBody(input, deps) {
|
|
|
144
167
|
if (!unwrapOrThrow(await completeDataProfile(deps.pool, analysisId)))
|
|
145
168
|
logTerminalNoop(logger, analysisId, "completion");
|
|
146
169
|
await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
|
|
170
|
+
// This is a terminal completion like any other, so it reports one — a consumer watching
|
|
171
|
+
// an empty-manifest profile sees it settle rather than seeing the stream simply stop.
|
|
172
|
+
await activity.complete();
|
|
147
173
|
return;
|
|
148
174
|
}
|
|
149
175
|
// 2. Register staged files in cortex_artifacts
|
|
@@ -193,6 +219,11 @@ export async function runDataProfileBody(input, deps) {
|
|
|
193
219
|
`All file paths in your response must be relative to the analysis root and must EXACTLY match the paths listed above.`,
|
|
194
220
|
].join("\n");
|
|
195
221
|
logger.info("starting sandbox", { executionId });
|
|
222
|
+
// Reported BEFORE the container exists, deliberately: provisioning is the longest single
|
|
223
|
+
// operation in a profile and it precedes the agent entirely, so a body that reported only
|
|
224
|
+
// from the agent loop would leave the longest wait of all unreported — the same defect the
|
|
225
|
+
// run panel's activity readout was built to remove.
|
|
226
|
+
await activity.sandboxInit();
|
|
196
227
|
const sandbox = await deps.sandboxClient.createSandbox({
|
|
197
228
|
runId: DATA_PROFILE_RUN_LITERAL,
|
|
198
229
|
stepId: DATA_PROFILE_STEP_LITERAL,
|
|
@@ -247,13 +278,27 @@ export async function runDataProfileBody(input, deps) {
|
|
|
247
278
|
const baseAgent = createDataProfilerAgent(sandboxAgentDeps);
|
|
248
279
|
const agentDef = { ...baseAgent, tools: [...baseAgent.tools, submitProfileTool] };
|
|
249
280
|
const signal = new AbortController().signal;
|
|
281
|
+
// Covers the gap between a ready sandbox and the agent's first tool call, which would
|
|
282
|
+
// otherwise still read as `Starting sandbox` long after the container was up. The run
|
|
283
|
+
// path's `Running ${agentId}` serves the same purpose at the same point.
|
|
284
|
+
await activity.agentStarting();
|
|
250
285
|
await runToTerminal(agentDef, [{ role: "user", content: prompt }], childSession, {
|
|
251
286
|
provider: deps.provider,
|
|
252
287
|
signal,
|
|
253
|
-
|
|
288
|
+
// Every tool call the profiler makes becomes live activity. Only `tool-started`
|
|
289
|
+
// is forwarded: the loop's other events (iteration boundaries, model deltas,
|
|
290
|
+
// tool completions) describe machinery rather than work, and the panel this
|
|
291
|
+
// feeds carries a single line whose value is that it always names what is
|
|
292
|
+
// happening now. Awaited in body order, because `DBOS.writeStream` allocates a
|
|
293
|
+
// function id and an unawaited write would race the next operation for the
|
|
294
|
+
// counter and desynchronise the recorded sequence on replay.
|
|
295
|
+
emit: async (event) => {
|
|
296
|
+
if (event.type === "tool-started")
|
|
297
|
+
await activity.forTool(event.name, event.input);
|
|
298
|
+
},
|
|
254
299
|
runStep: durableStep,
|
|
255
|
-
}, {
|
|
256
300
|
resolved: () => capturedProfile !== null,
|
|
301
|
+
}, {
|
|
257
302
|
tools: [submitProfileTool],
|
|
258
303
|
nudge: "You stopped without calling submit_profile, so no profile was " +
|
|
259
304
|
"recorded. Call submit_profile now with structured metadata for " +
|
|
@@ -264,6 +309,11 @@ export async function runDataProfileBody(input, deps) {
|
|
|
264
309
|
}
|
|
265
310
|
const profilerData = capturedProfile;
|
|
266
311
|
// 4. Index into vector store — real files only, consistent type metadata
|
|
312
|
+
//
|
|
313
|
+
// Reported as `indexing`, not `persisting`: the contract defines `persisting` as step
|
|
314
|
+
// bytes uploading to an artifact store, and a profile uploads nothing — its durable
|
|
315
|
+
// products are this vector index and the ledger row.
|
|
316
|
+
await activity.indexing();
|
|
267
317
|
const profilerByPath = new Map(profilerData.files.map((f) => [f.path, f]));
|
|
268
318
|
const normPath = (p) => p
|
|
269
319
|
.replace(/\/+/g, "/")
|
|
@@ -326,6 +376,13 @@ export async function runDataProfileBody(input, deps) {
|
|
|
326
376
|
logTerminalNoop(logger, analysisId, "completion");
|
|
327
377
|
}
|
|
328
378
|
await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
|
|
379
|
+
// LAST statement of the success path, and that placement is the guarantee that exactly
|
|
380
|
+
// one terminal activity is ever emitted. Everything that could still throw — the ledger
|
|
381
|
+
// write above, the revoke — is already behind us, so any failure on the way here reaches
|
|
382
|
+
// the catch having emitted no terminal at all and reports `failed` alone. Emitted any
|
|
383
|
+
// earlier, a failing write would produce BOTH terminals and leave the fold's winner
|
|
384
|
+
// decided by arrival order. The sandbox teardown below emits nothing for the same reason.
|
|
385
|
+
await activity.complete();
|
|
329
386
|
}
|
|
330
387
|
finally {
|
|
331
388
|
try {
|
|
@@ -338,9 +395,14 @@ export async function runDataProfileBody(input, deps) {
|
|
|
338
395
|
}
|
|
339
396
|
catch (err) {
|
|
340
397
|
logger.error("profile failed", logger.errorFields(err));
|
|
341
|
-
|
|
398
|
+
const reason = profileFailureReason(err);
|
|
399
|
+
if (!unwrapOrThrow(await failDataProfile(deps.pool, analysisId, reason)))
|
|
342
400
|
logTerminalNoop(logger, analysisId, "failure");
|
|
343
401
|
await deps.runAuthorizer.revoke(authorization, "data-profile-failed");
|
|
402
|
+
// The same bounded, user-safe line the ledger receives — never the raw error, whose paths and
|
|
403
|
+
// stack frames stay in the log record above. This is the only other terminal emission, so
|
|
404
|
+
// reaching it means the success path did not emit one.
|
|
405
|
+
await activity.failed(reason);
|
|
344
406
|
}
|
|
345
407
|
}
|
|
346
408
|
/**
|
|
@@ -28,7 +28,7 @@ export type ChemblOutput = {
|
|
|
28
28
|
readonly targets: ChemblTarget[];
|
|
29
29
|
};
|
|
30
30
|
export declare const chemblTool: import("../define-tool.js").Tool<{
|
|
31
|
-
action: "
|
|
31
|
+
action: "drug" | "targets" | "compounds" | "mechanism" | "bioactivity";
|
|
32
32
|
query?: string | undefined;
|
|
33
33
|
searchType?: "target" | "smiles" | "compound" | undefined;
|
|
34
34
|
chemblId?: string | undefined;
|