@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
|
@@ -12,11 +12,9 @@
|
|
|
12
12
|
* and it is why the record is served in full rather than summarized: an agent that
|
|
13
13
|
* cannot pull a fact here has no fallback but to re-derive it from the raw bytes.
|
|
14
14
|
*
|
|
15
|
-
* Bounded by construction. `inspect_run
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* reports `total` and `hasMore`: an elided tail is a fact the model can see and act on,
|
|
19
|
-
* never a silent truncation.
|
|
15
|
+
* Bounded by construction. Like `inspect_run`, the per-file scope is explicitly
|
|
16
|
+
* paged and always reports `total` and `hasMore`: an elided tail is a fact the
|
|
17
|
+
* model can see and act on, never a silent truncation.
|
|
20
18
|
*
|
|
21
19
|
* Lifecycle states are data variants, not errors (the `defineTool` contract): a missing,
|
|
22
20
|
* in-flight, failed, or stale profile is an ordinary, expected outcome the model must be
|
|
@@ -61,7 +59,17 @@ interface FilesOutput extends ProfileEnvelope {
|
|
|
61
59
|
readonly hasMore: boolean;
|
|
62
60
|
readonly files: DataProfileFile[];
|
|
63
61
|
}
|
|
64
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* No profile to serve — the honest lifecycle states, in the ok channel.
|
|
64
|
+
*
|
|
65
|
+
* Deliberately carries no staleness verdict. None of this variant's three
|
|
66
|
+
* producers retains a comparand: an analysis that was never profiled and one
|
|
67
|
+
* with no input files never had one, and `clearDataProfile` nulls
|
|
68
|
+
* `seed_input_file_ids` along with every other profile column, so a cleared row
|
|
69
|
+
* keeps no record of what it once covered. Such a field could only ever be
|
|
70
|
+
* constant here, and a constant field carries no information while implying
|
|
71
|
+
* that it does. `FailedOutput` omits one for the same reason — see its note.
|
|
72
|
+
*/
|
|
65
73
|
interface AbsentOutput {
|
|
66
74
|
readonly state: "absent";
|
|
67
75
|
readonly message: string;
|
|
@@ -71,9 +79,26 @@ interface PendingOutput {
|
|
|
71
79
|
readonly status: "pending" | "running";
|
|
72
80
|
readonly message: string;
|
|
73
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* A recorded failure with no profile to fall back on.
|
|
84
|
+
*
|
|
85
|
+
* Carries no staleness verdict, deliberately. Whether the failed attempt covered
|
|
86
|
+
* the files the analysis holds NOW is not derivable here: this tool reads one
|
|
87
|
+
* `cortex_analysis_state` row, the current input set is the embedder's knowledge,
|
|
88
|
+
* and `seed_input_file_ids` is the most recently seeded set rather than a
|
|
89
|
+
* per-attempt snapshot (`upsertAnalysis` overwrites it). A field reporting that
|
|
90
|
+
* verdict could only ever hold one value, which carries no information while
|
|
91
|
+
* implying that it does — and the tool description, being the whole of what an
|
|
92
|
+
* agent knows about this tool, must not advertise a distinction it cannot make.
|
|
93
|
+
*
|
|
94
|
+
* `failedAt` is reported instead: a fact the row actually holds, and one an agent
|
|
95
|
+
* that knows when the input set last changed can act on.
|
|
96
|
+
*/
|
|
74
97
|
interface FailedOutput {
|
|
75
98
|
readonly state: "failed";
|
|
76
99
|
readonly error: string | null;
|
|
100
|
+
/** When the failure was recorded; `null` when the row holds no time. */
|
|
101
|
+
readonly failedAt: string | null;
|
|
77
102
|
readonly message: string;
|
|
78
103
|
}
|
|
79
104
|
export type InspectDataProfileOutput = OverviewOutput | FilesOutput | AbsentOutput | PendingOutput | FailedOutput;
|
|
@@ -12,11 +12,9 @@
|
|
|
12
12
|
* and it is why the record is served in full rather than summarized: an agent that
|
|
13
13
|
* cannot pull a fact here has no fallback but to re-derive it from the raw bytes.
|
|
14
14
|
*
|
|
15
|
-
* Bounded by construction. `inspect_run
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* reports `total` and `hasMore`: an elided tail is a fact the model can see and act on,
|
|
19
|
-
* never a silent truncation.
|
|
15
|
+
* Bounded by construction. Like `inspect_run`, the per-file scope is explicitly
|
|
16
|
+
* paged and always reports `total` and `hasMore`: an elided tail is a fact the
|
|
17
|
+
* model can see and act on, never a silent truncation.
|
|
20
18
|
*
|
|
21
19
|
* Lifecycle states are data variants, not errors (the `defineTool` contract): a missing,
|
|
22
20
|
* in-flight, failed, or stale profile is an ordinary, expected outcome the model must be
|
|
@@ -57,6 +55,15 @@ function stalenessReasons(status, result) {
|
|
|
57
55
|
}
|
|
58
56
|
return reasons;
|
|
59
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The qualification the `failed` message must carry. A bare `failed: <error>`
|
|
60
|
+
* reads as a verdict on whatever files the agent is currently holding, and
|
|
61
|
+
* `message` is what an agent quotes back to the user — so the limit of what this
|
|
62
|
+
* row can establish belongs in the prose channel, not only in the field set.
|
|
63
|
+
*/
|
|
64
|
+
const FAILURE_SCOPE_NOTE = "This records an earlier attempt; this row cannot establish whether it covered the input files the analysis " +
|
|
65
|
+
"holds now, so do not read it as a verdict on the current data. Compare failedAt against when the input set " +
|
|
66
|
+
"last changed, and re-profile if they have moved apart.";
|
|
60
67
|
export function createInspectDataProfileTool(pool) {
|
|
61
68
|
return defineTool({
|
|
62
69
|
id: "inspect_data_profile",
|
|
@@ -72,7 +79,11 @@ export function createInspectDataProfileTool(pool) {
|
|
|
72
79
|
"always with the true total and hasMore, so you can see exactly what you have not read yet. " +
|
|
73
80
|
"The state field says what you got: 'ready'; 'stale' (a profile is returned but may not describe the current " +
|
|
74
81
|
"inputs — staleReason says why); 'pending' (profiling is still running); 'failed'; or 'absent' (never profiled, " +
|
|
75
|
-
"or the analysis has no input files)."
|
|
82
|
+
"or the analysis has no input files). " +
|
|
83
|
+
"A 'failed' state reports a PAST attempt, with failedAt naming when it was recorded — it is not a verdict on the " +
|
|
84
|
+
"input files you are holding now, and this tool cannot tell you whether that attempt covered them. Compare failedAt " +
|
|
85
|
+
"against when the input set last changed before you conclude anything: if the data moved since, say the profile " +
|
|
86
|
+
"needs re-running rather than diagnosing the data itself.",
|
|
76
87
|
inputSchema: z.object({
|
|
77
88
|
scope: z
|
|
78
89
|
.enum(["overview", "files"])
|
|
@@ -101,10 +112,15 @@ export function createInspectDataProfileTool(pool) {
|
|
|
101
112
|
const result = status.result;
|
|
102
113
|
if (!result) {
|
|
103
114
|
if (status.status === "failed") {
|
|
115
|
+
// A failure that DID leave a prior profile on the row never reaches
|
|
116
|
+
// here — it is served as `stale`, where `stalenessReasons` names the
|
|
117
|
+
// failed re-profile and compares the input sets through the single
|
|
118
|
+
// shared predicate. This branch is the case with nothing to compare.
|
|
104
119
|
return ok({
|
|
105
120
|
state: "failed",
|
|
106
121
|
error: status.error,
|
|
107
|
-
|
|
122
|
+
failedAt: status.completedAt,
|
|
123
|
+
message: `Data profiling failed and no earlier profile exists: ${status.error ?? "no reason recorded"}. ${FAILURE_SCOPE_NOTE}`,
|
|
108
124
|
});
|
|
109
125
|
}
|
|
110
126
|
if (status.status === "completed") {
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* inspectRun — inspect workflow runs for the current analysis.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Results are pull-only via this tool — the workflow does NOT write to the
|
|
9
|
-
* conversation thread on completion. The retired `run-result-writer` push
|
|
10
|
-
* payload (run status, step summaries, run synthesis, artifact paths) is
|
|
11
|
-
* surfaced here on demand: the run row + step `summaryPath` references +
|
|
12
|
-
* the `synthesisPath` for runs whose synthesis was produced let the
|
|
13
|
-
* conversation agent `read_file` the artefacts it needs.
|
|
4
|
+
* Results are pull-only: workflows never append completion messages to a
|
|
5
|
+
* conversation. List mode is active-first and paged. Targeted mode makes
|
|
6
|
+
* readiness explicit and can perform one bounded, abort-aware wait.
|
|
14
7
|
*/
|
|
15
8
|
import type { Pool } from "pg";
|
|
16
9
|
import type { CortexRunRow } from "../../state/schema.js";
|
|
@@ -20,7 +13,6 @@ interface FormattedStep {
|
|
|
20
13
|
agentId: string;
|
|
21
14
|
wave: number;
|
|
22
15
|
status: string;
|
|
23
|
-
/** Absent for steps that never ran (`pending`/`skipped`) — no output tree exists to point at. */
|
|
24
16
|
summaryPath?: string;
|
|
25
17
|
durationMs?: number | null;
|
|
26
18
|
error?: string | null;
|
|
@@ -29,15 +21,34 @@ interface FormattedStep {
|
|
|
29
21
|
finishReason?: string | null;
|
|
30
22
|
hitMaxSteps?: boolean | null;
|
|
31
23
|
}
|
|
24
|
+
interface WaitMetadata {
|
|
25
|
+
readonly requestedSeconds: number;
|
|
26
|
+
readonly cutoffReached: boolean;
|
|
27
|
+
}
|
|
32
28
|
type InspectRunOutput = {
|
|
33
|
-
message: string;
|
|
34
|
-
} | {
|
|
35
29
|
runs: FormattedRun[];
|
|
30
|
+
total: number;
|
|
31
|
+
page: number;
|
|
32
|
+
pageSize: number;
|
|
33
|
+
hasMore: boolean;
|
|
34
|
+
} | {
|
|
35
|
+
inspectionState: "not_found";
|
|
36
|
+
message: string;
|
|
37
|
+
wait?: WaitMetadata;
|
|
36
38
|
} | {
|
|
39
|
+
inspectionState: "in_progress" | "suspended" | "terminal";
|
|
40
|
+
message: string;
|
|
37
41
|
run: FormattedRun;
|
|
38
42
|
steps: FormattedStep[];
|
|
43
|
+
elapsedMs?: number;
|
|
44
|
+
wait?: WaitMetadata;
|
|
45
|
+
selfWaitPrevented?: boolean;
|
|
39
46
|
};
|
|
40
|
-
|
|
47
|
+
interface InspectRunClock {
|
|
48
|
+
now(): number;
|
|
49
|
+
wait(ms: number, signal: AbortSignal): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
declare function formatRun(r: CortexRunRow, verbose: boolean, exposePaths?: boolean): {
|
|
41
52
|
error?: string | null | undefined;
|
|
42
53
|
synthesisReason?: string | undefined;
|
|
43
54
|
runId: string;
|
|
@@ -49,8 +60,15 @@ declare function formatRun(r: CortexRunRow, verbose: boolean): {
|
|
|
49
60
|
synthesisPath: string | null;
|
|
50
61
|
synthesisStatus: "failed" | "produced" | "skipped_no_summaries" | "skipped_blocker" | null;
|
|
51
62
|
};
|
|
52
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Build the run-inspection tool. The optional clock is an internal testing
|
|
65
|
+
* seam for deterministic cutoff and transition coverage.
|
|
66
|
+
*/
|
|
67
|
+
export declare function createInspectRunTool(pool: Pool, clock?: InspectRunClock): import("../define-tool.js").Tool<{
|
|
53
68
|
runId?: string | undefined;
|
|
69
|
+
page?: number | undefined;
|
|
70
|
+
pageSize?: number | undefined;
|
|
71
|
+
waitForTerminalSeconds?: number | undefined;
|
|
54
72
|
includeDiagnostics?: boolean | undefined;
|
|
55
73
|
}, InspectRunOutput>;
|
|
56
74
|
export {};
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* inspectRun — inspect workflow runs for the current analysis.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Results are pull-only via this tool — the workflow does NOT write to the
|
|
9
|
-
* conversation thread on completion. The retired `run-result-writer` push
|
|
10
|
-
* payload (run status, step summaries, run synthesis, artifact paths) is
|
|
11
|
-
* surfaced here on demand: the run row + step `summaryPath` references +
|
|
12
|
-
* the `synthesisPath` for runs whose synthesis was produced let the
|
|
13
|
-
* conversation agent `read_file` the artefacts it needs.
|
|
4
|
+
* Results are pull-only: workflows never append completion messages to a
|
|
5
|
+
* conversation. List mode is active-first and paged. Targeted mode makes
|
|
6
|
+
* readiness explicit and can perform one bounded, abort-aware wait.
|
|
14
7
|
*/
|
|
8
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
15
9
|
import { ok } from "neverthrow";
|
|
16
10
|
import { z } from "zod";
|
|
17
|
-
import { queryRun, queryRunsByAnalysis, queryStepsByRun } from "../../state/index.js";
|
|
18
11
|
import { scopeResource } from "../../auth/types.js";
|
|
19
|
-
import { SYNTHESIS_STEP_ID } from "../../workspace/paths.js";
|
|
20
12
|
import { unwrapOrThrow } from "../../lib/result.js";
|
|
13
|
+
import { queryRun, queryRunsForInspection, queryStepsByRun } from "../../state/index.js";
|
|
14
|
+
import { SYNTHESIS_STEP_ID } from "../../workspace/paths.js";
|
|
21
15
|
import { defineTool } from "../define-tool.js";
|
|
22
|
-
|
|
16
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
17
|
+
const POLL_INTERVAL_MS = 1_000;
|
|
18
|
+
const systemClock = {
|
|
19
|
+
now: Date.now,
|
|
20
|
+
wait: (ms, signal) => sleep(ms, undefined, { signal }),
|
|
21
|
+
};
|
|
22
|
+
function isTerminal(status) {
|
|
23
|
+
return status === "completed" || status === "partial" || status === "failed" || status === "canceled";
|
|
24
|
+
}
|
|
25
|
+
function formatRun(r, verbose, exposePaths = isTerminal(r.status)) {
|
|
23
26
|
return {
|
|
24
27
|
runId: r.runId,
|
|
25
28
|
workflowName: r.workflowName,
|
|
@@ -27,74 +30,160 @@ function formatRun(r, verbose) {
|
|
|
27
30
|
startedAt: r.startedAt,
|
|
28
31
|
completedAt: r.completedAt,
|
|
29
32
|
planId: r.planId,
|
|
30
|
-
|
|
31
|
-
// result; a skipped/failed synthesis leaves no file, so pointing at a
|
|
32
|
-
// path would send the agent to a stale or absent artifact. The recorded
|
|
33
|
-
// outcome — surfaced as synthesisStatus (and synthesisReason when set) —
|
|
34
|
-
// lets a consumer tell "produced" from "skipped/failed/unknown" without
|
|
35
|
-
// reading a file.
|
|
36
|
-
synthesisPath: r.synthesisStatus === "produced" ? `runs/${r.runId}/synthesis.json` : null,
|
|
33
|
+
synthesisPath: exposePaths && r.synthesisStatus === "produced" ? `runs/${r.runId}/synthesis.json` : null,
|
|
37
34
|
synthesisStatus: r.synthesisStatus,
|
|
38
35
|
...(r.synthesisReason !== null ? { synthesisReason: r.synthesisReason } : {}),
|
|
39
36
|
...(verbose ? { error: r.error } : {}),
|
|
40
37
|
};
|
|
41
38
|
}
|
|
42
|
-
|
|
39
|
+
function formatSteps(runId, stepRows, verbose, exposePaths) {
|
|
40
|
+
return stepRows.map((s) => ({
|
|
41
|
+
stepId: s.stepId,
|
|
42
|
+
agentId: s.agentId,
|
|
43
|
+
wave: s.wave,
|
|
44
|
+
status: s.status,
|
|
45
|
+
...(exposePaths && s.status !== "pending" && s.status !== "skipped" && s.stepId !== SYNTHESIS_STEP_ID
|
|
46
|
+
? { summaryPath: `runs/${runId}/${s.stepId}/output/summary.md` }
|
|
47
|
+
: {}),
|
|
48
|
+
...(verbose
|
|
49
|
+
? {
|
|
50
|
+
durationMs: s.durationMs,
|
|
51
|
+
error: s.error,
|
|
52
|
+
attempts: s.attempts,
|
|
53
|
+
lastErrorClass: s.lastErrorClass,
|
|
54
|
+
finishReason: s.finishReason,
|
|
55
|
+
hitMaxSteps: s.hitMaxSteps,
|
|
56
|
+
}
|
|
57
|
+
: {}),
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
const inputSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
runId: z.string().optional().describe("Specific run to inspect. Omit to list runs."),
|
|
63
|
+
page: z.number().int().min(1).optional().describe("List page, starting at 1. Valid only without runId."),
|
|
64
|
+
pageSize: z.number().int().min(1).max(100).optional().describe("Rows per list page (1-100). Valid only without runId."),
|
|
65
|
+
waitForTerminalSeconds: z
|
|
66
|
+
.number()
|
|
67
|
+
.int()
|
|
68
|
+
.min(1)
|
|
69
|
+
.max(30)
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Wait up to this many seconds for a targeted running run to leave running state."),
|
|
72
|
+
includeDiagnostics: z
|
|
73
|
+
.boolean()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Include failure/retry telemetry: run.error and step.{durationMs,error,attempts,lastErrorClass,finishReason,hitMaxSteps}. Default false."),
|
|
76
|
+
})
|
|
77
|
+
.superRefine((input, refinement) => {
|
|
78
|
+
if (input.runId && (input.page !== undefined || input.pageSize !== undefined)) {
|
|
79
|
+
refinement.addIssue({
|
|
80
|
+
code: z.ZodIssueCode.custom,
|
|
81
|
+
message: "page and pageSize are valid only when runId is omitted",
|
|
82
|
+
path: input.page !== undefined ? ["page"] : ["pageSize"],
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (!input.runId && input.waitForTerminalSeconds !== undefined) {
|
|
86
|
+
refinement.addIssue({
|
|
87
|
+
code: z.ZodIssueCode.custom,
|
|
88
|
+
message: "waitForTerminalSeconds requires runId",
|
|
89
|
+
path: ["waitForTerminalSeconds"],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* Build the run-inspection tool. The optional clock is an internal testing
|
|
95
|
+
* seam for deterministic cutoff and transition coverage.
|
|
96
|
+
*/
|
|
97
|
+
export function createInspectRunTool(pool, clock = systemClock) {
|
|
43
98
|
return defineTool({
|
|
44
99
|
id: "inspect_run",
|
|
45
|
-
description: "Inspect workflow runs for this analysis. " +
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"Default response is lightweight — pass includeDiagnostics:true to add error/duration/retry telemetry.
|
|
49
|
-
|
|
50
|
-
inputSchema: z.object({
|
|
51
|
-
runId: z.string().optional().describe("Specific run to inspect. Omit to list all runs."),
|
|
52
|
-
includeDiagnostics: z
|
|
53
|
-
.boolean()
|
|
54
|
-
.optional()
|
|
55
|
-
.describe("Include failure/retry telemetry: run.error and step.{durationMs,error,attempts,lastErrorClass,finishReason,hitMaxSteps}. Default false."),
|
|
56
|
-
}),
|
|
100
|
+
description: "Inspect workflow runs for this analysis. Without a runId, returns an active-first paged list (running, suspended, then terminal history). " +
|
|
101
|
+
"With a runId, returns an explicit inspectionState and advertises output paths only after the run is terminal. " +
|
|
102
|
+
"For a user-directed check, waitForTerminalSeconds can wait once for 1-30 seconds; if the cutoff returns in_progress, stop polling for that turn. " +
|
|
103
|
+
"Default response is lightweight — pass includeDiagnostics:true to add error/duration/retry telemetry.",
|
|
104
|
+
inputSchema,
|
|
57
105
|
execute: async (input, ctx) => {
|
|
58
106
|
const resourceId = scopeResource(ctx.session.scope).resourceId;
|
|
59
107
|
const verbose = input.includeDiagnostics === true;
|
|
60
108
|
if (!input.runId) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
return ok({
|
|
109
|
+
const page = input.page ?? 1;
|
|
110
|
+
const pageSize = input.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
111
|
+
const offset = (page - 1) * pageSize;
|
|
112
|
+
const result = unwrapOrThrow(await queryRunsForInspection(pool, resourceId, { limit: pageSize, offset }));
|
|
113
|
+
return ok({
|
|
114
|
+
runs: result.runs.map((run) => formatRun(run, verbose)),
|
|
115
|
+
total: result.total,
|
|
116
|
+
page,
|
|
117
|
+
pageSize,
|
|
118
|
+
hasMore: offset + result.runs.length < result.total,
|
|
119
|
+
});
|
|
66
120
|
}
|
|
67
|
-
const
|
|
121
|
+
const waitSeconds = input.waitForTerminalSeconds;
|
|
122
|
+
let run = unwrapOrThrow(await queryRun(pool, input.runId));
|
|
68
123
|
if (!run || run.analysisId !== resourceId) {
|
|
69
|
-
return ok({
|
|
124
|
+
return ok({
|
|
125
|
+
inspectionState: "not_found",
|
|
126
|
+
message: `Run ${input.runId} was not found in this analysis.`,
|
|
127
|
+
...(waitSeconds === undefined ? {} : { wait: { requestedSeconds: waitSeconds, cutoffReached: false } }),
|
|
128
|
+
});
|
|
70
129
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// `synthesis.json`, surfaced as the run's `synthesisPath`, not a
|
|
81
|
-
// `{stepId}/output/summary.md`. Emitting the per-step path for it would
|
|
82
|
-
// point the agent at a file that never exists.
|
|
83
|
-
...(s.status === "pending" || s.status === "skipped" || s.stepId === SYNTHESIS_STEP_ID
|
|
84
|
-
? {}
|
|
85
|
-
: { summaryPath: `runs/${input.runId}/${s.stepId}/output/summary.md` }),
|
|
86
|
-
...(verbose
|
|
87
|
-
? {
|
|
88
|
-
durationMs: s.durationMs,
|
|
89
|
-
error: s.error,
|
|
90
|
-
attempts: s.attempts,
|
|
91
|
-
lastErrorClass: s.lastErrorClass,
|
|
92
|
-
finishReason: s.finishReason,
|
|
93
|
-
hitMaxSteps: s.hitMaxSteps,
|
|
130
|
+
const selfWait = waitSeconds !== undefined && ctx.session.runFrame?.runId === input.runId;
|
|
131
|
+
let cutoffReached = false;
|
|
132
|
+
if (waitSeconds !== undefined && run.status === "running" && !selfWait) {
|
|
133
|
+
const deadline = clock.now() + waitSeconds * 1_000;
|
|
134
|
+
while (run.status === "running") {
|
|
135
|
+
const remaining = deadline - clock.now();
|
|
136
|
+
if (remaining <= 0) {
|
|
137
|
+
cutoffReached = true;
|
|
138
|
+
break;
|
|
94
139
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
140
|
+
await clock.wait(Math.min(POLL_INTERVAL_MS, remaining), ctx.signal);
|
|
141
|
+
run = unwrapOrThrow(await queryRun(pool, input.runId));
|
|
142
|
+
if (!run || run.analysisId !== resourceId) {
|
|
143
|
+
return ok({
|
|
144
|
+
inspectionState: "not_found",
|
|
145
|
+
message: `Run ${input.runId} was not found in this analysis.`,
|
|
146
|
+
wait: { requestedSeconds: waitSeconds, cutoffReached: false },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const terminal = isTerminal(run.status);
|
|
152
|
+
const stepRows = unwrapOrThrow(await queryStepsByRun(pool, input.runId));
|
|
153
|
+
const steps = formatSteps(input.runId, stepRows, verbose, terminal);
|
|
154
|
+
const wait = waitSeconds === undefined ? {} : { wait: { requestedSeconds: waitSeconds, cutoffReached } };
|
|
155
|
+
if (run.status === "running") {
|
|
156
|
+
const message = selfWait
|
|
157
|
+
? "This run is still in progress. Results are not ready, and the enclosing workflow cannot finish while its current step waits for itself."
|
|
158
|
+
: cutoffReached
|
|
159
|
+
? "This run is still in progress after the requested wait cutoff. Results are not ready; stop polling for this turn."
|
|
160
|
+
: "This run is in progress. Results are not ready.";
|
|
161
|
+
return ok({
|
|
162
|
+
inspectionState: "in_progress",
|
|
163
|
+
message,
|
|
164
|
+
run: formatRun(run, verbose, false),
|
|
165
|
+
steps,
|
|
166
|
+
elapsedMs: Math.max(0, clock.now() - Date.parse(run.startedAt)),
|
|
167
|
+
...wait,
|
|
168
|
+
...(selfWait ? { selfWaitPrevented: true } : {}),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (run.status === "suspended_insufficient_funds") {
|
|
172
|
+
return ok({
|
|
173
|
+
inspectionState: "suspended",
|
|
174
|
+
message: "This run is suspended and will not progress until it is resumed.",
|
|
175
|
+
run: formatRun(run, verbose, false),
|
|
176
|
+
steps,
|
|
177
|
+
...wait,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return ok({
|
|
181
|
+
inspectionState: "terminal",
|
|
182
|
+
message: `This run is terminal with status ${run.status}.`,
|
|
183
|
+
run: formatRun(run, verbose, true),
|
|
184
|
+
steps,
|
|
185
|
+
...wait,
|
|
186
|
+
});
|
|
98
187
|
},
|
|
99
188
|
});
|
|
100
189
|
}
|
|
@@ -22,14 +22,11 @@ import { literatureReviewerPrompt } from "../../prompts/literature-reviewer.js";
|
|
|
22
22
|
import { composeSystemPrompt } from "../../agents/system-prompt.js";
|
|
23
23
|
import { forSubAgent } from "../../auth/types.js";
|
|
24
24
|
import { defineTool } from "../define-tool.js";
|
|
25
|
-
import { createNcbiTools } from "../bio/keys.js";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import { searchBgeeExpressionTool } from "../bio/search-bgee-expression.js";
|
|
29
|
-
import { createSearchDgidbTool } from "../bio/search-dgidb.js";
|
|
25
|
+
import { createChemDbTools, createNcbiTools } from "../bio/keys.js";
|
|
26
|
+
import { genePreclinicalProfileTool } from "../bio/gene-preclinical-profile.js";
|
|
27
|
+
import { lookupAnnotationTool } from "../bio/lookup-annotation.js";
|
|
30
28
|
import { searchGeneTool } from "../bio/search-gene.js";
|
|
31
29
|
import { searchInteractionsTool } from "../bio/search-interactions.js";
|
|
32
|
-
import { searchPathwayTool } from "../bio/search-pathway.js";
|
|
33
30
|
/** Sub-agent identity — appended to `callPath`, set as `agentId`. */
|
|
34
31
|
const AGENT_ID = "literature-reviewer";
|
|
35
32
|
/** Multi-tool research budget — search → read → synthesize over many genes. */
|
|
@@ -37,15 +34,14 @@ const REVIEWER_MAX_ITERATIONS = 30;
|
|
|
37
34
|
/** Build the `literature_reviewer` delegation tool bound to its provider. */
|
|
38
35
|
export function createLiteratureReviewerTool(deps) {
|
|
39
36
|
const ncbi = createNcbiTools(deps.bioKeys);
|
|
37
|
+
const chemDb = createChemDbTools(deps.bioKeys, { ...(deps.logger ? { logger: deps.logger } : {}) });
|
|
40
38
|
const reviewerTools = [
|
|
41
39
|
searchGeneTool,
|
|
42
|
-
|
|
43
|
-
lookupGoTermTool,
|
|
40
|
+
lookupAnnotationTool,
|
|
44
41
|
searchInteractionsTool,
|
|
45
42
|
ncbi.pubmed,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
getImpcKoProfileTool,
|
|
43
|
+
chemDb.drugGeneInteractions,
|
|
44
|
+
genePreclinicalProfileTool,
|
|
49
45
|
];
|
|
50
46
|
const agent = {
|
|
51
47
|
id: AGENT_ID,
|
|
@@ -209,6 +209,49 @@ export type RunProvenanceEvent = {
|
|
|
209
209
|
/** `atMs − run_started.atMs`: the workflow-observed run span in ms. */
|
|
210
210
|
durationMs: number;
|
|
211
211
|
};
|
|
212
|
+
/**
|
|
213
|
+
* A step's in-run display state. Wider than the ledger's `StepExecutionStatus`: `queued` is a
|
|
214
|
+
* scheduler-only condition (dependency-satisfied but held for budget capacity) that never reaches a
|
|
215
|
+
* database row, and `skipped` is asserted mid-run for a doomed dependent whose ledger row stays
|
|
216
|
+
* `pending` until the terminal sweep.
|
|
217
|
+
*/
|
|
218
|
+
export type StepRuntimeStatus = "pending" | "queued" | "running" | "completed" | "failed" | "skipped";
|
|
219
|
+
/**
|
|
220
|
+
* One step's state inside a {@link RunObservation}. `id` is the plan slug the ledger and the
|
|
221
|
+
* dependency edges use; `name` is the human phrase the plan author wrote, which is the only field
|
|
222
|
+
* that says what the step is FOR. `durationMs` and `error` appear only once the step has settled
|
|
223
|
+
* in a way that produced them.
|
|
224
|
+
*/
|
|
225
|
+
export interface RunStepObservation {
|
|
226
|
+
readonly id: string;
|
|
227
|
+
readonly name: string;
|
|
228
|
+
readonly agent: string;
|
|
229
|
+
readonly status: StepRuntimeStatus;
|
|
230
|
+
readonly durationMs?: number;
|
|
231
|
+
readonly error?: string;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* A whole-run snapshot handed to {@link ExecuteAnalysisDeps.observeRun}.
|
|
235
|
+
*
|
|
236
|
+
* Deliberately a SNAPSHOT rather than a transition event. The observer is invoked from the
|
|
237
|
+
* workflow body, so DBOS re-execution on recovery re-fires the whole sequence; a re-fired snapshot
|
|
238
|
+
* is idempotent for any consumer that renders from the newest one it has seen, whereas re-fired
|
|
239
|
+
* granular events would force every consumer to rebuild ordering and dedupe by identity. A
|
|
240
|
+
* consumer that misses an invocation loses nothing permanent — the next transition restates the
|
|
241
|
+
* full truth.
|
|
242
|
+
*
|
|
243
|
+
* Derived aggregates (completion counts, run duration) are deliberately ABSENT. They are
|
|
244
|
+
* computable from `steps` and recorded authoritatively on the run's ledger row; carrying them here
|
|
245
|
+
* would mint a second source that can disagree with the ledger a host also reads.
|
|
246
|
+
*/
|
|
247
|
+
export interface RunObservation {
|
|
248
|
+
readonly runId: string;
|
|
249
|
+
readonly analysisId: string;
|
|
250
|
+
/** `running` for every mid-flight snapshot; the derived terminal status on the last one. */
|
|
251
|
+
readonly status: ExecuteAnalysisFinalStatus;
|
|
252
|
+
/** Every plan step, including ones that have not started — never a delta. */
|
|
253
|
+
readonly steps: readonly RunStepObservation[];
|
|
254
|
+
}
|
|
212
255
|
/**
|
|
213
256
|
* Construction-time deps for the parent workflow. The order-of-operations is
|
|
214
257
|
* the body's own: it brackets the run, dispatches children, and synthesizes
|
|
@@ -247,6 +290,26 @@ export interface ExecuteAnalysisDeps {
|
|
|
247
290
|
* guarded so a throwing observer never fails the run.
|
|
248
291
|
*/
|
|
249
292
|
readonly emitProvenance?: (event: RunProvenanceEvent) => void;
|
|
293
|
+
/**
|
|
294
|
+
* Optional, fire-and-forget observation of the run's live shape, for a host driving a UI.
|
|
295
|
+
*
|
|
296
|
+
* Independent of {@link emitProvenance} in every direction: neither is implemented in terms of
|
|
297
|
+
* the other, they share no payload, and supplying one does not imply the other. They exist
|
|
298
|
+
* separately because provenance closes a signed, hash-chained record under a single-writer
|
|
299
|
+
* lock, while this is a lossy-tolerant display channel — coupling them would put a UI repaint
|
|
300
|
+
* behind the chain's write discipline and force provenance to carry step names and agent ids it
|
|
301
|
+
* has no reason to record.
|
|
302
|
+
*
|
|
303
|
+
* Invoked directly in the workflow body (NOT wrapped in `DBOS.runStep`) for the same reason
|
|
304
|
+
* `emitProvenance` is: body re-execution on recovery must re-fire it, and a cached step would
|
|
305
|
+
* suppress that. Idempotency comes free for rendering because the payload is a whole-run
|
|
306
|
+
* snapshot; a host taking a DURABLE side effect on one must key it by run id and status itself.
|
|
307
|
+
*
|
|
308
|
+
* Synchronous by signature (`void`, never a promise) so a host needing I/O has to dispatch it
|
|
309
|
+
* rather than await it inside the body's critical path. Every call site is guarded, so a
|
|
310
|
+
* throwing observer never fails the run.
|
|
311
|
+
*/
|
|
312
|
+
readonly observeRun?: (observation: RunObservation) => void;
|
|
250
313
|
}
|
|
251
314
|
/**
|
|
252
315
|
* Project the sandbox-step input handed to each child. The parent owns the
|