@inflexa-ai/harness 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/conversation-agent.d.ts +9 -15
- package/dist/agents/conversation-agent.js +9 -15
- package/dist/agents/sandbox/index.js +0 -3
- package/dist/agents/sandbox/shared.d.ts +2 -2
- package/dist/agents/sandbox/shared.js +1 -1
- package/dist/config/resource-limits.d.ts +0 -7
- package/dist/config/resource-limits.js +0 -1
- package/dist/execution/dbos-run-launcher.js +1 -26
- package/dist/execution/run-authorizer.d.ts +1 -1
- package/dist/execution/run-authorizer.js +1 -1
- package/dist/execution/run-launcher.d.ts +2 -18
- package/dist/execution/run-launcher.js +2 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -6
- package/dist/loop/run-agent.js +1 -0
- package/dist/memory/reconstruct-cards.d.ts +1 -1
- package/dist/memory/reconstruct-cards.js +7 -5
- package/dist/memory/thread-history.js +39 -1
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +27 -10
- package/dist/prompts/planner.d.ts +3 -0
- package/dist/prompts/planner.js +1 -1
- package/dist/prompts/sandbox-standards.d.ts +4 -4
- package/dist/prompts/sandbox-standards.js +5 -5
- package/dist/runtime/assemble.d.ts +5 -10
- package/dist/runtime/assemble.js +0 -4
- package/dist/runtime/boot.d.ts +2 -2
- package/dist/runtime/boot.js +1 -1
- package/dist/runtime/dbos.d.ts +4 -5
- package/dist/runtime/dbos.js +6 -7
- package/dist/sandbox/mount-plan.d.ts +1 -1
- package/dist/sandbox/sandbox-error.d.ts +1 -1
- package/dist/sandbox/sandbox-error.js +1 -1
- package/dist/sandbox/types.d.ts +1 -1
- package/dist/schemas/validate-plan.d.ts +1 -1
- package/dist/state/index.d.ts +4 -4
- package/dist/state/index.js +2 -2
- package/dist/state/plans.js +1 -1
- package/dist/state/runs.d.ts +10 -0
- package/dist/state/runs.js +47 -0
- package/dist/tools/__fixtures__/tool-context.js +1 -0
- package/dist/tools/ad-hoc-router.d.ts +31 -0
- package/dist/tools/ad-hoc-router.js +132 -0
- package/dist/tools/analysis-invocation.d.ts +4 -0
- package/dist/tools/analysis-invocation.js +17 -0
- package/dist/tools/define-tool.d.ts +6 -2
- package/dist/tools/define-tool.js +1 -1
- package/dist/tools/execute-analysis.d.ts +43 -0
- package/dist/tools/execute-analysis.js +252 -0
- package/dist/tools/iterate-report.js +7 -6
- package/dist/tools/research/generate-plan.js +1 -1
- package/dist/workflows/execute-analysis.d.ts +5 -5
- package/dist/workflows/execute-analysis.js +3 -5
- package/package.json +1 -1
- package/dist/agents/sandbox/ephemeral-executor.d.ts +0 -9
- package/dist/agents/sandbox/ephemeral-executor.js +0 -20
- package/dist/execution/ephemeral-runner.d.ts +0 -98
- package/dist/execution/ephemeral-runner.js +0 -189
- package/dist/prompts/sandbox/ephemeral-executor.d.ts +0 -1
- package/dist/prompts/sandbox/ephemeral-executor.js +0 -66
- package/dist/tools/execute-plan.d.ts +0 -64
- package/dist/tools/execute-plan.js +0 -211
- package/dist/tools/run-ephemeral.d.ts +0 -33
- package/dist/tools/run-ephemeral.js +0 -65
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { jsonSchema, tool as aiTool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { PLANNABLE_AGENT_CATALOG } from "../agents/sandbox-catalog.js";
|
|
4
|
+
import { forSubAgent } from "../auth/types.js";
|
|
5
|
+
import { DATA_PROFILE_ORIENTATION_MAX_CHARS, buildDataProfileOrientation } from "../app/data-profile-orientation.js";
|
|
6
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
7
|
+
import { unwrapOrThrow } from "../lib/result.js";
|
|
8
|
+
import { resourceEstimationSection } from "../prompts/planner.js";
|
|
9
|
+
import { loadDataProfileStatus } from "../state/index.js";
|
|
10
|
+
export const AD_HOC_ROUTER_AGENT_ID = "adhoc-router";
|
|
11
|
+
export const AD_HOC_ROUTER_TIMEOUT_MS = 10_000;
|
|
12
|
+
export const AD_HOC_FALLBACK_AGENT_ID = "scientific-executor";
|
|
13
|
+
const resourcesSchema = z.object({
|
|
14
|
+
cpu: z.number().positive(),
|
|
15
|
+
memoryGb: z.number().positive(),
|
|
16
|
+
gpu: z.object({ count: z.number().int().positive() }).optional(),
|
|
17
|
+
});
|
|
18
|
+
const routeSchema = z.object({
|
|
19
|
+
agentId: z.string().optional(),
|
|
20
|
+
resources: resourcesSchema.optional(),
|
|
21
|
+
rationale: z.string().optional(),
|
|
22
|
+
});
|
|
23
|
+
export function defaultAdHocResources(policy) {
|
|
24
|
+
return policy ? { cpu: Math.min(4, policy.perStep.maxCpu), memoryGb: Math.min(8, policy.perStep.maxMemoryGb) } : { cpu: 4, memoryGb: 8 };
|
|
25
|
+
}
|
|
26
|
+
export function validAdHocResources(value, policy) {
|
|
27
|
+
const parsed = resourcesSchema.safeParse(value);
|
|
28
|
+
if (!parsed.success)
|
|
29
|
+
return null;
|
|
30
|
+
const r = parsed.data;
|
|
31
|
+
const maxCpu = policy?.perStep.maxCpu ?? 4;
|
|
32
|
+
const maxMemoryGb = policy?.perStep.maxMemoryGb ?? 8;
|
|
33
|
+
const maxGpuCount = policy?.perStep.maxGpuCount ?? 0;
|
|
34
|
+
if (r.cpu > maxCpu || r.memoryGb > maxMemoryGb || (r.gpu?.count ?? 0) > maxGpuCount)
|
|
35
|
+
return null;
|
|
36
|
+
return r;
|
|
37
|
+
}
|
|
38
|
+
function resourceBounds(policy) {
|
|
39
|
+
const defaults = defaultAdHocResources(policy);
|
|
40
|
+
if (!policy) {
|
|
41
|
+
return `CPU range 1..${defaults.cpu}; default ${defaults.cpu}. Memory range 1..${defaults.memoryGb} GB; default ${defaults.memoryGb}. GPU range 0..0.`;
|
|
42
|
+
}
|
|
43
|
+
const { perStep } = policy;
|
|
44
|
+
return [
|
|
45
|
+
`CPU range ${Math.min(1, perStep.maxCpu)}..${perStep.maxCpu}; default ${defaults.cpu}.`,
|
|
46
|
+
`Memory range ${Math.min(1, perStep.maxMemoryGb)}..${perStep.maxMemoryGb} GB; default ${defaults.memoryGb}.`,
|
|
47
|
+
`GPU range 0..${perStep.maxGpuCount}; omit gpu unless required.`,
|
|
48
|
+
].join(" ");
|
|
49
|
+
}
|
|
50
|
+
async function profileOrientation(pool, analysisId, logger) {
|
|
51
|
+
try {
|
|
52
|
+
const status = unwrapOrThrow(await loadDataProfileStatus(pool, analysisId));
|
|
53
|
+
if (!status?.result)
|
|
54
|
+
return "No persisted data-profile facts are available.";
|
|
55
|
+
return buildDataProfileOrientation(status.result, DATA_PROFILE_ORIENTATION_MAX_CHARS);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
logger.warn("could not load data profile for ad hoc routing", { error: error instanceof Error ? error.message : String(error) });
|
|
59
|
+
return "The persisted data profile could not be loaded; route from the request alone.";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function routeTool() {
|
|
63
|
+
return {
|
|
64
|
+
submit_route: aiTool({
|
|
65
|
+
description: "Submit the single specialist and resource recommendation. This is the only valid response.",
|
|
66
|
+
inputSchema: jsonSchema(z.toJSONSchema(routeSchema)),
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function routeAdHocRequest(deps, input) {
|
|
71
|
+
const logger = (deps.logger ?? createNoopLogger()).named("adhoc-router").with({ analysisId: input.analysisId, model: deps.model });
|
|
72
|
+
const catalog = PLANNABLE_AGENT_CATALOG.map((agent) => `- ${agent.id}: capabilities [${agent.capabilities.join(", ")}]; suitable for [${agent.suitableFor.join(", ")}]`).join("\n");
|
|
73
|
+
const orientation = await profileOrientation(deps.pool, input.analysisId, logger);
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timer = setTimeout(() => controller.abort(new Error("ad hoc routing timed out")), deps.timeoutMs ?? AD_HOC_ROUTER_TIMEOUT_MS);
|
|
76
|
+
const signal = AbortSignal.any([input.signal, controller.signal]);
|
|
77
|
+
let raw;
|
|
78
|
+
let failure;
|
|
79
|
+
try {
|
|
80
|
+
const response = unwrapOrThrow(await deps.provider.chat({
|
|
81
|
+
system: [
|
|
82
|
+
"Select exactly one specialist for a targeted one-step analysis and estimate its sandbox resources.",
|
|
83
|
+
"Choose only from the supplied catalog. Do not create a plan, reject the request, or select scientific-executor.",
|
|
84
|
+
resourceEstimationSection(deps.resourcePolicy),
|
|
85
|
+
resourceBounds(deps.resourcePolicy),
|
|
86
|
+
"Respond only by calling submit_route.",
|
|
87
|
+
].join("\n\n"),
|
|
88
|
+
messages: [
|
|
89
|
+
{
|
|
90
|
+
role: "user",
|
|
91
|
+
content: `Request:\n${input.request}\n\nPersisted data-profile orientation:\n${orientation}\n\nEligible specialists:\n${catalog}`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
tools: routeTool(),
|
|
95
|
+
toolChoice: { type: "tool", toolName: "submit_route" },
|
|
96
|
+
}, forSubAgent(input.session, AD_HOC_ROUTER_AGENT_ID), signal));
|
|
97
|
+
const call = Array.isArray(response.message.content)
|
|
98
|
+
? response.message.content.find((part) => part.type === "tool-call" && part.toolName === "submit_route")
|
|
99
|
+
: undefined;
|
|
100
|
+
raw = call?.input;
|
|
101
|
+
if (!call)
|
|
102
|
+
failure = "malformed";
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (input.signal.aborted)
|
|
106
|
+
throw error;
|
|
107
|
+
failure = controller.signal.aborted && !input.signal.aborted ? "timeout" : "provider_error";
|
|
108
|
+
logger.warn("ad hoc route fell back", { failure, error: error instanceof Error ? error.message : String(error) });
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
}
|
|
113
|
+
const candidate = raw !== null && typeof raw === "object" ? raw : {};
|
|
114
|
+
const requestedAgent = typeof candidate.agentId === "string" ? candidate.agentId : undefined;
|
|
115
|
+
const validAgent = requestedAgent !== undefined && PLANNABLE_AGENT_CATALOG.some((agent) => agent.id === requestedAgent);
|
|
116
|
+
const agentId = validAgent ? requestedAgent : AD_HOC_FALLBACK_AGENT_ID;
|
|
117
|
+
if (!failure && !requestedAgent)
|
|
118
|
+
failure = "no_match";
|
|
119
|
+
if (!failure && !validAgent)
|
|
120
|
+
failure = "invalid_agent";
|
|
121
|
+
const validResources = validAdHocResources(candidate.resources, deps.resourcePolicy);
|
|
122
|
+
const resources = validResources ?? defaultAdHocResources(deps.resourcePolicy);
|
|
123
|
+
if (!validResources && candidate.resources !== undefined && !failure)
|
|
124
|
+
failure = "invalid_resources";
|
|
125
|
+
const rationale = typeof candidate.rationale === "string" && candidate.rationale.trim()
|
|
126
|
+
? candidate.rationale.trim()
|
|
127
|
+
: failure
|
|
128
|
+
? `Deterministic fallback: ${failure}`
|
|
129
|
+
: `Selected ${agentId}`;
|
|
130
|
+
logger.info("ad hoc route selected", { agentId, resources, rationale, failure });
|
|
131
|
+
return { agentId, resources, rationale, ...(failure ? { fallbackClass: failure } : {}) };
|
|
132
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Deterministic internal plan id in the existing persistence shape. */
|
|
2
|
+
export declare function adHocPlanId(analysisId: string, invocationId: string): string;
|
|
3
|
+
/** Deterministic RFC-4122 UUID used as both cortex run id and DBOS workflow id. */
|
|
4
|
+
export declare function adHocRunId(analysisId: string, invocationId: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const TOOL_ID = "execute_analysis";
|
|
3
|
+
function digest(analysisId, invocationId) {
|
|
4
|
+
return createHash("sha256").update(`${TOOL_ID}\0${analysisId}\0${invocationId}`, "utf8").digest();
|
|
5
|
+
}
|
|
6
|
+
/** Deterministic internal plan id in the existing persistence shape. */
|
|
7
|
+
export function adHocPlanId(analysisId, invocationId) {
|
|
8
|
+
return `pln-${digest(analysisId, invocationId).toString("hex", 0, 4)}`;
|
|
9
|
+
}
|
|
10
|
+
/** Deterministic RFC-4122 UUID used as both cortex run id and DBOS workflow id. */
|
|
11
|
+
export function adHocRunId(analysisId, invocationId) {
|
|
12
|
+
const bytes = Buffer.from(digest(analysisId, invocationId).subarray(4, 20));
|
|
13
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
|
14
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
15
|
+
const hex = bytes.toString("hex");
|
|
16
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
17
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* captures its deps and calls `defineTool`.
|
|
9
9
|
*
|
|
10
10
|
* `ToolContext` carries only request-scoped values every tool may need
|
|
11
|
-
* (`session`, `signal`, `emit`, `runStep`, `ask`) — no pool, no sandbox, no
|
|
11
|
+
* (`invocationId`, `session`, `signal`, `emit`, `runStep`, `ask`) — no pool, no sandbox, no
|
|
12
12
|
* logger. The error contract: an expected
|
|
13
13
|
* outcome ("not found", "no results") stays in the ok channel as a data
|
|
14
14
|
* variant (`ok({ found: false })`); an unexpected failure is an `err(ToolError)`
|
|
@@ -39,11 +39,15 @@ export type ToolExecutionMode = "step" | "workflow" | "inline";
|
|
|
39
39
|
export declare function isToolError(value: unknown): value is ToolError;
|
|
40
40
|
/**
|
|
41
41
|
* The request-scoped values passed to every tool's `execute`. No injected
|
|
42
|
-
* dependencies (see the harness-durable-runtime spec) —
|
|
42
|
+
* dependencies (see the harness-durable-runtime spec) — invocation identity,
|
|
43
|
+
* `session`, `signal`, `emit`, the `runStep`
|
|
43
44
|
* durability seam (`passthroughStep` in chat, `DBOS.runStep` in workflows) a
|
|
44
45
|
* tool uses to wrap its own durable work, and the `ask` user-approval seam.
|
|
45
46
|
*/
|
|
46
47
|
export interface ToolContext {
|
|
48
|
+
/** Stable identity of this AI SDK tool call. Redelivery preserves it; a new
|
|
49
|
+
* model-issued call receives a new value. */
|
|
50
|
+
readonly invocationId: string;
|
|
47
51
|
readonly session: AgentSession;
|
|
48
52
|
readonly signal: AbortSignal;
|
|
49
53
|
readonly emit: EmitFn;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* captures its deps and calls `defineTool`.
|
|
9
9
|
*
|
|
10
10
|
* `ToolContext` carries only request-scoped values every tool may need
|
|
11
|
-
* (`session`, `signal`, `emit`, `runStep`, `ask`) — no pool, no sandbox, no
|
|
11
|
+
* (`invocationId`, `session`, `signal`, `emit`, `runStep`, `ask`) — no pool, no sandbox, no
|
|
12
12
|
* logger. The error contract: an expected
|
|
13
13
|
* outcome ("not found", "no results") stays in the ok channel as a data
|
|
14
14
|
* variant (`ok({ found: false })`); an unexpected failure is an `err(ToolError)`
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified async edge for approved plans and explicitly requested one-step ad
|
|
3
|
+
* hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
|
|
4
|
+
*/
|
|
5
|
+
import type { Pool } from "pg";
|
|
6
|
+
import type { ResourcePolicy } from "../config/resource-limits.js";
|
|
7
|
+
import type { RunAuthorizer } from "../execution/run-authorizer.js";
|
|
8
|
+
import type { RunLauncher } from "../execution/run-launcher.js";
|
|
9
|
+
import type { Logger } from "../lib/logger.js";
|
|
10
|
+
import type { ChatProvider } from "../providers/types.js";
|
|
11
|
+
import { type AnalysisPlan } from "../schemas/workflow-state.js";
|
|
12
|
+
import type { ExecuteAnalysisInput, ExecuteAnalysisResult } from "../workflows/execute-analysis.js";
|
|
13
|
+
import { type AdHocRoute } from "./ad-hoc-router.js";
|
|
14
|
+
type ExecuteAnalysisWorkflow = (input: ExecuteAnalysisInput) => Promise<ExecuteAnalysisResult>;
|
|
15
|
+
export interface ExecuteAnalysisToolDeps {
|
|
16
|
+
readonly pool: Pool;
|
|
17
|
+
readonly executeAnalysisWorkflow: ExecuteAnalysisWorkflow;
|
|
18
|
+
readonly runAuthorizer: RunAuthorizer;
|
|
19
|
+
readonly runLauncher: RunLauncher;
|
|
20
|
+
readonly resourcePolicy?: ResourcePolicy;
|
|
21
|
+
readonly utilityProvider: ChatProvider;
|
|
22
|
+
readonly utilityModel: string;
|
|
23
|
+
readonly logger?: Logger;
|
|
24
|
+
}
|
|
25
|
+
export declare class PlanNotFoundError extends Error {
|
|
26
|
+
constructor(planId: string);
|
|
27
|
+
}
|
|
28
|
+
export declare class PlanMalformedError extends Error {
|
|
29
|
+
constructor(planId: string);
|
|
30
|
+
}
|
|
31
|
+
export declare class PlanValidationError extends Error {
|
|
32
|
+
readonly errors: string[];
|
|
33
|
+
constructor(planId: string, errors: string[]);
|
|
34
|
+
}
|
|
35
|
+
export declare function buildAdHocPlan(request: string, route: AdHocRoute, createdAt?: string): AnalysisPlan;
|
|
36
|
+
export declare function createExecuteAnalysisTool(deps: ExecuteAnalysisToolDeps): import("./define-tool.js").Tool<{
|
|
37
|
+
mode: "plan" | "adhoc";
|
|
38
|
+
planId?: string | undefined;
|
|
39
|
+
request?: string | undefined;
|
|
40
|
+
}, {
|
|
41
|
+
runId: string;
|
|
42
|
+
}>;
|
|
43
|
+
export {};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified async edge for approved plans and explicitly requested one-step ad
|
|
3
|
+
* hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { ok } from "neverthrow";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
|
|
9
|
+
import { DEFAULT_SANDBOX_MAX_STEPS } from "../config/resource-limits.js";
|
|
10
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
11
|
+
import { unwrapOrThrow } from "../lib/result.js";
|
|
12
|
+
import { buildRunCardData } from "../memory/card-builders.js";
|
|
13
|
+
import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
|
|
14
|
+
import { validatePlan } from "../schemas/validate-plan.js";
|
|
15
|
+
import { RunDedupCollisionError, insertRun, loadPlan, queryActiveRun, reserveRunById, updateRunStatus, upsertPlan } from "../state/index.js";
|
|
16
|
+
import { routeAdHocRequest } from "./ad-hoc-router.js";
|
|
17
|
+
import { adHocPlanId, adHocRunId } from "./analysis-invocation.js";
|
|
18
|
+
import { defineTool } from "./define-tool.js";
|
|
19
|
+
const planIdSchema = z.string().regex(/^pln-[a-f0-9]{8}$/, "planId must be a pln-<8hex> value");
|
|
20
|
+
const inputSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
mode: z.enum(["plan", "adhoc"]),
|
|
23
|
+
planId: planIdSchema.optional(),
|
|
24
|
+
request: z.string().min(1).optional(),
|
|
25
|
+
})
|
|
26
|
+
.superRefine((value, ctx) => {
|
|
27
|
+
if (value.mode === "plan" && (value.planId === undefined || value.request !== undefined)) {
|
|
28
|
+
ctx.addIssue({ code: "custom", message: "plan mode requires planId and forbids request" });
|
|
29
|
+
}
|
|
30
|
+
if (value.mode === "adhoc" && (value.request === undefined || value.planId !== undefined)) {
|
|
31
|
+
ctx.addIssue({ code: "custom", message: "adhoc mode requires request and forbids planId" });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
export class PlanNotFoundError extends Error {
|
|
35
|
+
constructor(planId) {
|
|
36
|
+
super(`Plan ${planId} not found for this analysis`);
|
|
37
|
+
this.name = "PlanNotFoundError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class PlanMalformedError extends Error {
|
|
41
|
+
constructor(planId) {
|
|
42
|
+
super(`Plan ${planId} is malformed`);
|
|
43
|
+
this.name = "PlanMalformedError";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export class PlanValidationError extends Error {
|
|
47
|
+
errors;
|
|
48
|
+
constructor(planId, errors) {
|
|
49
|
+
super(`Plan ${planId} failed validation: ${errors.length} error(s)`);
|
|
50
|
+
this.errors = errors;
|
|
51
|
+
this.name = "PlanValidationError";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isDedupCollision(err) {
|
|
55
|
+
return err instanceof RunDedupCollisionError || (err instanceof Error && err.name === "RunDedupCollisionError");
|
|
56
|
+
}
|
|
57
|
+
function planSummary(plan) {
|
|
58
|
+
return plan.title?.trim() || plan.analytical_narrative.trim().slice(0, 280);
|
|
59
|
+
}
|
|
60
|
+
function workflowInput(planId, plan, args) {
|
|
61
|
+
return {
|
|
62
|
+
analysisId: args.analysisId,
|
|
63
|
+
planId,
|
|
64
|
+
planSummary: planSummary(plan),
|
|
65
|
+
threadId: args.threadId,
|
|
66
|
+
steps: plan.steps.map((step) => ({ id: step.id, depends_on: step.depends_on ?? [] })),
|
|
67
|
+
planStepById: Object.fromEntries(plan.steps.map((step) => [step.id, step])),
|
|
68
|
+
agentByStepId: Object.fromEntries(plan.steps.map((step) => [step.id, step.agent ?? "unknown"])),
|
|
69
|
+
resourcesByStepId: Object.fromEntries(plan.steps.map((step) => {
|
|
70
|
+
if (!step.resources)
|
|
71
|
+
throw new Error(`Step "${step.id}" has no resources — validation should have rejected it`);
|
|
72
|
+
return [step.id, step.resources];
|
|
73
|
+
})),
|
|
74
|
+
timeoutByStepId: Object.fromEntries(plan.steps.filter((step) => step.timeout !== undefined).map((step) => [step.id, step.timeout])),
|
|
75
|
+
budget: args.budget,
|
|
76
|
+
runSession: args.runSession,
|
|
77
|
+
ownsMandate: args.ownsMandate,
|
|
78
|
+
synthesisEnabled: args.synthesisEnabled,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function validateStoredPlan(raw, planId) {
|
|
82
|
+
const parsed = AnalysisPlanSchema.safeParse(raw);
|
|
83
|
+
if (!parsed.success)
|
|
84
|
+
throw new PlanMalformedError(planId);
|
|
85
|
+
const result = validatePlan(parsed.data);
|
|
86
|
+
if (!result.valid)
|
|
87
|
+
throw new PlanValidationError(planId, result.errors);
|
|
88
|
+
return parsed.data;
|
|
89
|
+
}
|
|
90
|
+
function adHocTitle(request) {
|
|
91
|
+
const compact = request.trim().replace(/\s+/g, " ");
|
|
92
|
+
return compact.length <= 80 ? compact : `${compact.slice(0, 77)}…`;
|
|
93
|
+
}
|
|
94
|
+
export function buildAdHocPlan(request, route, createdAt = new Date().toISOString()) {
|
|
95
|
+
const meta = SANDBOX_AGENT_META[route.agentId];
|
|
96
|
+
const plan = {
|
|
97
|
+
title: adHocTitle(request),
|
|
98
|
+
analytical_narrative: `Execute one targeted analysis step for the explicit request: ${request}`,
|
|
99
|
+
created_at: createdAt,
|
|
100
|
+
steps: [
|
|
101
|
+
{
|
|
102
|
+
id: "T1S1",
|
|
103
|
+
name: "Ad hoc analysis",
|
|
104
|
+
track: "ad-hoc",
|
|
105
|
+
step_type: "analysis",
|
|
106
|
+
question: request,
|
|
107
|
+
context: `Automatically routed to ${route.agentId}. Routing rationale: ${route.rationale}`,
|
|
108
|
+
constraints: [
|
|
109
|
+
"Use the staged analysis inputs and persisted data profile; do not invent missing data.",
|
|
110
|
+
"Complete only this targeted request; do not expand it into a multi-step plan.",
|
|
111
|
+
],
|
|
112
|
+
acceptance_criteria: [
|
|
113
|
+
"Persist every script needed to reproduce the computation.",
|
|
114
|
+
"Persist machine-readable result file(s), even when the answer is a single scalar.",
|
|
115
|
+
"Provide a direct result summary grounded in the persisted outputs.",
|
|
116
|
+
],
|
|
117
|
+
caveats: route.fallbackClass ? [`Automatic routing fallback: ${route.fallbackClass}`] : undefined,
|
|
118
|
+
depends_on: [],
|
|
119
|
+
status: "pending",
|
|
120
|
+
resources: route.resources,
|
|
121
|
+
agent: route.agentId,
|
|
122
|
+
maxSteps: meta?.defaultMaxSteps ?? DEFAULT_SANDBOX_MAX_STEPS,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
return AnalysisPlanSchema.parse(plan);
|
|
127
|
+
}
|
|
128
|
+
async function persistedAdHocPlan(deps, args) {
|
|
129
|
+
const existing = unwrapOrThrow(await loadPlan(deps.pool, args.planId, { analysisId: args.analysisId }));
|
|
130
|
+
if (existing)
|
|
131
|
+
return validateStoredPlan(existing, args.planId);
|
|
132
|
+
const route = await routeAdHocRequest({
|
|
133
|
+
provider: deps.utilityProvider,
|
|
134
|
+
model: deps.utilityModel,
|
|
135
|
+
pool: deps.pool,
|
|
136
|
+
resourcePolicy: deps.resourcePolicy,
|
|
137
|
+
logger: deps.logger,
|
|
138
|
+
}, { analysisId: args.analysisId, request: args.request, session: args.ctx.session, signal: args.ctx.signal });
|
|
139
|
+
const candidate = buildAdHocPlan(args.request, route);
|
|
140
|
+
unwrapOrThrow(await upsertPlan(deps.pool, { planId: args.planId, analysisId: args.analysisId, plan: candidate }));
|
|
141
|
+
const stored = unwrapOrThrow(await loadPlan(deps.pool, args.planId, { analysisId: args.analysisId }));
|
|
142
|
+
if (!stored)
|
|
143
|
+
throw new Error(`Internal ad hoc plan ${args.planId} could not be reloaded`);
|
|
144
|
+
return validateStoredPlan(stored, args.planId);
|
|
145
|
+
}
|
|
146
|
+
export function createExecuteAnalysisTool(deps) {
|
|
147
|
+
const logger = (deps.logger ?? createNoopLogger()).named("execute-analysis-tool");
|
|
148
|
+
return defineTool({
|
|
149
|
+
id: "execute_analysis",
|
|
150
|
+
description: "Launch analysis computation asynchronously. Use mode=plan with the planId only after the user approved that stored plan. " +
|
|
151
|
+
"Use mode=adhoc with the user's exact targeted computational request when they explicitly asked to run/compute/test/compare it; " +
|
|
152
|
+
"that explicit request is consent and needs no synthetic-plan approval. If computation is merely your suggestion, ask first. " +
|
|
153
|
+
"You never choose the sandbox specialist: ad hoc routing does that automatically. Returns runId; inspect results on a later turn.",
|
|
154
|
+
inputSchema,
|
|
155
|
+
execute: async (input, ctx) => {
|
|
156
|
+
if (ctx.session.scope.kind !== "analysis")
|
|
157
|
+
throw new Error("execute_analysis requires an analysis-scoped session");
|
|
158
|
+
if (!ctx.session.auth)
|
|
159
|
+
throw new Error("execute_analysis: session is missing its auth capability");
|
|
160
|
+
const analysisId = ctx.session.scope.analysisId;
|
|
161
|
+
const threadId = ctx.session.scope.threadId ?? null;
|
|
162
|
+
const planId = input.mode === "plan" ? input.planId : adHocPlanId(analysisId, ctx.invocationId);
|
|
163
|
+
const emitRunCard = async (runId) => {
|
|
164
|
+
const card = await buildRunCardData(deps.pool, { planId, analysisId, runId }).unwrapOr(null);
|
|
165
|
+
if (card)
|
|
166
|
+
await ctx.emit({ type: "data-run-card", source: ctx.session.provenance, data: card });
|
|
167
|
+
};
|
|
168
|
+
let plan;
|
|
169
|
+
if (input.mode === "plan") {
|
|
170
|
+
const raw = unwrapOrThrow(await loadPlan(deps.pool, planId, { analysisId }));
|
|
171
|
+
if (!raw)
|
|
172
|
+
throw new PlanNotFoundError(planId);
|
|
173
|
+
plan = validateStoredPlan(raw, planId);
|
|
174
|
+
const active = unwrapOrThrow(await queryActiveRun(deps.pool, analysisId, planId));
|
|
175
|
+
if (active) {
|
|
176
|
+
await emitRunCard(active.runId);
|
|
177
|
+
return ok({ runId: active.runId });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
plan = await persistedAdHocPlan(deps, { analysisId, request: input.request, ctx, planId });
|
|
182
|
+
}
|
|
183
|
+
const runId = input.mode === "plan" ? randomUUID() : adHocRunId(analysisId, ctx.invocationId);
|
|
184
|
+
if (input.mode === "plan") {
|
|
185
|
+
try {
|
|
186
|
+
unwrapOrThrow(await insertRun(deps.pool, {
|
|
187
|
+
runId,
|
|
188
|
+
analysisId,
|
|
189
|
+
threadId,
|
|
190
|
+
workflowName: "executeAnalysis",
|
|
191
|
+
planId,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (isDedupCollision(error)) {
|
|
196
|
+
const active = unwrapOrThrow(await queryActiveRun(deps.pool, analysisId, planId));
|
|
197
|
+
if (active) {
|
|
198
|
+
await emitRunCard(active.runId);
|
|
199
|
+
return ok({ runId: active.runId });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const reservation = unwrapOrThrow(await reserveRunById(deps.pool, {
|
|
207
|
+
runId,
|
|
208
|
+
analysisId,
|
|
209
|
+
threadId,
|
|
210
|
+
workflowName: "executeAnalysis",
|
|
211
|
+
planId,
|
|
212
|
+
}));
|
|
213
|
+
if (!reservation.inserted) {
|
|
214
|
+
await emitRunCard(reservation.row.runId);
|
|
215
|
+
return ok({ runId: reservation.row.runId });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
let authorization;
|
|
219
|
+
try {
|
|
220
|
+
authorization = await deps.runAuthorizer.authorize({
|
|
221
|
+
auth: ctx.session.auth,
|
|
222
|
+
scope: ctx.session.scope,
|
|
223
|
+
provenance: ctx.session.provenance,
|
|
224
|
+
frame: { runId },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
await updateRunStatus(deps.pool, runId, "failed", "run authorization failed").match(() => { }, () => { });
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
const wfInput = workflowInput(planId, plan, {
|
|
232
|
+
analysisId,
|
|
233
|
+
threadId,
|
|
234
|
+
runSession: authorization.runSession,
|
|
235
|
+
ownsMandate: authorization.ownsMandate,
|
|
236
|
+
budget: deps.resourcePolicy?.budget,
|
|
237
|
+
synthesisEnabled: input.mode === "plan",
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
await deps.runLauncher.launch(deps.executeAnalysisWorkflow, { workflowId: runId }, wfInput);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
await deps.runAuthorizer.revoke(authorization, "workflow-start-failed").catch(() => { });
|
|
244
|
+
await updateRunStatus(deps.pool, runId, "failed", "workflow start failed").match(() => { }, () => { });
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
logger.info("analysis launched", { analysisId, runId, planId, mode: input.mode });
|
|
248
|
+
await emitRunCard(runId);
|
|
249
|
+
return ok({ runId });
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
@@ -130,7 +130,7 @@ const intentField = () => z
|
|
|
130
130
|
.describe("Why this section exists and what to emphasize (e.g. 'hero — headline " +
|
|
131
131
|
"finding', 'downplay if space is tight'). Emphasis, not layout — the " +
|
|
132
132
|
"builder picks the components, sizing and alternation.");
|
|
133
|
-
/** Free-text row transform — the reason to
|
|
133
|
+
/** Free-text row transform — the reason to avoid launching analysis just to pre-slice a CSV. */
|
|
134
134
|
const transformField = () => z
|
|
135
135
|
.string()
|
|
136
136
|
.optional()
|
|
@@ -138,7 +138,7 @@ const transformField = () => z
|
|
|
138
138
|
"asset — filter, derive, aggregate, sort (e.g. 'filter padj < 0.05 and " +
|
|
139
139
|
"abs(log2FoldChange) > 1', 'compute -log10(padj) as neg_log_padj', 'group " +
|
|
140
140
|
"by sample and sum count', 'sort by mean_count desc, take top 50'). Use " +
|
|
141
|
-
"this instead of
|
|
141
|
+
"this instead of launching an ad hoc analysis to pre-slice the file. The text " +
|
|
142
142
|
"is rendered verbatim as a provenance footnote, so the data reads as " +
|
|
143
143
|
"processed, not fabricated.");
|
|
144
144
|
const SectionSchema = z.discriminatedUnion("type", [
|
|
@@ -359,14 +359,15 @@ treatment. Pass the composed brief as \`submit_report\`'s \`report\` field.
|
|
|
359
359
|
their content reaches the report as prose you write into \`narrative\` /
|
|
360
360
|
\`methods\` sections.
|
|
361
361
|
|
|
362
|
-
## Do NOT
|
|
362
|
+
## Do NOT launch ad hoc analysis for a section transform
|
|
363
363
|
- Not to peek at a CSV — pre-flight already parsed its columns and head rows.
|
|
364
364
|
- Not to filter, slice, rank, or derive columns from a single CSV about to be
|
|
365
365
|
rendered — \`chart.content.transform\` and \`table.content.transform\` do exactly
|
|
366
366
|
that client-side, and the transform text renders as a provenance footnote.
|
|
367
|
-
-
|
|
368
|
-
|
|
369
|
-
|
|
367
|
+
- A durable ad hoc analysis is appropriate only when the user explicitly asks
|
|
368
|
+
for computation no single section transform covers: cross-file aggregation,
|
|
369
|
+
statistics needing a real numerical library, or a derived CSV the report then
|
|
370
|
+
lists as a fresh source.
|
|
370
371
|
|
|
371
372
|
## Choosing a section type
|
|
372
373
|
- narrative — prose you wrote: context, story, interpretation.
|
|
@@ -420,7 +420,7 @@ export function createGeneratePlanTool(deps) {
|
|
|
420
420
|
"to the planner directly. Do NOT summarize or re-type them into this call; you cannot restate that " +
|
|
421
421
|
"record more faithfully than the record itself. Pass only what the profile cannot hold: the research " +
|
|
422
422
|
"question, facts the user told you (analystNotes), prior run results, and their constraints. " +
|
|
423
|
-
"Returns a structured plan ready for show_plan and
|
|
423
|
+
"Returns a structured plan ready for show_plan and execute_analysis plan mode, or a clarification question if " +
|
|
424
424
|
"the planner is missing something it cannot infer.",
|
|
425
425
|
inputSchema: z.object({
|
|
426
426
|
researchQuestion: z.string().describe("What the user wants to analyze — their goal and specific questions."),
|
|
@@ -115,6 +115,11 @@ export interface ExecuteAnalysisInput {
|
|
|
115
115
|
* dependency-satisfied step starts immediately — the legacy fan-out.
|
|
116
116
|
*/
|
|
117
117
|
readonly budget?: MachineBudget;
|
|
118
|
+
/**
|
|
119
|
+
* Replay-stable run-level synthesis decision. Absent preserves the legacy
|
|
120
|
+
* behavior for workflows persisted before this field existed.
|
|
121
|
+
*/
|
|
122
|
+
readonly synthesisEnabled?: boolean;
|
|
118
123
|
/**
|
|
119
124
|
* Durable `RunSession` minted at the async edge by `executePlan`.
|
|
120
125
|
* Carries the run-authorization credential, identity, scope, and `runFrame.runId`.
|
|
@@ -230,11 +235,6 @@ export interface ExecuteAnalysisDeps {
|
|
|
230
235
|
readonly bioKeys: BioToolKeys;
|
|
231
236
|
/** Run-level billing-bracket seam (managed: external bracket; OSS: no-op). */
|
|
232
237
|
readonly runCharge: RunCharge;
|
|
233
|
-
/**
|
|
234
|
-
* When false, skip run-level synthesis entirely (findings stay empty).
|
|
235
|
-
* Config, not a seam. Defaults to true.
|
|
236
|
-
*/
|
|
237
|
-
readonly synthesisEnabled?: boolean;
|
|
238
238
|
/** Run-authorization seam — the terminal path revokes through `revoke`. */
|
|
239
239
|
readonly runAuthorizer: RunAuthorizer;
|
|
240
240
|
/**
|
|
@@ -342,7 +342,7 @@ export async function runExecuteAnalysisBody(input, deps) {
|
|
|
342
342
|
// `pending` and the terminal sweep finalizes it to `skipped` — honest
|
|
343
343
|
// ("never ran, never will"), like any never-dispatched DAG step. Its wave
|
|
344
344
|
// sits after every DAG level so ledger-ordered readers render it last.
|
|
345
|
-
const synthesisEnabled =
|
|
345
|
+
const synthesisEnabled = input.synthesisEnabled ?? true;
|
|
346
346
|
const synthesisWave = Math.max(-1, ...levels.values()) + 1;
|
|
347
347
|
await DBOS.runStep(async () => {
|
|
348
348
|
const dagRows = input.steps.map((step) => ({
|
|
@@ -403,10 +403,8 @@ export async function runExecuteAnalysisBody(input, deps) {
|
|
|
403
403
|
// recovered replay reuses the original instants (same rule as `startedAtMs`).
|
|
404
404
|
const synthStartedAtMs = await DBOS.now();
|
|
405
405
|
// Mark the seeded `synthesis` row running. The same upsert the DAG children
|
|
406
|
-
// use: it stamps started_at, and it
|
|
407
|
-
//
|
|
408
|
-
// redeploy) — the mark self-heals rather than trusting the seed's snapshot
|
|
409
|
-
// of the config. Log-don't-fail: a progress row must never fail an
|
|
406
|
+
// use: it stamps started_at, and it creates the row outright if a legacy
|
|
407
|
+
// persisted input did not seed it. Log-don't-fail: a progress row must never fail an
|
|
410
408
|
// otherwise-healthy run. Cancellation still re-propagates — it is not a
|
|
411
409
|
// write failure, and swallowing it here would only defer it one operation.
|
|
412
410
|
try {
|
package/package.json
CHANGED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ephemeral Executor — read-only sandbox agent for quick computations.
|
|
3
|
-
* Cannot create files; step conventions don't apply.
|
|
4
|
-
*/
|
|
5
|
-
import type { AgentDefinition } from "../../loop/types.js";
|
|
6
|
-
import { type SandboxAgentDeps } from "./shared.js";
|
|
7
|
-
import type { AgentMeta } from "./types.js";
|
|
8
|
-
export declare const meta: AgentMeta;
|
|
9
|
-
export declare function createEphemeralExecutorAgent(deps: SandboxAgentDeps): AgentDefinition;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ephemeral Executor — read-only sandbox agent for quick computations.
|
|
3
|
-
* Cannot create files; step conventions don't apply.
|
|
4
|
-
*/
|
|
5
|
-
import { ephemeralExecutorPrompt } from "../../prompts/sandbox/ephemeral-executor.js";
|
|
6
|
-
import { BASE_SANDBOX_TOOLS, createSandboxAgent } from "./shared.js";
|
|
7
|
-
export const meta = {
|
|
8
|
-
id: "ephemeral-executor",
|
|
9
|
-
capabilities: ["data inspection", "quick statistics", "data transformation", "table preview"],
|
|
10
|
-
suitableFor: ["bulk-rna-seq", "single-cell", "proteomics", "metabolomics", "genomics", "transcriptomics", "general-omics"],
|
|
11
|
-
skills: ["shared/omics-general"],
|
|
12
|
-
tools: [...BASE_SANDBOX_TOOLS, "pubmed", "searchGene", "searchPathway", "lookupGoTerm", "searchInteractions"],
|
|
13
|
-
plannable: false,
|
|
14
|
-
};
|
|
15
|
-
export function createEphemeralExecutorAgent(deps) {
|
|
16
|
-
return createSandboxAgent(deps, meta, ephemeralExecutorPrompt, {
|
|
17
|
-
appendAnalysisStepStandards: false,
|
|
18
|
-
readOnly: true,
|
|
19
|
-
});
|
|
20
|
-
}
|