@inflexa-ai/harness 0.39.1 → 0.40.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/README.md +2 -3
- package/dist/agents/conversation-agent.d.ts +3 -0
- package/dist/agents/conversation-agent.js +13 -2
- package/dist/agents/report-session-agent.d.ts +3 -0
- package/dist/agents/report-session-agent.js +2 -0
- package/dist/agents/sandbox/cheminformatics-agent.js +0 -3
- package/dist/agents/sandbox/shared.d.ts +11 -2
- package/dist/agents/sandbox/shared.js +4 -0
- package/dist/app/chat-turn.d.ts +75 -12
- package/dist/app/chat-turn.js +149 -21
- package/dist/app/message-assembly.d.ts +16 -52
- package/dist/app/message-assembly.js +41 -68
- package/dist/app/run-activity.d.ts +4 -6
- package/dist/app/run-activity.js +7 -27
- package/dist/app/spawn-report-session.d.ts +6 -11
- package/dist/app/spawn-report-session.js +5 -6
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +1 -0
- package/dist/billing/usage-recorder.d.ts +3 -4
- package/dist/billing/usage-recorder.js +3 -4
- package/dist/contracts/chat-parts.d.ts +11 -1
- package/dist/contracts/chat-parts.js +1 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/part-registry.js +1 -0
- package/dist/contracts/report-blocks.d.ts +8 -8
- package/dist/contracts/schemas/chat-parts.d.ts +32 -9
- package/dist/contracts/schemas/chat-parts.js +10 -0
- package/dist/execution/artifact-metadata.d.ts +26 -33
- package/dist/execution/artifact-metadata.js +44 -106
- package/dist/execution/artifact-registration.d.ts +12 -2
- package/dist/execution/artifact-registration.js +9 -4
- package/dist/execution/post-step-pipeline.d.ts +26 -22
- package/dist/execution/post-step-pipeline.js +30 -24
- package/dist/execution/run-synthesis.d.ts +3 -0
- package/dist/execution/run-synthesis.js +11 -1
- package/dist/execution/step-summary.d.ts +21 -30
- package/dist/execution/step-summary.js +30 -57
- package/dist/index.d.ts +11 -10
- package/dist/index.js +16 -35
- package/dist/lib/zod-issues.js +2 -4
- package/dist/loop/compaction.d.ts +20 -0
- package/dist/loop/compaction.js +2 -0
- package/dist/loop/continue-agent.d.ts +40 -0
- package/dist/loop/continue-agent.js +38 -0
- package/dist/loop/metrics.d.ts +30 -22
- package/dist/loop/metrics.js +51 -22
- package/dist/loop/run-agent.d.ts +111 -13
- package/dist/loop/run-agent.js +467 -220
- package/dist/loop/run-to-terminal.d.ts +16 -17
- package/dist/loop/run-to-terminal.js +24 -35
- package/dist/loop/tool-mask.d.ts +23 -0
- package/dist/loop/tool-mask.js +46 -0
- package/dist/loop/tool-output.d.ts +48 -0
- package/dist/loop/tool-output.js +79 -0
- package/dist/loop/types.d.ts +6 -3
- package/dist/memory/ai-sdk-message-storage.d.ts +49 -2
- package/dist/memory/ai-sdk-message-storage.js +78 -2
- package/dist/memory/conversation-display-recorder.d.ts +6 -0
- package/dist/memory/conversation-display-recorder.js +64 -22
- package/dist/memory/conversation-display-replay.d.ts +5 -14
- package/dist/memory/conversation-display-replay.js +66 -32
- package/dist/memory/conversation-display-storage.d.ts +2 -1
- package/dist/memory/conversation-display-storage.js +2 -1
- package/dist/memory/conversation-view.d.ts +53 -0
- package/dist/memory/conversation-view.js +130 -0
- package/dist/memory/count-tokens.d.ts +5 -7
- package/dist/memory/count-tokens.js +7 -9
- package/dist/memory/thread-history.d.ts +74 -102
- package/dist/memory/thread-history.js +181 -246
- package/dist/memory/thread-store.d.ts +3 -3
- package/dist/memory/thread-store.js +7 -2
- package/dist/memory/tool-call-integrity.d.ts +15 -21
- package/dist/memory/tool-call-integrity.js +31 -38
- package/dist/memory/working-memory.d.ts +2 -5
- package/dist/memory/working-memory.js +2 -5
- package/dist/prompts/SOUL.d.ts +2 -2
- package/dist/prompts/SOUL.js +2 -7
- package/dist/prompts/analogical-reasoner.d.ts +1 -1
- package/dist/prompts/analogical-reasoner.js +16 -32
- package/dist/prompts/briefing.js +1 -1
- package/dist/prompts/compaction.d.ts +4 -0
- package/dist/prompts/compaction.js +24 -0
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +26 -35
- package/dist/prompts/execute-analysis/step-summary.js +7 -5
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +29 -34
- package/dist/prompts/planner.js +15 -29
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +2 -33
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +1 -2
- package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/cheminformatics-agent.js +10 -29
- package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
- package/dist/prompts/sandbox/chromatin-agent.js +1 -2
- package/dist/prompts/sandbox/data-profiler.js +6 -3
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +20 -24
- package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
- package/dist/prompts/sandbox/enrichment-agent.js +3 -2
- package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
- package/dist/prompts/sandbox/genomic-variant-agent.js +0 -2
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +7 -14
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +0 -2
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +14 -21
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -19
- package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.js +4 -5
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +7 -8
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +19 -19
- package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/proteomics-agent.js +3 -4
- package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
- package/dist/prompts/sandbox/scientific-executor.js +7 -10
- package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
- package/dist/prompts/sandbox/single-cell-agent.js +2 -4
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +3 -5
- package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.js +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +10 -19
- package/dist/prompts/sandbox-standards.d.ts +7 -7
- package/dist/prompts/sandbox-standards.js +74 -76
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +16 -42
- package/dist/providers/ai-sdk.d.ts +39 -6
- package/dist/providers/ai-sdk.js +117 -18
- package/dist/providers/errors.d.ts +2 -0
- package/dist/providers/errors.js +5 -0
- package/dist/providers/prompt-cache.d.ts +43 -41
- package/dist/providers/prompt-cache.js +47 -41
- package/dist/providers/reasoning.d.ts +10 -21
- package/dist/providers/reasoning.js +10 -21
- package/dist/providers/types.d.ts +21 -5
- package/dist/report-model/authoring-grammar.d.ts +2 -2
- package/dist/report-model/pin-snapshot.js +4 -10
- package/dist/runtime/assemble.d.ts +9 -7
- package/dist/runtime/assemble.js +7 -3
- package/dist/sandbox/create-sandbox.d.ts +2 -2
- package/dist/sandbox/submit-exec.d.ts +1 -1
- package/dist/schemas/artifact-manifest.d.ts +4 -4
- package/dist/state/artifacts.d.ts +11 -9
- package/dist/state/artifacts.js +112 -93
- package/dist/state/data-profile.d.ts +2 -2
- package/dist/state/data-profile.js +2 -2
- package/dist/state/init.js +39 -4
- package/dist/state/purge-analysis.js +4 -0
- package/dist/state/tool-outputs.d.ts +4 -0
- package/dist/state/tool-outputs.js +30 -0
- package/dist/tasks/data-profile.d.ts +31 -10
- package/dist/tasks/data-profile.js +116 -95
- package/dist/tools/ad-hoc-router.d.ts +8 -0
- package/dist/tools/ad-hoc-router.js +21 -2
- package/dist/tools/display/show-user.d.ts +1 -1
- package/dist/tools/execute-analysis.d.ts +3 -0
- package/dist/tools/execute-analysis.js +9 -1
- package/dist/tools/read-tool-output.d.ts +7 -0
- package/dist/tools/read-tool-output.js +130 -0
- package/dist/tools/research/generate-analogy-report.d.ts +10 -26
- package/dist/tools/research/generate-analogy-report.js +76 -163
- package/dist/tools/research/generate-plan.d.ts +3 -0
- package/dist/tools/research/generate-plan.js +4 -1
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +3 -0
- package/dist/tools/sandbox/report-blocker.d.ts +18 -5
- package/dist/tools/sandbox/report-blocker.js +54 -6
- package/dist/tools/sandbox/submit-file-metadata.d.ts +30 -0
- package/dist/tools/sandbox/submit-file-metadata.js +67 -0
- package/dist/tools/start-report-session.d.ts +1 -3
- package/dist/tools/start-report-session.js +0 -2
- package/dist/tools/workspace/execute-command.js +7 -4
- package/dist/tools/workspace/result-bounds.d.ts +4 -4
- package/dist/tools/workspace/result-bounds.js +6 -5
- package/dist/tools/workspace/workspace-search.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +3 -0
- package/dist/workflows/execute-analysis.js +6 -3
- package/dist/workflows/sandbox-step.d.ts +30 -9
- package/dist/workflows/sandbox-step.js +37 -8
- package/package.json +9 -9
- package/dist/providers/anthropic.d.ts +0 -25
- package/dist/providers/anthropic.js +0 -28
|
@@ -225,9 +225,9 @@ export function expireStaleDataProfile(pool, analysisId, timeoutMs) {
|
|
|
225
225
|
* Reset a ledger row wedged at `running` with no workflow behind it back to
|
|
226
226
|
* `failed`, so the normal retry path can re-profile it.
|
|
227
227
|
*
|
|
228
|
-
* A start that
|
|
228
|
+
* A start that fails after the CAS already flipped the row to `running`
|
|
229
229
|
* compensates itself (`triggerDataProfile`/`runDataProfile` fail the ledger in
|
|
230
|
-
* their
|
|
230
|
+
* their dispatch). This covers the residual case that compensation cannot: a host
|
|
231
231
|
* that dies in the window between the CAS and the `DBOS.startWorkflow` insert
|
|
232
232
|
* leaves a `running` row with no workflow for recovery to resume — nothing
|
|
233
233
|
* would ever move it off `running`, and every later trigger reports
|
package/dist/state/init.js
CHANGED
|
@@ -242,8 +242,8 @@ CREATE INDEX IF NOT EXISTS idx_cortex_regulatory_chunks_metadata
|
|
|
242
242
|
-- the JSON operators retractLastTurn uses work the same on either type.
|
|
243
243
|
--
|
|
244
244
|
-- reported_usage holds a whole turn's TokenUsageRollup — what providers reported
|
|
245
|
-
-- for the turn this row completed —
|
|
246
|
-
--
|
|
245
|
+
-- for the turn this row completed — on the LAST assistant row of an older turn
|
|
246
|
+
-- only. cortex_thread_turns holds the figures of each later turn.
|
|
247
247
|
-- It is NOT the tokens column beside it, and the two must never be read for
|
|
248
248
|
-- each other: tokens is an offline js-tiktoken estimate stamped on EVERY row so
|
|
249
249
|
-- the read path can window by budget without a provider round trip, and it exists
|
|
@@ -283,8 +283,8 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
283
283
|
role TEXT,
|
|
284
284
|
content_jsonb JSONB,
|
|
285
285
|
message_envelope JSON,
|
|
286
|
-
-- Versioned AI SDK UIMessage projection
|
|
287
|
-
--
|
|
286
|
+
-- Versioned AI SDK UIMessage projection of one written group, on the first
|
|
287
|
+
-- row of that group. Model-history reads never select it.
|
|
288
288
|
display_envelope JSONB,
|
|
289
289
|
tokens INTEGER NOT NULL,
|
|
290
290
|
reported_usage JSONB,
|
|
@@ -294,6 +294,24 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
294
294
|
PRIMARY KEY (thread_id, seq)
|
|
295
295
|
);
|
|
296
296
|
|
|
297
|
+
-- The state of one chat turn, and no message: a close never changes a row of
|
|
298
|
+
-- messages. messages.reported_usage and messages.turn_duration_ms hold the
|
|
299
|
+
-- figures of the older turns, which have no record here.
|
|
300
|
+
CREATE TABLE IF NOT EXISTS cortex_thread_turns (
|
|
301
|
+
thread_id TEXT NOT NULL,
|
|
302
|
+
-- The seq of the user row that opens the turn.
|
|
303
|
+
start_seq BIGINT NOT NULL,
|
|
304
|
+
status TEXT NOT NULL CHECK (status IN ('open', 'done', 'aborted', 'failed')),
|
|
305
|
+
-- A short text from the class of the error, never its message, because each later turn sends the note.
|
|
306
|
+
reason TEXT,
|
|
307
|
+
-- Absent when no provider reported a quantity, the same as messages.reported_usage.
|
|
308
|
+
reported_usage JSONB,
|
|
309
|
+
turn_duration_ms BIGINT,
|
|
310
|
+
opened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
311
|
+
closed_at TIMESTAMPTZ,
|
|
312
|
+
PRIMARY KEY (thread_id, start_seq)
|
|
313
|
+
);
|
|
314
|
+
|
|
297
315
|
-- Conversation thread metadata — the harness ThreadStore module's table,
|
|
298
316
|
-- one row per conversation thread. Holds the analysis-scope, title, and
|
|
299
317
|
-- soft-delete tombstone the harness messages table deliberately lacks.
|
|
@@ -413,6 +431,23 @@ CREATE TABLE IF NOT EXISTS cortex_ask_grants (
|
|
|
413
431
|
created_at TEXT NOT NULL,
|
|
414
432
|
PRIMARY KEY (analysis_id, user_id, grant_key)
|
|
415
433
|
);
|
|
434
|
+
|
|
435
|
+
-- The kept text of each cut tool result. thread_id is null for a text of a run, thus purgeThread removes
|
|
436
|
+
-- only the texts of chat turns, and purgeAnalysis removes each row by analysis_id.
|
|
437
|
+
CREATE TABLE IF NOT EXISTS cortex_tool_outputs (
|
|
438
|
+
analysis_id TEXT NOT NULL,
|
|
439
|
+
ref TEXT NOT NULL,
|
|
440
|
+
tool_name TEXT NOT NULL,
|
|
441
|
+
tool_call_id TEXT NOT NULL,
|
|
442
|
+
thread_id TEXT,
|
|
443
|
+
content TEXT NOT NULL,
|
|
444
|
+
total_length INTEGER NOT NULL,
|
|
445
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
446
|
+
PRIMARY KEY (analysis_id, ref)
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
CREATE INDEX IF NOT EXISTS cortex_tool_outputs_thread_idx
|
|
450
|
+
ON cortex_tool_outputs (thread_id) WHERE thread_id IS NOT NULL;
|
|
416
451
|
`;
|
|
417
452
|
/**
|
|
418
453
|
* Initialize Cortex state tables. Safe to call on every startup and across
|
|
@@ -78,6 +78,8 @@ const ANALYSIS_KEYED_DELETES = [
|
|
|
78
78
|
{ op: "purgeAnalysis.workingMemory", sql: "DELETE FROM cortex_working_memory WHERE analysis_id = $1" },
|
|
79
79
|
{ op: "purgeAnalysis.asks", sql: "DELETE FROM cortex_asks WHERE analysis_id = $1" },
|
|
80
80
|
{ op: "purgeAnalysis.askGrants", sql: "DELETE FROM cortex_ask_grants WHERE analysis_id = $1" },
|
|
81
|
+
// By its own analysis id, not through the thread rows: a kept text of a run names no thread.
|
|
82
|
+
{ op: "purgeAnalysis.toolOutputs", sql: "DELETE FROM cortex_tool_outputs WHERE analysis_id = $1" },
|
|
81
83
|
{ op: "purgeAnalysis.plans", sql: "DELETE FROM cortex_plans WHERE analysis_id = $1" },
|
|
82
84
|
{ op: "purgeAnalysis.reportVersions", sql: "DELETE FROM cortex_report_versions WHERE analysis_id = $1" },
|
|
83
85
|
{ op: "purgeAnalysis.reportSessionState", sql: "DELETE FROM cortex_report_session_state WHERE analysis_id = $1" },
|
|
@@ -133,6 +135,8 @@ export function createAnalysisPurge({ pool, workflows, logger: injected }) {
|
|
|
133
135
|
// thread is still the analysis's, and a purge takes it too.
|
|
134
136
|
deleteRows(client, "purgeAnalysis.messages", `DELETE FROM messages
|
|
135
137
|
WHERE thread_id IN (SELECT thread_id FROM cortex_analysis_threads WHERE analysis_id = $1)`, analysisId)
|
|
138
|
+
.andThen((messages) => deleteRows(client, "purgeAnalysis.turnRecords", `DELETE FROM cortex_thread_turns
|
|
139
|
+
WHERE thread_id IN (SELECT thread_id FROM cortex_analysis_threads WHERE analysis_id = $1)`, analysisId).map(() => messages))
|
|
136
140
|
.andThen((messages) => deleteRows(client, "purgeAnalysis.threads", "DELETE FROM cortex_analysis_threads WHERE analysis_id = $1", analysisId).map((threads) => ({
|
|
137
141
|
threads,
|
|
138
142
|
messages,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The Postgres realization of the tool output store of the loop, over `cortex_tool_outputs`. */
|
|
2
|
+
import type { Pool } from "pg";
|
|
3
|
+
import type { ToolOutputStore } from "../loop/tool-output.js";
|
|
4
|
+
export declare function createToolOutputStore(pool: Pool): ToolOutputStore;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** The Postgres realization of the tool output store of the loop, over `cortex_tool_outputs`. */
|
|
2
|
+
import { tryMutation, tryQuery } from "../lib/db-result.js";
|
|
3
|
+
function toKeptToolOutput(row) {
|
|
4
|
+
return {
|
|
5
|
+
analysisId: row.analysis_id,
|
|
6
|
+
ref: row.ref,
|
|
7
|
+
toolName: row.tool_name,
|
|
8
|
+
toolCallId: row.tool_call_id,
|
|
9
|
+
...(row.thread_id === null ? {} : { threadId: row.thread_id }),
|
|
10
|
+
content: row.content,
|
|
11
|
+
totalLength: row.total_length,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function createToolOutputStore(pool) {
|
|
15
|
+
return {
|
|
16
|
+
put: (output) => tryMutation("toolOutputs.put", () => pool.query(
|
|
17
|
+
// The update sets no `created_at`, thus the row keeps the time of its first put.
|
|
18
|
+
`INSERT INTO cortex_tool_outputs (analysis_id, ref, tool_name, tool_call_id, thread_id, content, total_length)
|
|
19
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
20
|
+
ON CONFLICT (analysis_id, ref) DO UPDATE SET
|
|
21
|
+
tool_name = EXCLUDED.tool_name,
|
|
22
|
+
tool_call_id = EXCLUDED.tool_call_id,
|
|
23
|
+
thread_id = EXCLUDED.thread_id,
|
|
24
|
+
content = EXCLUDED.content,
|
|
25
|
+
total_length = EXCLUDED.total_length`, [output.analysisId, output.ref, output.toolName, output.toolCallId, output.threadId ?? null, output.content, output.totalLength])).map(() => undefined),
|
|
26
|
+
get: (analysisId, ref) => tryQuery("toolOutputs.get", () => pool.query(`SELECT analysis_id, ref, tool_name, tool_call_id, thread_id, content, total_length
|
|
27
|
+
FROM cortex_tool_outputs
|
|
28
|
+
WHERE analysis_id = $1 AND ref = $2`, [analysisId, ref])).map(({ rows }) => (rows[0] === undefined ? null : toKeptToolOutput(rows[0]))),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* triggers dedup via the ledger CAS (only one `tryStart/tryRerun/tryRetry`
|
|
18
18
|
* UPDATE wins); each winning attempt starts a workflow under a per-attempt id.
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
20
|
+
import { ResultAsync } from "neverthrow";
|
|
21
21
|
import type { Pool } from "pg";
|
|
22
22
|
import { type AuthContext, type RunSession } from "../auth/types.js";
|
|
23
23
|
import type { EnvironmentStorePaths } from "../config/environment-stores.js";
|
|
@@ -26,6 +26,8 @@ import type { RunAuthorization, RunAuthorizer } from "../execution/run-authorize
|
|
|
26
26
|
import type { StagedInput } from "../execution/staged-input.js";
|
|
27
27
|
import type { SandboxAgentDeps } from "../agents/sandbox/shared.js";
|
|
28
28
|
import type { BioToolKeys } from "../tools/bio/keys.js";
|
|
29
|
+
import type { ToolOutputStore } from "../loop/tool-output.js";
|
|
30
|
+
import type { DbError } from "../lib/db-result.js";
|
|
29
31
|
import { type GateRefusal } from "../lib/hooks.js";
|
|
30
32
|
import type { Logger } from "../lib/logger.js";
|
|
31
33
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
@@ -62,6 +64,8 @@ export interface DataProfileDeps extends EnvironmentStorePaths {
|
|
|
62
64
|
readonly skillsDir: string;
|
|
63
65
|
/** LLM usage-accounting seam for the profiler agent loop; omitted falls back to the no-op recorder. */
|
|
64
66
|
readonly usageRecorder?: UsageRecorder;
|
|
67
|
+
/** The store of the profiler loop and of its read tool. */
|
|
68
|
+
readonly toolOutputStore?: ToolOutputStore;
|
|
65
69
|
/**
|
|
66
70
|
* The farm-extension seam of the embedder. Bound, it rides into the
|
|
67
71
|
* profiler agent deps, and the substrate attaches `link_packages`. Thus
|
|
@@ -149,9 +153,25 @@ export declare function runDataProfileBody(input: DataProfileWorkflowInput, deps
|
|
|
149
153
|
* - `"already_running"`: a workflow already owns the row, and nothing new ran.
|
|
150
154
|
* - `"completed"`: the analysis has no input files, so the row is `completed` at once
|
|
151
155
|
* with no result. No workflow ran (see {@link completeEmptyDataProfile}).
|
|
152
|
-
* - `"failed"`: the trigger refused the call
|
|
156
|
+
* - `"failed"`: the trigger refused the call, and the row is unchanged.
|
|
153
157
|
*/
|
|
154
158
|
export type DataProfileTriggerResult = "started" | "restarted" | "already_running" | "completed" | "failed";
|
|
159
|
+
/**
|
|
160
|
+
* Why the dispatch of a claimed profile started no workflow. The dispatch writes `failed` to the claimed
|
|
161
|
+
* row before a caller reads the `err`:
|
|
162
|
+
* - `refused`: `RunAuthorizer.authorize` gave an `err`. `reason` and `suspend` are the values of the host.
|
|
163
|
+
* - `start_failed`: DBOS did not start the workflow. `cause` is the throw of DBOS.
|
|
164
|
+
*/
|
|
165
|
+
export type DataProfileStartError = {
|
|
166
|
+
readonly type: "refused";
|
|
167
|
+
readonly reason: string;
|
|
168
|
+
readonly suspend: boolean;
|
|
169
|
+
} | {
|
|
170
|
+
readonly type: "start_failed";
|
|
171
|
+
readonly cause: unknown;
|
|
172
|
+
};
|
|
173
|
+
/** A one-line, user-facing description of a `DataProfileStartError`. */
|
|
174
|
+
export declare function describeDataProfileStartError(e: DataProfileStartError): string;
|
|
155
175
|
/**
|
|
156
176
|
* Route-side deps for triggering the data-profile workflow: the ledger pool,
|
|
157
177
|
* the run authorizer, and the registered workflow callable. The body's
|
|
@@ -213,16 +233,17 @@ export declare function dataProfileWorkflowId(analysisId: string, nonce: string)
|
|
|
213
233
|
* `"completed"`. An empty manifest against a seed that names files is a divergence
|
|
214
234
|
* between the caller and the ledger, and the trigger refuses it.
|
|
215
235
|
*
|
|
216
|
-
* Returns what happened, so the caller can surface it (e.g. in the seed response).
|
|
236
|
+
* Returns what happened, so the caller can surface it (e.g. in the seed response). A
|
|
237
|
+
* failed ledger read or claim is the `err`. The dispatch settles its own row, thus a
|
|
238
|
+
* refusal or a failed start after a claim shows on the ledger only.
|
|
217
239
|
*/
|
|
218
|
-
export declare function triggerDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams):
|
|
240
|
+
export declare function triggerDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams): ResultAsync<DataProfileTriggerResult, DbError>;
|
|
219
241
|
/**
|
|
220
242
|
* Start the data-profile workflow for an already-claimed analysis (the retry
|
|
221
243
|
* route claims via `tryRetryDataProfile`, then calls this). The caller does not
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* promise resolves: the row is where a caller reads the outcome.
|
|
244
|
+
* wait for the completion of the workflow. A refused authorization and a failed
|
|
245
|
+
* start each settle the row as `failed` before the `err` (see
|
|
246
|
+
* {@link dispatchClaimedProfile}), thus a caller that minted an authorization
|
|
247
|
+
* can revoke it.
|
|
227
248
|
*/
|
|
228
|
-
export declare function runDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams):
|
|
249
|
+
export declare function runDataProfile(deps: DataProfileTriggerDeps, params: DataProfileTriggerParams): ResultAsync<void, DataProfileStartError>;
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { DBOS, Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
|
|
21
21
|
import { randomUUID } from "node:crypto";
|
|
22
|
-
import { err, ok } from "neverthrow";
|
|
22
|
+
import { ResultAsync, err, ok } from "neverthrow";
|
|
23
23
|
import { forStep, forSubAgent } from "../auth/types.js";
|
|
24
24
|
import { createDataProfilerAgent } from "../agents/sandbox/data-profiler.js";
|
|
25
25
|
import { runToTerminal } from "../loop/run-to-terminal.js";
|
|
@@ -271,6 +271,7 @@ export function profilerSandboxAgentDeps(deps, step) {
|
|
|
271
271
|
...(deps.imagePackagesFile ? { imagePackagesFile: deps.imagePackagesFile } : {}),
|
|
272
272
|
...(deps.refStorePath ? { refStorePath: deps.refStorePath } : {}),
|
|
273
273
|
...(deps.extendAnalysisFarm ? { extendAnalysisFarm: deps.extendAnalysisFarm } : {}),
|
|
274
|
+
...(deps.toolOutputStore ? { toolOutputStore: deps.toolOutputStore } : {}),
|
|
274
275
|
bioKeys: deps.bioKeys,
|
|
275
276
|
step,
|
|
276
277
|
};
|
|
@@ -345,14 +346,14 @@ export async function runDataProfileBody(input, deps) {
|
|
|
345
346
|
return;
|
|
346
347
|
}
|
|
347
348
|
// 2. Register staged files in cortex_artifacts
|
|
348
|
-
await upsertArtifacts(deps.pool, stagedInputs.map((f) => ({
|
|
349
|
+
unwrapOrThrow(await upsertArtifacts(deps.pool, stagedInputs.map((f) => ({
|
|
349
350
|
resourceId: analysisId,
|
|
350
351
|
path: inputArtifactPath(f),
|
|
351
352
|
hash: f.hash,
|
|
352
353
|
size: f.size,
|
|
353
354
|
role: "input",
|
|
354
355
|
fileId: f.fileId,
|
|
355
|
-
})));
|
|
356
|
+
}))));
|
|
356
357
|
const executionId = generateExecutionId(DATA_PROFILE_AGENT_ID);
|
|
357
358
|
const workflowId = DBOS.workflowID ?? `${DATA_PROFILE_RUN_LITERAL}:${executionId}`;
|
|
358
359
|
const childSession = forSubAgent(runSession, DATA_PROFILE_AGENT_ID);
|
|
@@ -548,6 +549,7 @@ export async function runDataProfileBody(input, deps) {
|
|
|
548
549
|
runStep: durableStep,
|
|
549
550
|
resolved: () => accepted !== null,
|
|
550
551
|
usageRecorder: deps.usageRecorder,
|
|
552
|
+
toolOutputStore: deps.toolOutputStore,
|
|
551
553
|
isFatalLoopError: (err) => err instanceof DBOSErrors.DBOSWorkflowCancelledError,
|
|
552
554
|
}, {
|
|
553
555
|
tools: [submitProfileTool],
|
|
@@ -636,6 +638,15 @@ function settleTerminalWrite(logger, analysisId, write, status) {
|
|
|
636
638
|
}
|
|
637
639
|
recordRunCompleted({ workflow: "data_profile", status, durationMs: elapsedSinceIso(write.startedAt) });
|
|
638
640
|
}
|
|
641
|
+
/** A one-line, user-facing description of a `DataProfileStartError`. */
|
|
642
|
+
export function describeDataProfileStartError(e) {
|
|
643
|
+
switch (e.type) {
|
|
644
|
+
case "refused":
|
|
645
|
+
return `the profile run was not authorized: ${e.reason}`;
|
|
646
|
+
case "start_failed":
|
|
647
|
+
return `the profile workflow did not start: ${profileFailureReason(e.cause)}`;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
639
650
|
/**
|
|
640
651
|
* Resolve the `RunSession` the workflow runs under. Whether the authorization
|
|
641
652
|
* is freshly minted (Cortex-owned) or reused (caller-owned) is decided by the
|
|
@@ -667,18 +678,14 @@ export function dataProfileWorkflowId(analysisId, nonce) {
|
|
|
667
678
|
return `dataprofile:${analysisId}:${nonce}`;
|
|
668
679
|
}
|
|
669
680
|
/**
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
* not awaited.
|
|
681
|
+
* Start the data-profile workflow under the authorization and a per-attempt id
|
|
682
|
+
* `dataprofile:{analysisId}:{nonce}` — concurrent triggers are already
|
|
683
|
+
* serialized by the ledger CAS. The caller has already staged the inputs and
|
|
684
|
+
* supplied the manifest in `params.stagedInputs`; this forwards it into the
|
|
685
|
+
* workflow input. The handle result is not awaited.
|
|
676
686
|
*/
|
|
677
|
-
async function startDataProfileWorkflow(deps, params) {
|
|
678
|
-
const
|
|
679
|
-
if (authorized.isErr())
|
|
680
|
-
return err(authorized.error);
|
|
681
|
-
const { runSession, ownsMandate } = authorized.value; // oss-core-managed-ok
|
|
687
|
+
async function startDataProfileWorkflow(deps, params, authorization) {
|
|
688
|
+
const { runSession, ownsMandate } = authorization; // oss-core-managed-ok
|
|
682
689
|
const attemptNonce = randomUUID();
|
|
683
690
|
await DBOS.startWorkflow(deps.workflow, {
|
|
684
691
|
workflowID: dataProfileWorkflowId(params.analysisId, attemptNonce),
|
|
@@ -688,20 +695,29 @@ async function startDataProfileWorkflow(deps, params) {
|
|
|
688
695
|
ownsMandate, // oss-core-managed-ok
|
|
689
696
|
stagedInputs: params.stagedInputs,
|
|
690
697
|
});
|
|
691
|
-
return ok(undefined);
|
|
692
698
|
}
|
|
693
699
|
/**
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
700
|
+
* Authorize and start a claimed profile. The claim already flipped the row to
|
|
701
|
+
* `running`, thus a refused authorization and a failed start each settle the
|
|
702
|
+
* row as `failed` before the `err`, and the row never wedges at `running`. A
|
|
703
|
+
* rejected `authorize` promise is a defect of the host: the row is settled the
|
|
704
|
+
* same way, and the rejection passes through.
|
|
697
705
|
*/
|
|
698
706
|
async function dispatchClaimedProfile(deps, params, phase) {
|
|
699
|
-
const
|
|
700
|
-
await compensateStartFailure(deps, params.analysisId, phase,
|
|
701
|
-
|
|
707
|
+
const authorized = await Promise.resolve(authorizeDataProfile(deps, params)).catch(async (defect) => {
|
|
708
|
+
await compensateStartFailure(deps, params.analysisId, phase, defect);
|
|
709
|
+
throw defect;
|
|
702
710
|
});
|
|
703
|
-
if (
|
|
704
|
-
await refuseClaimedProfile(deps, params.analysisId, phase,
|
|
711
|
+
if (authorized.isErr()) {
|
|
712
|
+
await refuseClaimedProfile(deps, params.analysisId, phase, authorized.error);
|
|
713
|
+
return err({ type: "refused", reason: authorized.error.reason, suspend: authorized.error.kind === "suspended" });
|
|
714
|
+
}
|
|
715
|
+
const started = await ResultAsync.fromPromise(startDataProfileWorkflow(deps, params, authorized.value), (cause) => cause);
|
|
716
|
+
if (started.isErr()) {
|
|
717
|
+
await compensateStartFailure(deps, params.analysisId, phase, started.error);
|
|
718
|
+
return err({ type: "start_failed", cause: started.error });
|
|
719
|
+
}
|
|
720
|
+
return ok(undefined);
|
|
705
721
|
}
|
|
706
722
|
/**
|
|
707
723
|
* Attempt to claim and run data profiling for an analysis, then start the workflow
|
|
@@ -720,70 +736,85 @@ async function dispatchClaimedProfile(deps, params, phase) {
|
|
|
720
736
|
* `"completed"`. An empty manifest against a seed that names files is a divergence
|
|
721
737
|
* between the caller and the ledger, and the trigger refuses it.
|
|
722
738
|
*
|
|
723
|
-
* Returns what happened, so the caller can surface it (e.g. in the seed response).
|
|
739
|
+
* Returns what happened, so the caller can surface it (e.g. in the seed response). A
|
|
740
|
+
* failed ledger read or claim is the `err`. The dispatch settles its own row, thus a
|
|
741
|
+
* refusal or a failed start after a claim shows on the ledger only.
|
|
724
742
|
*/
|
|
725
|
-
export
|
|
743
|
+
export function triggerDataProfile(deps, params) {
|
|
744
|
+
return new ResultAsync(claimAndDispatchProfile(deps, params));
|
|
745
|
+
}
|
|
746
|
+
async function claimAndDispatchProfile(deps, params) {
|
|
726
747
|
const logger = (deps.logger ?? createNoopLogger()).named("data-profile").with({ analysisId: params.analysisId });
|
|
727
748
|
const { analysisId } = params;
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// `completed` at once, with no workflow. The CAS carries the same seed
|
|
748
|
-
// predicate as this pre-read, so a seed upsert that lands in between cannot be
|
|
749
|
-
// hidden behind a finished profile: the stamp refuses, and the status read
|
|
750
|
-
// below names the cause.
|
|
751
|
-
if (unwrapOrThrow(await completeEmptyDataProfile(deps.pool, analysisId)))
|
|
752
|
-
return "completed";
|
|
753
|
-
const current = unwrapOrThrow(await loadDataProfileStatus(deps.pool, analysisId));
|
|
754
|
-
if (current?.status === "running")
|
|
755
|
-
return "already_running";
|
|
756
|
-
logger.error("trigger rejected: empty-set completion refused (no analysis row, or the seed now names files)");
|
|
757
|
-
return "failed";
|
|
758
|
-
}
|
|
759
|
-
// A non-empty manifest against a seed that names no file: the caller skipped the
|
|
760
|
-
// seed, or the analysis row is missing. Name the cause before any claim.
|
|
761
|
-
if (seeded === null || seeded.length === 0) {
|
|
762
|
-
logger.error("trigger rejected: no seeded input set (missing analysis row or caller skipped seeding)");
|
|
763
|
-
return "failed";
|
|
764
|
-
}
|
|
765
|
-
const started = unwrapOrThrow(await tryStartDataProfile(deps.pool, analysisId));
|
|
766
|
-
if (started) {
|
|
767
|
-
void dispatchClaimedProfile(deps, params, "Run");
|
|
768
|
-
return "started";
|
|
749
|
+
// ADVISORY pre-check, not the enforcement — the claim CAS carries the same seed
|
|
750
|
+
// conjunct (see `SEEDED` in state/data-profile.ts) and is what actually makes a
|
|
751
|
+
// seedless `running` row impossible. This read exists only to name the cause: on
|
|
752
|
+
// the ordinary non-racing path it turns an opaque "no claim matched" into a
|
|
753
|
+
// precise reason. An empty array is not a seed — it names zero files. Reads via
|
|
754
|
+
// `loadSeedInputFileIds`, which returns null for BOTH a missing analysis row and
|
|
755
|
+
// a NULL-seed row (`loadDataProfileStatus` would hide the seed of a cleared row).
|
|
756
|
+
const seedRead = await loadSeedInputFileIds(deps.pool, analysisId);
|
|
757
|
+
if (seedRead.isErr())
|
|
758
|
+
return err(seedRead.error);
|
|
759
|
+
const seeded = seedRead.value;
|
|
760
|
+
if (params.stagedInputs.length === 0) {
|
|
761
|
+
// The trigger validates the manifest it is about to dispatch against the ledger
|
|
762
|
+
// seed. A seeded row profiled against an EMPTY manifest would claim `running` and
|
|
763
|
+
// then hit the body's empty-manifest path, and complete with a NULL result while
|
|
764
|
+
// the seed still names files. Refuse the divergence before any claim.
|
|
765
|
+
if (seeded !== null && seeded.length > 0) {
|
|
766
|
+
logger.error("trigger rejected: empty manifest dispatched against a non-empty seed", { seededCount: seeded.length });
|
|
767
|
+
return ok("failed");
|
|
769
768
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
if (
|
|
777
|
-
return
|
|
778
|
-
|
|
769
|
+
// The analysis has no input files, so there is nothing to profile. The row is
|
|
770
|
+
// `completed` at once, with no workflow. The CAS carries the same seed
|
|
771
|
+
// predicate as this pre-read, so a seed upsert that lands in between cannot be
|
|
772
|
+
// hidden behind a finished profile: the stamp refuses, and the status read
|
|
773
|
+
// below names the cause.
|
|
774
|
+
const completed = await completeEmptyDataProfile(deps.pool, analysisId);
|
|
775
|
+
if (completed.isErr())
|
|
776
|
+
return err(completed.error);
|
|
777
|
+
if (completed.value)
|
|
778
|
+
return ok("completed");
|
|
779
|
+
const current = await loadDataProfileStatus(deps.pool, analysisId);
|
|
780
|
+
if (current.isErr())
|
|
781
|
+
return err(current.error);
|
|
782
|
+
if (current.value?.status === "running")
|
|
783
|
+
return ok("already_running");
|
|
784
|
+
logger.error("trigger rejected: empty-set completion refused (no analysis row, or the seed now names files)");
|
|
785
|
+
return ok("failed");
|
|
779
786
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
787
|
+
// A non-empty manifest against a seed that names no file: the caller skipped the
|
|
788
|
+
// seed, or the analysis row is missing. Name the cause before any claim.
|
|
789
|
+
if (seeded === null || seeded.length === 0) {
|
|
790
|
+
logger.error("trigger rejected: no seeded input set (missing analysis row or caller skipped seeding)");
|
|
791
|
+
return ok("failed");
|
|
792
|
+
}
|
|
793
|
+
const started = await tryStartDataProfile(deps.pool, analysisId);
|
|
794
|
+
if (started.isErr())
|
|
795
|
+
return err(started.error);
|
|
796
|
+
if (started.value) {
|
|
797
|
+
dispatchInBackground(deps, params, "Run");
|
|
798
|
+
return ok("started");
|
|
799
|
+
}
|
|
800
|
+
const restarted = await tryRerunDataProfile(deps.pool, analysisId);
|
|
801
|
+
if (restarted.isErr())
|
|
802
|
+
return err(restarted.error);
|
|
803
|
+
if (restarted.value) {
|
|
804
|
+
dispatchInBackground(deps, params, "Re-run");
|
|
805
|
+
return ok("restarted");
|
|
783
806
|
}
|
|
807
|
+
const status = await loadDataProfileStatus(deps.pool, analysisId);
|
|
808
|
+
if (status.isErr())
|
|
809
|
+
return err(status.error);
|
|
810
|
+
return ok(status.value?.status === "running" ? "already_running" : "failed");
|
|
811
|
+
}
|
|
812
|
+
/** The dispatch settles the row on each arm and logs a defect, thus the trigger keeps no outcome of it. */
|
|
813
|
+
function dispatchInBackground(deps, params, phase) {
|
|
814
|
+
void dispatchClaimedProfile(deps, params, phase).catch(() => undefined);
|
|
784
815
|
}
|
|
785
816
|
/**
|
|
786
|
-
* Compensate a
|
|
817
|
+
* Compensate a start that failed after the claim. The ledger CAS in
|
|
787
818
|
* `tryStart`/`tryRerun`/`tryRetry` already flipped the row to `running`; if the
|
|
788
819
|
* dispatch never landed a workflow, DBOS has nothing to recover, so without
|
|
789
820
|
* this the row would sit at `running` forever and every later trigger would
|
|
@@ -827,23 +858,13 @@ async function failClaimedProfile(deps, analysisId, phase, reason) {
|
|
|
827
858
|
/**
|
|
828
859
|
* Start the data-profile workflow for an already-claimed analysis (the retry
|
|
829
860
|
* route claims via `tryRetryDataProfile`, then calls this). The caller does not
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
* promise resolves: the row is where a caller reads the outcome.
|
|
861
|
+
* wait for the completion of the workflow. A refused authorization and a failed
|
|
862
|
+
* start each settle the row as `failed` before the `err` (see
|
|
863
|
+
* {@link dispatchClaimedProfile}), thus a caller that minted an authorization
|
|
864
|
+
* can revoke it.
|
|
835
865
|
*/
|
|
836
|
-
export
|
|
837
|
-
|
|
838
|
-
try {
|
|
839
|
-
started = await startDataProfileWorkflow(deps, params);
|
|
840
|
-
}
|
|
841
|
-
catch (cause) {
|
|
842
|
-
await compensateStartFailure(deps, params.analysisId, "Retry", cause);
|
|
843
|
-
throw cause;
|
|
844
|
-
}
|
|
845
|
-
if (started.isErr())
|
|
846
|
-
await refuseClaimedProfile(deps, params.analysisId, "Retry", started.error);
|
|
866
|
+
export function runDataProfile(deps, params) {
|
|
867
|
+
return new ResultAsync(dispatchClaimedProfile(deps, params, "Retry"));
|
|
847
868
|
}
|
|
848
869
|
/**
|
|
849
870
|
* Concise, UI-safe failure reason. `data_profile_error` is surfaced verbatim
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Pool } from "pg";
|
|
2
2
|
import { type AgentSession } from "../auth/types.js";
|
|
3
|
+
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
3
4
|
import type { ResourcePolicy, ResourceSpec } from "../config/resource-limits.js";
|
|
4
5
|
import type { Logger } from "../lib/logger.js";
|
|
6
|
+
import { type AgentRunUsage } from "../loop/metrics.js";
|
|
5
7
|
import { type ChatProvider } from "../providers/types.js";
|
|
6
8
|
import type { CheckedPackage } from "./sandbox/list-available-packages.js";
|
|
7
9
|
export declare const AD_HOC_ROUTER_AGENT_ID = "adhoc-router";
|
|
@@ -48,6 +50,8 @@ export interface AdHocRouterDeps {
|
|
|
48
50
|
* the entries reach the link pass as the model wrote them.
|
|
49
51
|
*/
|
|
50
52
|
readonly resolvePackages?: (names: readonly string[]) => Promise<readonly CheckedPackage[] | null>;
|
|
53
|
+
/** Falls back to the no-op recorder when omitted. */
|
|
54
|
+
readonly usageRecorder?: UsageRecorder;
|
|
51
55
|
}
|
|
52
56
|
export declare function defaultAdHocResources(policy?: ResourcePolicy): ResourceSpec;
|
|
53
57
|
export declare function validAdHocResources(value: unknown, policy?: ResourcePolicy): ResourceSpec | null;
|
|
@@ -56,4 +60,8 @@ export declare function routeAdHocRequest(deps: AdHocRouterDeps, input: {
|
|
|
56
60
|
request: string;
|
|
57
61
|
session: AgentSession;
|
|
58
62
|
signal: AbortSignal;
|
|
63
|
+
/** The router call folds its usage into this, so the turn total includes it. */
|
|
64
|
+
readonly turnUsage?: AgentRunUsage;
|
|
65
|
+
/** Carried into the record key of the router usage entry. */
|
|
66
|
+
readonly invocationId?: string;
|
|
59
67
|
}): Promise<AdHocRoute>;
|
|
@@ -3,8 +3,11 @@ import { z } from "zod";
|
|
|
3
3
|
import { PLANNABLE_AGENT_CATALOG } from "../agents/sandbox-catalog.js";
|
|
4
4
|
import { forSubAgent } from "../auth/types.js";
|
|
5
5
|
import { DATA_PROFILE_ORIENTATION_MAX_CHARS, buildDataProfileOrientation } from "../app/data-profile-orientation.js";
|
|
6
|
+
import { createNoopUsageRecorder } from "../billing/noop-usage-recorder.js";
|
|
6
7
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
7
8
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
9
|
+
import { countChatTokens } from "../loop/metrics.js";
|
|
10
|
+
import { accountForChatCall } from "../loop/run-agent.js";
|
|
8
11
|
import { packagesSection, resourceEstimationSection } from "../prompts/planner.js";
|
|
9
12
|
import { effectiveDeadlineMs } from "../providers/types.js";
|
|
10
13
|
import { formatQuery, parseQuery } from "../sandbox/package-identity.js";
|
|
@@ -12,6 +15,8 @@ import { loadDataProfileStatus } from "../state/index.js";
|
|
|
12
15
|
export const AD_HOC_ROUTER_AGENT_ID = "adhoc-router";
|
|
13
16
|
export const AD_HOC_ROUTER_TIMEOUT_MS = 10_000;
|
|
14
17
|
export const AD_HOC_FALLBACK_AGENT_ID = "scientific-executor";
|
|
18
|
+
/** Must differ from a loop step name such as `llm-0`, since both share the usage-record key space. */
|
|
19
|
+
const AD_HOC_ROUTE_CALL_NAME = "adhoc-route";
|
|
15
20
|
const resourcesSchema = z.object({
|
|
16
21
|
cpu: z.number().positive(),
|
|
17
22
|
memoryGb: z.number().positive(),
|
|
@@ -152,6 +157,7 @@ function routeTool() {
|
|
|
152
157
|
}
|
|
153
158
|
export async function routeAdHocRequest(deps, input) {
|
|
154
159
|
const logger = (deps.logger ?? createNoopLogger()).named("adhoc-router").with({ analysisId: input.analysisId, model: deps.model });
|
|
160
|
+
const routerSession = forSubAgent(input.session, AD_HOC_ROUTER_AGENT_ID);
|
|
155
161
|
const catalog = PLANNABLE_AGENT_CATALOG.map((agent) => `- ${agent.id}: capabilities [${agent.capabilities.join(", ")}]; suitable for [${agent.suitableFor.join(", ")}]`).join("\n");
|
|
156
162
|
const orientation = await profileOrientation(deps.pool, input.analysisId, logger);
|
|
157
163
|
const controller = new AbortController();
|
|
@@ -160,7 +166,8 @@ export async function routeAdHocRequest(deps, input) {
|
|
|
160
166
|
let raw;
|
|
161
167
|
let failure;
|
|
162
168
|
try {
|
|
163
|
-
const response = unwrapOrThrow(await deps.provider
|
|
169
|
+
const response = unwrapOrThrow(await deps.provider
|
|
170
|
+
.chat({
|
|
164
171
|
system: [
|
|
165
172
|
"Select exactly one specialist for a targeted one-step analysis, estimate its sandbox resources, and name the packages that the step imports.",
|
|
166
173
|
"Choose only from the supplied catalog. Do not create a plan, reject the request, or select scientific-executor.",
|
|
@@ -179,7 +186,19 @@ export async function routeAdHocRequest(deps, input) {
|
|
|
179
186
|
// No forced tool choice: some Claude models reject it with a
|
|
180
187
|
// 400. A reply without the call falls back below as `malformed`.
|
|
181
188
|
tools: routeTool(),
|
|
182
|
-
},
|
|
189
|
+
}, routerSession, signal)
|
|
190
|
+
.map(countChatTokens(AD_HOC_ROUTER_AGENT_ID)));
|
|
191
|
+
// A failed call throws at unwrapOrThrow above, so no usage is recorded.
|
|
192
|
+
accountForChatCall(response, {
|
|
193
|
+
session: routerSession,
|
|
194
|
+
agentId: AD_HOC_ROUTER_AGENT_ID,
|
|
195
|
+
callPath: routerSession.provenance.callPath,
|
|
196
|
+
stepName: AD_HOC_ROUTE_CALL_NAME,
|
|
197
|
+
...(input.invocationId === undefined ? {} : { invocationId: input.invocationId }),
|
|
198
|
+
usageRecorder: deps.usageRecorder ?? createNoopUsageRecorder(),
|
|
199
|
+
logger,
|
|
200
|
+
rollups: input.turnUsage === undefined ? [] : [input.turnUsage],
|
|
201
|
+
});
|
|
183
202
|
const call = Array.isArray(response.message.content)
|
|
184
203
|
? response.message.content.find((part) => part.type === "tool-call" && part.toolName === "submit_route")
|
|
185
204
|
: undefined;
|
|
@@ -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" | "markdown" | "
|
|
29
|
+
kind: "code" | "markdown" | "echart" | "svg" | "table" | "structure";
|
|
30
30
|
title?: string | undefined;
|
|
31
31
|
spec?: Record<string, unknown> | undefined;
|
|
32
32
|
dataPath?: string | undefined;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
|
|
4
4
|
*/
|
|
5
5
|
import type { Pool } from "pg";
|
|
6
|
+
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
6
7
|
import type { EnvironmentStorePaths } from "../config/environment-stores.js";
|
|
7
8
|
import type { ResourcePolicy } from "../config/resource-limits.js";
|
|
8
9
|
import type { RunAuthorizer } from "../execution/run-authorizer.js";
|
|
@@ -29,6 +30,8 @@ export interface ExecuteAnalysisToolDeps extends Pick<EnvironmentStorePaths, "fa
|
|
|
29
30
|
* run reserves anything. Without it, the link pass returns at once.
|
|
30
31
|
*/
|
|
31
32
|
readonly extendAnalysisFarm?: ExtendAnalysisFarm;
|
|
33
|
+
/** Falls back to the no-op recorder when omitted. */
|
|
34
|
+
readonly usageRecorder?: UsageRecorder;
|
|
32
35
|
}
|
|
33
36
|
export declare class PlanNotFoundError extends Error {
|
|
34
37
|
constructor(planId: string);
|