@inflexa-ai/harness 0.11.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.
Files changed (66) hide show
  1. package/dist/agents/conversation-agent.d.ts +9 -15
  2. package/dist/agents/conversation-agent.js +9 -15
  3. package/dist/agents/sandbox/index.js +0 -3
  4. package/dist/agents/sandbox/shared.d.ts +2 -2
  5. package/dist/agents/sandbox/shared.js +1 -1
  6. package/dist/config/resource-limits.d.ts +0 -7
  7. package/dist/config/resource-limits.js +0 -1
  8. package/dist/execution/dbos-run-launcher.js +1 -26
  9. package/dist/execution/run-authorizer.d.ts +1 -1
  10. package/dist/execution/run-authorizer.js +1 -1
  11. package/dist/execution/run-launcher.d.ts +2 -18
  12. package/dist/execution/run-launcher.js +2 -4
  13. package/dist/index.d.ts +3 -3
  14. package/dist/index.js +4 -6
  15. package/dist/loop/run-agent.js +1 -0
  16. package/dist/memory/reconstruct-cards.d.ts +1 -1
  17. package/dist/memory/reconstruct-cards.js +7 -5
  18. package/dist/memory/thread-history.js +39 -1
  19. package/dist/prompts/conversation.d.ts +1 -1
  20. package/dist/prompts/conversation.js +27 -10
  21. package/dist/prompts/planner.d.ts +3 -0
  22. package/dist/prompts/planner.js +1 -1
  23. package/dist/prompts/sandbox-standards.d.ts +4 -4
  24. package/dist/prompts/sandbox-standards.js +5 -5
  25. package/dist/runtime/assemble.d.ts +5 -10
  26. package/dist/runtime/assemble.js +0 -4
  27. package/dist/runtime/boot.d.ts +2 -2
  28. package/dist/runtime/boot.js +1 -1
  29. package/dist/runtime/dbos.d.ts +4 -5
  30. package/dist/runtime/dbos.js +6 -7
  31. package/dist/sandbox/mount-plan.d.ts +1 -1
  32. package/dist/sandbox/sandbox-error.d.ts +1 -1
  33. package/dist/sandbox/sandbox-error.js +1 -1
  34. package/dist/sandbox/types.d.ts +1 -1
  35. package/dist/schemas/validate-plan.d.ts +1 -1
  36. package/dist/state/index.d.ts +4 -4
  37. package/dist/state/index.js +2 -2
  38. package/dist/state/plans.js +1 -1
  39. package/dist/state/runs.d.ts +10 -0
  40. package/dist/state/runs.js +47 -0
  41. package/dist/tools/__fixtures__/tool-context.js +1 -0
  42. package/dist/tools/ad-hoc-router.d.ts +31 -0
  43. package/dist/tools/ad-hoc-router.js +132 -0
  44. package/dist/tools/analysis-invocation.d.ts +4 -0
  45. package/dist/tools/analysis-invocation.js +17 -0
  46. package/dist/tools/bio/search-geo-datasets.js +2 -1
  47. package/dist/tools/define-tool.d.ts +6 -2
  48. package/dist/tools/define-tool.js +1 -1
  49. package/dist/tools/execute-analysis.d.ts +43 -0
  50. package/dist/tools/execute-analysis.js +252 -0
  51. package/dist/tools/iterate-report.js +7 -6
  52. package/dist/tools/research/generate-plan.js +1 -1
  53. package/dist/tools/workspace/list-files.js +4 -3
  54. package/dist/workflows/execute-analysis.d.ts +5 -5
  55. package/dist/workflows/execute-analysis.js +3 -5
  56. package/package.json +1 -1
  57. package/dist/agents/sandbox/ephemeral-executor.d.ts +0 -9
  58. package/dist/agents/sandbox/ephemeral-executor.js +0 -20
  59. package/dist/execution/ephemeral-runner.d.ts +0 -98
  60. package/dist/execution/ephemeral-runner.js +0 -189
  61. package/dist/prompts/sandbox/ephemeral-executor.d.ts +0 -1
  62. package/dist/prompts/sandbox/ephemeral-executor.js +0 -66
  63. package/dist/tools/execute-plan.d.ts +0 -64
  64. package/dist/tools/execute-plan.js +0 -211
  65. package/dist/tools/run-ephemeral.d.ts +0 -33
  66. package/dist/tools/run-ephemeral.js +0 -65
@@ -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
- }
@@ -1,98 +0,0 @@
1
- /**
2
- * Ephemeral runner — read-only sandbox execution for ad-hoc data
3
- * exploration, run as a turn-scoped DBOS workflow.
4
- *
5
- * The `run_ephemeral` conversation tool authorizes the run at the async
6
- * edge, starts this workflow, and awaits its result inline within the chat
7
- * turn. The workflow is "bare": it has no run-event stream, so `emit` is a
8
- * no-op and live sandbox progress events are dropped. It is never recovered
9
- * — chat disconnect cancels it (`DBOS.cancelWorkflow`), and a boot-time sweep
10
- * cancels any `ephemeral:`-prefixed PENDING workflow a dead pod left behind.
11
- *
12
- * Lifecycle: create sandbox → build the ephemeral-executor with synthetic
13
- * step coords (`runId = stepId = "ephemeral"`, the literal the host-managed
14
- * billing rules key on) → drive `runAgent` over the executor with `durableStep` →
15
- * final text + step count → teardown the sandbox in `finally` on every
16
- * terminal path.
17
- *
18
- * Read-only is enforced, not aspirational: the executor agent is built with
19
- * `readOnly: true` (no `write_file`/`edit_file` tools) and its sandbox is
20
- * provisioned with no read-write mount — only the read-only analysis tree.
21
- * Container-local `/tmp` stays writable (torn down with the sandbox); nothing
22
- * persists. The prompt's "you physically cannot write files" is now true.
23
- */
24
- import type { Pool } from "pg";
25
- import type { RunSession } from "../auth/types.js";
26
- import type { EnvironmentStorePaths } from "../config/environment-stores.js";
27
- import type { ResourcePolicy } from "../config/resource-limits.js";
28
- import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
29
- import type { SandboxClient } from "../sandbox/client.js";
30
- import type { BioToolKeys } from "../tools/bio/keys.js";
31
- import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
32
- import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
33
- import type { Logger } from "../lib/logger.js";
34
- /** Sub-agent identity — appended to `callPath`, set as `agentId`. */
35
- export declare const EPHEMERAL_AGENT_ID = "ephemeral-executor";
36
- /** Workflow-id prefix — the boot-time never-recover sweep matches on it. */
37
- export declare const EPHEMERAL_WORKFLOW_PREFIX: "ephemeral:";
38
- /** The body's construction-time deps — closed over at registration. */
39
- export interface EphemeralDeps extends EnvironmentStorePaths {
40
- /** Operational logging seam; omitted falls back to no-op. */
41
- readonly logger?: Logger;
42
- readonly provider: ChatProvider;
43
- readonly pool: Pool;
44
- readonly sandboxClient: SandboxClient;
45
- readonly workspaceFs: WorkspaceFilesystem;
46
- /** Embedding provider for the executor's in-sandbox `workspace_search`. */
47
- readonly embedding: EmbeddingProvider;
48
- /** Workspace-root resolution seam (see workspace/paths.ts). */
49
- readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
50
- /** Model id — provenance / metric label; the provider owns the wire model. */
51
- readonly model: string;
52
- /** API keys for the bio/chem tools the ephemeral executor may use. */
53
- readonly bioKeys: BioToolKeys;
54
- /** Host resource policy — its `ephemeral` spec overrides the default sandbox size. */
55
- readonly resourcePolicy?: ResourcePolicy;
56
- }
57
- /**
58
- * Workflow input. JSON-serialisable — the authorized `RunSession` carries the
59
- * run credential; the body reads it and never re-authorizes.
60
- */
61
- export interface EphemeralWorkflowInput {
62
- readonly runSession: RunSession;
63
- readonly prompt: string;
64
- /** Optional cap on the inner agent's tool-call budget. */
65
- readonly maxIterations?: number;
66
- /** Optional deadline override for sandbox-server execs (unix ms). */
67
- readonly deadlineMs?: number;
68
- }
69
- export interface EphemeralResult {
70
- readonly text: string;
71
- readonly durationMs: number;
72
- readonly stepsUsed: number;
73
- }
74
- /**
75
- * Register the ephemeral workflow with DBOS. Returns the registered callable
76
- * so the `run_ephemeral` tool can dispatch via `DBOS.startWorkflow`.
77
- */
78
- export declare function registerEphemeralWorkflow(deps: EphemeralDeps): (input: EphemeralWorkflowInput) => Promise<EphemeralResult>;
79
- /**
80
- * The executor's seed: its workspace frame, then the caller's ask.
81
- *
82
- * An ephemeral run is not scheduled by `executeAnalysis`, so no step briefing is
83
- * composed for it — and the sandbox system prompt names no paths (it is static
84
- * per agent type, so the provider's prompt cache can reuse it). The analysis
85
- * root would otherwise reach the agent nowhere: `run_ephemeral`'s `prompt` is
86
- * free text authored by the conversation agent.
87
- *
88
- * The frame differs from a step's, so this does not reuse `renderWorkspace`:
89
- * there is no writable directory to name. The read-only tree IS the cwd.
90
- */
91
- export declare function ephemeralSeed(analysisId: string, prompt: string): string;
92
- /**
93
- * Run the ephemeral-executor agent against a fresh read-only sandbox.
94
- * Throws on sandbox start failure; otherwise returns the agent's final
95
- * text plus timing. Sandbox teardown fires on every terminal path —
96
- * success, error, cancel.
97
- */
98
- export declare function runEphemeralBody(input: EphemeralWorkflowInput, deps: EphemeralDeps): Promise<EphemeralResult>;
@@ -1,189 +0,0 @@
1
- /**
2
- * Ephemeral runner — read-only sandbox execution for ad-hoc data
3
- * exploration, run as a turn-scoped DBOS workflow.
4
- *
5
- * The `run_ephemeral` conversation tool authorizes the run at the async
6
- * edge, starts this workflow, and awaits its result inline within the chat
7
- * turn. The workflow is "bare": it has no run-event stream, so `emit` is a
8
- * no-op and live sandbox progress events are dropped. It is never recovered
9
- * — chat disconnect cancels it (`DBOS.cancelWorkflow`), and a boot-time sweep
10
- * cancels any `ephemeral:`-prefixed PENDING workflow a dead pod left behind.
11
- *
12
- * Lifecycle: create sandbox → build the ephemeral-executor with synthetic
13
- * step coords (`runId = stepId = "ephemeral"`, the literal the host-managed
14
- * billing rules key on) → drive `runAgent` over the executor with `durableStep` →
15
- * final text + step count → teardown the sandbox in `finally` on every
16
- * terminal path.
17
- *
18
- * Read-only is enforced, not aspirational: the executor agent is built with
19
- * `readOnly: true` (no `write_file`/`edit_file` tools) and its sandbox is
20
- * provisioned with no read-write mount — only the read-only analysis tree.
21
- * Container-local `/tmp` stays writable (torn down with the sandbox); nothing
22
- * persists. The prompt's "you physically cannot write files" is now true.
23
- */
24
- import { DBOS, Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
25
- import { forSubAgent } from "../auth/types.js";
26
- import { finalText, runAgent } from "../loop/run-agent.js";
27
- import { durableStep } from "../loop/run-step.js";
28
- import { generateExecutionId } from "../sandbox/execution-id.js";
29
- import { mintSandboxIdentity } from "../sandbox/identity.js";
30
- import { createEphemeralExecutorAgent } from "../agents/sandbox/ephemeral-executor.js";
31
- import { createNoopLogger } from "../lib/console-logger.js";
32
- /** Sub-agent identity — appended to `callPath`, set as `agentId`. */
33
- export const EPHEMERAL_AGENT_ID = "ephemeral-executor";
34
- /**
35
- * Synthetic run/step literal stamped into the agent's step coords.
36
- * Host-managed billing rules key on this literal to recognize chat-turn
37
- * ephemeral runs (no `cortex_runs` row, no DBOS stream).
38
- */
39
- const EPHEMERAL_RUN_LITERAL = "ephemeral";
40
- /** Workflow-id prefix — the boot-time never-recover sweep matches on it. */
41
- export const EPHEMERAL_WORKFLOW_PREFIX = "ephemeral:";
42
- /** Default budget — chat-turn-scoped, no recovery, no checkpointing. */
43
- const DEFAULT_DEADLINE_MS = 120_000;
44
- /** Ephemeral runs have no planner estimate, so the sandbox box is chosen here
45
- * explicitly — the policy's `ephemeral` spec when supplied, otherwise this
46
- * mid-range default; either way clamped to cluster limits at create time. */
47
- const EPHEMERAL_SANDBOX_RESOURCES = { cpu: 4, memoryGb: 8 };
48
- /**
49
- * Register the ephemeral workflow with DBOS. Returns the registered callable
50
- * so the `run_ephemeral` tool can dispatch via `DBOS.startWorkflow`.
51
- */
52
- export function registerEphemeralWorkflow(deps) {
53
- return DBOS.registerWorkflow((input) => runEphemeralBody(input, deps), { name: "ephemeral" });
54
- }
55
- /**
56
- * The executor's seed: its workspace frame, then the caller's ask.
57
- *
58
- * An ephemeral run is not scheduled by `executeAnalysis`, so no step briefing is
59
- * composed for it — and the sandbox system prompt names no paths (it is static
60
- * per agent type, so the provider's prompt cache can reuse it). The analysis
61
- * root would otherwise reach the agent nowhere: `run_ephemeral`'s `prompt` is
62
- * free text authored by the conversation agent.
63
- *
64
- * The frame differs from a step's, so this does not reuse `renderWorkspace`:
65
- * there is no writable directory to name. The read-only tree IS the cwd.
66
- */
67
- export function ephemeralSeed(analysisId, prompt) {
68
- const root = `/${analysisId}`;
69
- return [
70
- "## Workspace",
71
- `- Analysis root (read-only): \`${root}\` — inputs at \`${root}/data/inputs/\`, prior runs at \`${root}/runs/\`.`,
72
- `- The analysis root is also your cwd, and what relative paths resolve against. The sandbox is read-only: there is nowhere to write, so return every result inline.`,
73
- "",
74
- prompt,
75
- ].join("\n");
76
- }
77
- /**
78
- * Run the ephemeral-executor agent against a fresh read-only sandbox.
79
- * Throws on sandbox start failure; otherwise returns the agent's final
80
- * text plus timing. Sandbox teardown fires on every terminal path —
81
- * success, error, cancel.
82
- */
83
- export async function runEphemeralBody(input, deps) {
84
- const { runSession } = input;
85
- if (runSession.scope.kind !== "analysis") {
86
- throw new Error(`runEphemeralBody requires an analysis-scoped session — got ${runSession.scope.kind}`);
87
- }
88
- const analysisId = runSession.scope.analysisId;
89
- // Bound after the scope guard: `analysisId` comes off the session's scope,
90
- // not the workflow input.
91
- const logger = (deps.logger ?? createNoopLogger()).named("ephemeral").with({ analysisId });
92
- const startedAt = performance.now();
93
- const executionId = generateExecutionId(EPHEMERAL_AGENT_ID);
94
- const workflowId = DBOS.workflowID ?? `${EPHEMERAL_WORKFLOW_PREFIX}${executionId}`;
95
- const childSession = forSubAgent(runSession, EPHEMERAL_AGENT_ID);
96
- // Resolver throw = loud workflow failure, per the workspace-root-resolution contract.
97
- const workspaceRoot = deps.resolveWorkspaceRoot(analysisId);
98
- logger.info("starting sandbox", { executionId });
99
- const sandbox = await deps.sandboxClient.createSandbox({
100
- runId: EPHEMERAL_RUN_LITERAL,
101
- stepId: EPHEMERAL_RUN_LITERAL,
102
- analysisId,
103
- execId: null,
104
- childWorkflowId: workflowId,
105
- resources: deps.resourcePolicy?.ephemeral ?? EPHEMERAL_SANDBOX_RESOURCES,
106
- // No read-write step mount — only the read-only analysis tree.
107
- readOnly: true,
108
- }, mintSandboxIdentity(EPHEMERAL_RUN_LITERAL));
109
- try {
110
- // The fallback reads the CHECKPOINTED clock, not `Date.now()`: `awaitExec`
111
- // gates on this absolute deadline, and its recovery-pull is a durable step,
112
- // so a wall-clock deadline that grew on replay would shift which loop
113
- // iteration crosses the deadline and desynchronise the recorded function-ID
114
- // sequence (see the harness-durable-runtime spec; sandbox-step.ts does the
115
- // same). `input.deadlineMs`, when present, is already replay-stable.
116
- const deadlineAbs = input.deadlineMs ?? (await DBOS.now()) + DEFAULT_DEADLINE_MS;
117
- const nextFunctionId = makeNextFunctionId();
118
- const sandboxAgentDeps = {
119
- provider: deps.provider,
120
- pool: deps.pool,
121
- sandboxClient: deps.sandboxClient,
122
- workspaceFs: deps.workspaceFs,
123
- embedding: deps.embedding,
124
- model: deps.model,
125
- bioKeys: deps.bioKeys,
126
- ...(deps.packagesFile ? { packagesFile: deps.packagesFile } : {}),
127
- ...(deps.refStorePath ? { refStorePath: deps.refStorePath } : {}),
128
- step: {
129
- sandbox,
130
- workspaceRoot,
131
- analysisId,
132
- runId: EPHEMERAL_RUN_LITERAL,
133
- stepId: EPHEMERAL_RUN_LITERAL,
134
- workflowId,
135
- // Enforced read-only: the executor agent gets no write_file/edit_file
136
- // tools and the sandbox has no read-write mount. This prefix is not a
137
- // writable location — it only resolves `execute_command`'s default cwd
138
- // to the read-only analysis-tree root (`/{analysisId}`), where reads land.
139
- allowedWritePrefix: workspaceRoot,
140
- nextFunctionId,
141
- deadlineMs: () => deadlineAbs,
142
- },
143
- };
144
- const agentDef = createEphemeralExecutorAgent(sandboxAgentDeps);
145
- const agent = input.maxIterations !== undefined ? { ...agentDef, maxIterations: input.maxIterations } : agentDef;
146
- const signal = new AbortController().signal;
147
- const { messages: finalMessages } = await runAgent(agent, [{ role: "user", content: ephemeralSeed(analysisId, input.prompt) }], childSession, {
148
- provider: deps.provider,
149
- signal,
150
- emit: () => { },
151
- runStep: durableStep,
152
- isFatalLoopError: (err) => err instanceof DBOSErrors.DBOSWorkflowCancelledError,
153
- });
154
- const text = finalText(finalMessages);
155
- const stepsUsed = countAssistantTurns(finalMessages);
156
- const durationMs = Math.round(performance.now() - startedAt);
157
- logger.info("completed", { executionId, stepsUsed, durationMs });
158
- return { text, stepsUsed, durationMs };
159
- }
160
- catch (err) {
161
- const message = err instanceof Error ? err.message : String(err);
162
- logger.error("failed", { executionId, err: message });
163
- throw err;
164
- }
165
- finally {
166
- try {
167
- await deps.sandboxClient.teardown(sandbox);
168
- }
169
- catch (teardownErr) {
170
- // Teardown is idempotent — "already gone" is success. A failure here
171
- // is best-effort logged so the chat turn doesn't fail on cleanup.
172
- logger.warn("teardown failed (non-fatal)", { executionId, ...logger.errorFields(teardownErr) });
173
- }
174
- }
175
- }
176
- /** Per-call function-id minter — replay-deterministic. */
177
- function makeNextFunctionId() {
178
- let n = 0;
179
- return () => `fn-${(n++).toString(36)}`;
180
- }
181
- /** The agent's final assistant turn is the `stepsUsed` proxy — every loop
182
- * iteration produces one assistant message. */
183
- function countAssistantTurns(messages) {
184
- let n = 0;
185
- for (const m of messages)
186
- if (m.role === "assistant")
187
- n++;
188
- return n;
189
- }
@@ -1 +0,0 @@
1
- export declare const ephemeralExecutorPrompt = "# Ephemeral Executor\n\nYou are a read-only compute agent. You execute scripts on the user's data\nand return the results. You **cannot create or save files** \u2014 your sandbox\nis fully read-only.\n\n## What You Do\n\n1. Read data files to understand their structure\n2. Write and execute R or Python scripts via `execute_command`\n3. Return the script output (tables, statistics, plot data) in your response\n\n## Constraints\n\n- **Read-only**: You physically cannot write files. Do not attempt to save\n output to disk. All results must be returned in your text response.\n- **Inline output**: Print results to stdout. For plots, use base64 encoding\n and print the encoded string so the caller can display it.\n- **Structured responses**: When returning tabular data, format as markdown\n tables or JSON arrays. When returning statistics, use clear labeled output.\n\n## Script Execution Pattern\n\nFor Python:\n```\nexecute_command python3 -c \"\nimport pandas as pd\ndf = pd.read_csv('/{resourceId}/data/inputs/{fileId}/data.csv')\nprint(df.describe().to_markdown())\n\"\n```\n\nFor R:\n```\nexecute_command Rscript -e \"\ndf <- read.csv('/{resourceId}/data/inputs/{fileId}/data.csv')\nsummary(df)\n\"\n```\n\nFor plots (base64):\n```\nexecute_command python3 -c \"\nimport matplotlib\nmatplotlib.use('Agg')\nimport matplotlib.pyplot as plt\nimport pandas as pd\nimport base64, io\n\ndf = pd.read_csv('...')\nfig, ax = plt.subplots()\ndf.plot(ax=ax)\nbuf = io.BytesIO()\nfig.savefig(buf, format='png')\nprint(base64.b64encode(buf.getvalue()).decode())\n\"\n```\n\n## Do NOT\n\n- Attempt to write files \u2014 it will fail silently or error\n- Install packages at runtime\n- Return raw binary data without base64 encoding\n- Guess data structure without inspecting the file first\n- Run long-running computations (you have a short timeout)\n";
@@ -1,66 +0,0 @@
1
- export const ephemeralExecutorPrompt = `# Ephemeral Executor
2
-
3
- You are a read-only compute agent. You execute scripts on the user's data
4
- and return the results. You **cannot create or save files** — your sandbox
5
- is fully read-only.
6
-
7
- ## What You Do
8
-
9
- 1. Read data files to understand their structure
10
- 2. Write and execute R or Python scripts via \`execute_command\`
11
- 3. Return the script output (tables, statistics, plot data) in your response
12
-
13
- ## Constraints
14
-
15
- - **Read-only**: You physically cannot write files. Do not attempt to save
16
- output to disk. All results must be returned in your text response.
17
- - **Inline output**: Print results to stdout. For plots, use base64 encoding
18
- and print the encoded string so the caller can display it.
19
- - **Structured responses**: When returning tabular data, format as markdown
20
- tables or JSON arrays. When returning statistics, use clear labeled output.
21
-
22
- ## Script Execution Pattern
23
-
24
- For Python:
25
- \`\`\`
26
- execute_command python3 -c "
27
- import pandas as pd
28
- df = pd.read_csv('/{resourceId}/data/inputs/{fileId}/data.csv')
29
- print(df.describe().to_markdown())
30
- "
31
- \`\`\`
32
-
33
- For R:
34
- \`\`\`
35
- execute_command Rscript -e "
36
- df <- read.csv('/{resourceId}/data/inputs/{fileId}/data.csv')
37
- summary(df)
38
- "
39
- \`\`\`
40
-
41
- For plots (base64):
42
- \`\`\`
43
- execute_command python3 -c "
44
- import matplotlib
45
- matplotlib.use('Agg')
46
- import matplotlib.pyplot as plt
47
- import pandas as pd
48
- import base64, io
49
-
50
- df = pd.read_csv('...')
51
- fig, ax = plt.subplots()
52
- df.plot(ax=ax)
53
- buf = io.BytesIO()
54
- fig.savefig(buf, format='png')
55
- print(base64.b64encode(buf.getvalue()).decode())
56
- "
57
- \`\`\`
58
-
59
- ## Do NOT
60
-
61
- - Attempt to write files — it will fail silently or error
62
- - Install packages at runtime
63
- - Return raw binary data without base64 encoding
64
- - Guess data structure without inspecting the file first
65
- - Run long-running computations (you have a short timeout)
66
- `;
@@ -1,64 +0,0 @@
1
- /**
2
- * executePlan — launch the DBOS `executeAnalysis` parent workflow for an
3
- * approved plan.
4
- *
5
- * Results are pull-only: the tool does NOT register a `.then(saveRunResult)`
6
- * or any completion callback that writes to the conversation thread. The
7
- * conversation agent retrieves run results via `inspectRun` on a later turn.
8
- *
9
- * Authorization contract: the tool runs in the chat HTTP route, so its session
10
- * carries the live auth. It authorizes through the injected `RunAuthorizer`
11
- * seam and never reads a credential. The flow is dedup → reserve → authorize
12
- * → startWorkflow:
13
- *
14
- * 1. Dedup pre-check via `queryActiveRun`. On hit, return the existing
15
- * `runId` with no authorization and no `startWorkflow`.
16
- * 2. Reserve the slot: `insertRun` the `cortex_runs` row (`runId =
17
- * randomUUID()`, the bare UUID that IS the DBOS workflowID) BEFORE
18
- * authorizing. The partial-unique index is the race backstop; the loser
19
- * returns the winner's `runId` without authorizing — nothing to revoke.
20
- * 3. `runAuthorizer.authorize({ ..., frame: { runId } })` — authorizes the
21
- * run; the authorizer's persisted handle now lands because the row
22
- * exists. On failure, mark the reserved row `failed` (releasing the dedup
23
- * slot for a retry) and rethrow.
24
- * 4. `runLauncher.launch(executeAnalysis, { workflowId: runId }, ...)`,
25
- * passing the `RunSession` in the workflow input. On failure, revoke + mark
26
- * failed.
27
- */
28
- import type { Pool } from "pg";
29
- import type { ResourcePolicy } from "../config/resource-limits.js";
30
- import type { RunAuthorizer } from "../execution/run-authorizer.js";
31
- import type { RunLauncher } from "../execution/run-launcher.js";
32
- import type { ExecuteAnalysisInput, ExecuteAnalysisResult } from "../workflows/execute-analysis.js";
33
- type ExecuteAnalysisWorkflow = (input: ExecuteAnalysisInput) => Promise<ExecuteAnalysisResult>;
34
- export interface ExecutePlanToolDeps {
35
- readonly pool: Pool;
36
- /** Registered `executeAnalysis` workflow callable — produced by `registerExecuteAnalysis` (wired by `assembleCoreRuntime`). */
37
- readonly executeAnalysisWorkflow: ExecuteAnalysisWorkflow;
38
- /** Authorizes the run at the async edge and revokes on the abort paths. */
39
- readonly runAuthorizer: RunAuthorizer;
40
- /** Starts the durable run — the durability engine stays behind this seam. */
41
- readonly runLauncher: RunLauncher;
42
- /**
43
- * Host resource policy. The machine budget is snapshotted into the workflow
44
- * input here at the async edge — the workflow body never reads live config,
45
- * so a mid-run config edit cannot change a running run's admission decisions.
46
- */
47
- readonly resourcePolicy?: ResourcePolicy;
48
- }
49
- export declare class PlanNotFoundError extends Error {
50
- constructor(planId: string);
51
- }
52
- export declare class PlanMalformedError extends Error {
53
- constructor(planId: string);
54
- }
55
- export declare class PlanValidationError extends Error {
56
- readonly errors: string[];
57
- constructor(planId: string, errors: string[]);
58
- }
59
- export declare function createExecutePlanTool(deps: ExecutePlanToolDeps): import("./define-tool.js").Tool<{
60
- planId: string;
61
- }, {
62
- runId: string;
63
- }>;
64
- export {};
@@ -1,211 +0,0 @@
1
- /**
2
- * executePlan — launch the DBOS `executeAnalysis` parent workflow for an
3
- * approved plan.
4
- *
5
- * Results are pull-only: the tool does NOT register a `.then(saveRunResult)`
6
- * or any completion callback that writes to the conversation thread. The
7
- * conversation agent retrieves run results via `inspectRun` on a later turn.
8
- *
9
- * Authorization contract: the tool runs in the chat HTTP route, so its session
10
- * carries the live auth. It authorizes through the injected `RunAuthorizer`
11
- * seam and never reads a credential. The flow is dedup → reserve → authorize
12
- * → startWorkflow:
13
- *
14
- * 1. Dedup pre-check via `queryActiveRun`. On hit, return the existing
15
- * `runId` with no authorization and no `startWorkflow`.
16
- * 2. Reserve the slot: `insertRun` the `cortex_runs` row (`runId =
17
- * randomUUID()`, the bare UUID that IS the DBOS workflowID) BEFORE
18
- * authorizing. The partial-unique index is the race backstop; the loser
19
- * returns the winner's `runId` without authorizing — nothing to revoke.
20
- * 3. `runAuthorizer.authorize({ ..., frame: { runId } })` — authorizes the
21
- * run; the authorizer's persisted handle now lands because the row
22
- * exists. On failure, mark the reserved row `failed` (releasing the dedup
23
- * slot for a retry) and rethrow.
24
- * 4. `runLauncher.launch(executeAnalysis, { workflowId: runId }, ...)`,
25
- * passing the `RunSession` in the workflow input. On failure, revoke + mark
26
- * failed.
27
- */
28
- import { randomUUID } from "node:crypto";
29
- import { ok } from "neverthrow";
30
- import { z } from "zod";
31
- import { buildRunCardData } from "../memory/card-builders.js";
32
- import { unwrapOrThrow } from "../lib/result.js";
33
- import { validatePlan } from "../schemas/validate-plan.js";
34
- import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
35
- import { RunDedupCollisionError, insertRun, loadPlan, queryActiveRun, updateRunStatus } from "../state/index.js";
36
- import { defineTool } from "./define-tool.js";
37
- const planIdSchema = z.string().regex(/^pln-[a-f0-9]{8}$/, "planId must be a pln-<8hex> value");
38
- const inputSchema = z.object({
39
- planId: planIdSchema,
40
- });
41
- export class PlanNotFoundError extends Error {
42
- constructor(planId) {
43
- super(`Plan ${planId} not found for this analysis`);
44
- this.name = "PlanNotFoundError";
45
- }
46
- }
47
- export class PlanMalformedError extends Error {
48
- constructor(planId) {
49
- super(`Plan ${planId} is malformed`);
50
- this.name = "PlanMalformedError";
51
- }
52
- }
53
- export class PlanValidationError extends Error {
54
- errors;
55
- constructor(planId, errors) {
56
- super(`Plan ${planId} failed validation: ${errors.length} error(s)`);
57
- this.errors = errors;
58
- this.name = "PlanValidationError";
59
- }
60
- }
61
- function isDedupCollision(err) {
62
- return err instanceof RunDedupCollisionError || (err instanceof Error && err.name === "RunDedupCollisionError");
63
- }
64
- export function createExecutePlanTool(deps) {
65
- const { pool, executeAnalysisWorkflow, runAuthorizer, runLauncher, resourcePolicy } = deps;
66
- return defineTool({
67
- id: "execute_plan",
68
- description: "Execute an approved analysis plan by its planId. " +
69
- "Pass the planId returned from generatePlan — the tool resolves the plan " +
70
- "server-side, validates the DAG, and starts the executeAnalysis workflow " +
71
- "asynchronously. Returns the runId on success. " +
72
- "The user sees a run card in chat (also tracked in the Runs panel); " +
73
- "run results become available for inspection on a later turn. " +
74
- "Do not instruct the user to invoke any tool.",
75
- inputSchema,
76
- execute: async (input, ctx) => {
77
- const { planId } = input;
78
- // Mirror show_plan: emit a live display card so chat renders a run card
79
- // rather than a generic "used 1 tool" chip. Null (plan/run unresolvable)
80
- // simply skips the card — the run still starts.
81
- const emitRunCard = async (analysisId, runId) => {
82
- const card = await buildRunCardData(pool, {
83
- planId,
84
- analysisId,
85
- runId,
86
- }).unwrapOr(null);
87
- if (card) {
88
- await ctx.emit({
89
- type: "data-run-card",
90
- source: ctx.session.provenance,
91
- data: card,
92
- });
93
- }
94
- };
95
- const { session } = ctx;
96
- if (session.scope.kind !== "analysis") {
97
- throw new Error("executePlan can only be invoked on an analysis-scoped session");
98
- }
99
- if (!session.auth) {
100
- throw new Error("executePlan: session is missing its auth capability");
101
- }
102
- const analysisId = session.scope.analysisId;
103
- const threadId = session.scope.threadId ?? null;
104
- const raw = unwrapOrThrow(await loadPlan(pool, planId, { analysisId }));
105
- if (!raw)
106
- throw new PlanNotFoundError(planId);
107
- const parsed = AnalysisPlanSchema.safeParse(raw);
108
- if (!parsed.success)
109
- throw new PlanMalformedError(planId);
110
- const plan = parsed.data;
111
- const result = validatePlan(plan);
112
- if (!result.valid) {
113
- throw new PlanValidationError(planId, result.errors);
114
- }
115
- // (1) Dedup pre-check — common case is the user re-clicked Execute on
116
- // an in-flight plan. A hit avoids both the authorization and the DBOS
117
- // startWorkflow.
118
- const existingPre = unwrapOrThrow(await queryActiveRun(pool, analysisId, planId));
119
- if (existingPre) {
120
- await emitRunCard(analysisId, existingPre.runId);
121
- return ok({ runId: existingPre.runId });
122
- }
123
- // (2) Reserve the dedup slot by inserting the run row BEFORE authorizing.
124
- // The partial-unique index is the race backstop for two callers that both
125
- // passed the pre-check; the loser returns the winner's runId without
126
- // having authorized — there is nothing to revoke. The authorization handle is left
127
- // null here and populated by the authorizer, whose UPDATE lands
128
- // because the row now exists.
129
- const runId = randomUUID();
130
- try {
131
- // On the dedup collision `insertRun` throws `RunDedupCollisionError`
132
- // (a control-flow signal that rides out of the ResultAsync as a
133
- // rejection) — `unwrapOrThrow` never sees it; the catch below does.
134
- unwrapOrThrow(await insertRun(pool, {
135
- runId,
136
- analysisId,
137
- threadId,
138
- workflowName: "executeAnalysis",
139
- planId,
140
- }));
141
- }
142
- catch (err) {
143
- if (isDedupCollision(err)) {
144
- const existing = unwrapOrThrow(await queryActiveRun(pool, analysisId, planId));
145
- if (existing) {
146
- await emitRunCard(analysisId, existing.runId);
147
- return ok({ runId: existing.runId });
148
- }
149
- }
150
- throw err;
151
- }
152
- // (3) Authorize the run. The row exists, so the authorizer's persisted handle
153
- // persists. On failure, mark the reserved row failed — releasing the
154
- // partial-unique slot so a retry can re-run — and rethrow.
155
- let authorization;
156
- try {
157
- authorization = await runAuthorizer.authorize({
158
- auth: session.auth,
159
- scope: session.scope,
160
- provenance: session.provenance,
161
- frame: { runId },
162
- });
163
- }
164
- catch (err) {
165
- await updateRunStatus(pool, runId, "failed", "run authorization failed").match(() => { }, () => { });
166
- throw err;
167
- }
168
- const { runSession } = authorization;
169
- const planSummary = plan.title?.trim() || plan.analytical_narrative.trim().slice(0, 280);
170
- const workflowInput = {
171
- analysisId,
172
- planId,
173
- planSummary,
174
- threadId,
175
- steps: plan.steps.map((s) => ({
176
- id: s.id,
177
- depends_on: s.depends_on ?? [],
178
- })),
179
- // The step DATA, not a rendered prompt: each step's seed is composed
180
- // by the scheduler at dispatch, when its dependencies' results exist.
181
- // A prompt frozen here — before the first step has run — could never
182
- // mention them.
183
- planStepById: Object.fromEntries(plan.steps.map((s) => [s.id, s])),
184
- agentByStepId: Object.fromEntries(plan.steps.map((s) => [s.id, s.agent ?? "unknown"])),
185
- resourcesByStepId: Object.fromEntries(plan.steps.map((s) => {
186
- if (!s.resources) {
187
- throw new Error(`Step "${s.id}" has no resources — validatePlan should have rejected this plan`);
188
- }
189
- return [s.id, s.resources];
190
- })),
191
- timeoutByStepId: Object.fromEntries(plan.steps.filter((s) => s.timeout !== undefined).map((s) => [s.id, s.timeout])),
192
- budget: resourcePolicy?.budget,
193
- runSession,
194
- ownsMandate: authorization.ownsMandate, // oss-core-managed-ok
195
- };
196
- // (4) Dispatch the workflow. `workflowId = runId` — both are the same
197
- // bare UUID. On failure after insert + authorize, revoke the authorization and
198
- // mark the row failed.
199
- try {
200
- await runLauncher.launch(executeAnalysisWorkflow, { workflowId: runId }, workflowInput);
201
- }
202
- catch (err) {
203
- await runAuthorizer.revoke(authorization, "workflow-start-failed").catch(() => { });
204
- await updateRunStatus(pool, runId, "failed", "workflow start failed").match(() => { }, () => { });
205
- throw err;
206
- }
207
- await emitRunCard(analysisId, runId);
208
- return ok({ runId });
209
- },
210
- });
211
- }