@inflexa-ai/harness 0.34.0 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/agents/sandbox/shared.d.ts +1 -7
- package/dist/agents/sandbox/shared.js +6 -3
- package/dist/config/environment-stores.d.ts +9 -6
- package/dist/index.d.ts +4 -1
- package/dist/index.js +14 -2
- package/dist/loop/run-agent.js +9 -9
- package/dist/prompts/sandbox-standards.d.ts +7 -7
- package/dist/prompts/sandbox-standards.js +6 -6
- package/dist/providers/prompt-cache.d.ts +109 -14
- package/dist/providers/prompt-cache.js +196 -14
- package/dist/sandbox/client.d.ts +10 -1
- package/dist/sandbox/create-sandbox.d.ts +12 -0
- package/dist/sandbox/create-sandbox.js +45 -8
- package/dist/sandbox/docker-client.d.ts +11 -0
- package/dist/sandbox/docker-client.js +19 -5
- package/dist/sandbox/farm.d.ts +3 -1
- package/dist/sandbox/farm.js +3 -1
- package/dist/sandbox/image-packages.d.ts +107 -0
- package/dist/sandbox/image-packages.js +107 -0
- package/dist/sandbox/k8s-client.d.ts +11 -0
- package/dist/sandbox/k8s-client.js +20 -4
- package/dist/sandbox/sandbox-error.d.ts +40 -5
- package/dist/sandbox/sandbox-error.js +79 -5
- package/dist/tools/sandbox/list-available-packages.d.ts +18 -16
- package/dist/tools/sandbox/list-available-packages.js +90 -52
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Requirements:
|
|
|
9
9
|
- Node.js `>=24` (the runtime; Bun is used only to run tests)
|
|
10
10
|
- Postgres with pgvector (app tables + durable workflow state)
|
|
11
11
|
- a sandbox backend, selected by `SANDBOX_BACKEND` (`docker` | `k8s`)
|
|
12
|
+
- a package store, plus a `farmSource` that names where the package farm of an analysis comes from
|
|
12
13
|
- a chat/embedding provider — Anthropic, or any OpenAI-compatible endpoint
|
|
13
14
|
|
|
14
15
|
```bash
|
|
@@ -18,6 +19,8 @@ tsc -p tsconfig.json # build: emit dist/ from src/ (also `npm run b
|
|
|
18
19
|
bun test # run the test suite
|
|
19
20
|
```
|
|
20
21
|
|
|
22
|
+
The runtime image bakes no R or Python package, so the packages arrive from a host **package store** that the backend mounts read-only at `/mnt/libs`. `farmSource` is a **required** backend config field, and it has two kinds: `fixed` names one farm for every analysis (the managed shape), and `per-analysis` supplies a resolver of the embedder. `ExtendAnalysisFarm` is the optional link seam behind the `link_packages` tool. The harness composes no farm and installs nothing — the embedder owns the store — and the harness names the missing packages but no host command, because a managed deployment holds no `inflexa` binary.
|
|
23
|
+
|
|
21
24
|
The package emits `dist/` from `src/` and publishes only `dist`. Most configuration arrives through dependency objects passed to the composition point — the LLM backend, for instance, is a fully injected provider config (endpoint/key/model or a `LanguageModel` instance), not read from env; helper modules read conventional env vars (`DB_PG_*`, sandbox limits) — see `CONTEXT.md` and the specs for the catalog.
|
|
22
25
|
|
|
23
26
|
## Public surface
|
|
@@ -20,7 +20,7 @@ import type { Pool } from "pg";
|
|
|
20
20
|
import type { AgentDefinition } from "../../loop/types.js";
|
|
21
21
|
import type { ChatProvider, EmbeddingProvider } from "../../providers/types.js";
|
|
22
22
|
import type { SandboxClient } from "../../sandbox/client.js";
|
|
23
|
-
import type { ExtendAnalysisFarm, SandboxRef
|
|
23
|
+
import type { ExtendAnalysisFarm, SandboxRef } from "../../sandbox/types.js";
|
|
24
24
|
import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
|
|
25
25
|
import type { ProvenanceCollector as LineageCollector } from "../../provenance/collector.js";
|
|
26
26
|
import { type BioToolKeys } from "../../tools/bio/keys.js";
|
|
@@ -91,12 +91,6 @@ export interface SandboxAgentDeps extends EnvironmentStorePaths {
|
|
|
91
91
|
* and the package-link prompt layer appends. Without it, neither exists.
|
|
92
92
|
*/
|
|
93
93
|
readonly extendAnalysisFarm?: ExtendAnalysisFarm;
|
|
94
|
-
/**
|
|
95
|
-
* The declared toolchain owner. Keys the orient-core environment text.
|
|
96
|
-
* Absent keeps the legacy text, thus an old embedder keeps its cached
|
|
97
|
-
* prompt prefix.
|
|
98
|
-
*/
|
|
99
|
-
readonly toolchainSource?: ToolchainSource;
|
|
100
94
|
}
|
|
101
95
|
/** Per-agent override for the prompt composition and tool surface. */
|
|
102
96
|
export interface SandboxAgentPromptOptions {
|
|
@@ -183,10 +183,13 @@ function buildWorkspaceTools(deps, readOnly) {
|
|
|
183
183
|
export function createSandboxAgent(deps, meta, body, opts = {}) {
|
|
184
184
|
const appendStandards = opts.appendAnalysisStepStandards ?? true;
|
|
185
185
|
// The layer order is fixed by the harness-sandbox-agents spec: the agent
|
|
186
|
-
// body, the orient core keyed on the
|
|
187
|
-
// layer only when the seam is bound, then the step standards.
|
|
186
|
+
// body, the orient core keyed on the toolchain of the sandbox client, the
|
|
187
|
+
// package-link layer only when the seam is bound, then the step standards.
|
|
188
|
+
// The client is the one declaration: the environment it provisions and the
|
|
189
|
+
// text that describes it come from the same value, thus a bag cannot
|
|
190
|
+
// describe a toolchain the sandbox does not have.
|
|
188
191
|
const sandboxLayer = [
|
|
189
|
-
sandboxOrientCorePromptFor(deps.toolchainSource),
|
|
192
|
+
sandboxOrientCorePromptFor(deps.sandboxClient.toolchainSource),
|
|
190
193
|
...(deps.extendAnalysisFarm ? [sandboxPackageLinkPrompt] : []),
|
|
191
194
|
...(appendStandards ? [sandboxAnalysisStepStandardsPrompt] : []),
|
|
192
195
|
];
|
|
@@ -45,11 +45,14 @@ export interface EnvironmentStorePaths {
|
|
|
45
45
|
*/
|
|
46
46
|
readonly farmLockFile?: string;
|
|
47
47
|
/**
|
|
48
|
-
* Host path of the
|
|
49
|
-
*
|
|
50
|
-
* `list_available_packages` merges into its report. Omit when the
|
|
51
|
-
*
|
|
52
|
-
*
|
|
48
|
+
* Host path of the image inventory record, `image-packages.json` at the
|
|
49
|
+
* root of the package store — the image-owned tools and Node packages
|
|
50
|
+
* that `list_available_packages` merges into its report. Omit when the
|
|
51
|
+
* host's own process sees the store at the container mountpoint, which
|
|
52
|
+
* makes `/mnt/libs/image-packages.json` correct as-is; set it when the
|
|
53
|
+
* host reads the store somewhere else. An absent or invalid record
|
|
54
|
+
* merges nothing, and that is a normal state: a store packed before the
|
|
55
|
+
* record existed carries none, and the rest of the report stays whole.
|
|
53
56
|
*/
|
|
54
57
|
readonly imagePackagesFile?: string;
|
|
55
58
|
/**
|
|
@@ -81,7 +84,7 @@ export type PoolInventoryRead = {
|
|
|
81
84
|
/** One package entry of an inventory section, with the store identity where the source records it. */
|
|
82
85
|
export interface PoolInventoryPackage {
|
|
83
86
|
readonly name: string;
|
|
84
|
-
/** The pinned version, rendered as `name==version`. Absent
|
|
87
|
+
/** The pinned version, rendered as `name==version`. Absent where the source pins none. */
|
|
85
88
|
readonly version?: string;
|
|
86
89
|
/** The content-addressed store directory of the package. */
|
|
87
90
|
readonly storeDir?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -71,7 +71,7 @@ export type { AiSdkProviderConfig, ConfiguredAiSdkProviderDeps } from "./provide
|
|
|
71
71
|
export { createEmbeddingProvider } from "./providers/embedding.js";
|
|
72
72
|
export type { EmbeddingProviderDeps } from "./providers/embedding.js";
|
|
73
73
|
export type { AgentChat, ChatProvider, EmbeddingProvider, ChatRequest, ChatResponse, ChatStreamEvent, ChatUsage, ModelMessage, PromptCachePolicy, ReasoningPolicy, } from "./providers/types.js";
|
|
74
|
-
export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "./providers/prompt-cache.js";
|
|
74
|
+
export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions, withPromptCacheBreakpoint } from "./providers/prompt-cache.js";
|
|
75
75
|
export { DEFAULT_REASONING } from "./providers/reasoning.js";
|
|
76
76
|
export { toProviderError, isProviderError } from "./providers/errors.js";
|
|
77
77
|
export type { ProviderError } from "./providers/errors.js";
|
|
@@ -134,9 +134,12 @@ export type { ResolveWorkspaceRoot } from "./workspace/paths.js";
|
|
|
134
134
|
export { createSandboxClient } from "./sandbox/create-sandbox.js";
|
|
135
135
|
export type { CreateSandboxClientConfig, SandboxBackendConfig } from "./sandbox/create-sandbox.js";
|
|
136
136
|
export type { SandboxClient } from "./sandbox/client.js";
|
|
137
|
+
export { describeSandboxError, SandboxFailure } from "./sandbox/sandbox-error.js";
|
|
138
|
+
export type { SandboxError } from "./sandbox/sandbox-error.js";
|
|
137
139
|
export type { ExtendAnalysisFarm, FarmLocation, FarmResolution, FarmSource, PackageRequest, PackageRequestOutcome, ResolveAnalysisFarm, ToolchainSource, } from "./sandbox/types.js";
|
|
138
140
|
export { FARM_LOCK_FILE, FarmLockSchema, readFarmLock, readFarmLockFile } from "./sandbox/farm.js";
|
|
139
141
|
export type { FarmLock, FarmLockError } from "./sandbox/farm.js";
|
|
142
|
+
export { IMAGE_PACKAGES_FILE } from "./sandbox/image-packages.js";
|
|
140
143
|
export type { EnvironmentStorePaths, PoolInventoryPackage, PoolInventoryRead, PoolInventorySection } from "./config/environment-stores.js";
|
|
141
144
|
export { MachineBudgetSchema, ResourceLimitsSchema, ResourcePolicySchema, ResourceSpecSchema, parseResourcePolicy } from "./config/resource-limits.js";
|
|
142
145
|
export type { MachineBudget, ResourceLimits, ResourcePolicy, ResourceSpec } from "./config/resource-limits.js";
|
package/dist/index.js
CHANGED
|
@@ -110,8 +110,14 @@ export { createEmbeddingProvider } from "./providers/embedding.js";
|
|
|
110
110
|
// re-sends its prefix every iteration and breaks even immediately). Hosts whose
|
|
111
111
|
// endpoint ignores cache directives — notably the Claude Max OAuth path — pass
|
|
112
112
|
// `"off"`; the `cortex.harness.agent.cache_*_tokens` metrics show which case a
|
|
113
|
-
// deployment is actually in.
|
|
114
|
-
|
|
113
|
+
// deployment is actually in. `withPromptCacheBreakpoint` is the placement: it
|
|
114
|
+
// puts the ONE marker of a request on its last message, where every hop upstream
|
|
115
|
+
// can count it, in the namespace of each vendor that needs an explicit
|
|
116
|
+
// breakpoint (Anthropic and Bedrock; the OpenAI family and Gemini cache without
|
|
117
|
+
// one). Never attach `promptCacheProviderOptions` to a request itself — that
|
|
118
|
+
// emits a top-level field an intermediary cannot see, and a proxy that trims to
|
|
119
|
+
// Anthropic's cap of four breakpoints then sends five.
|
|
120
|
+
export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions, withPromptCacheBreakpoint } from "./providers/prompt-cache.js";
|
|
115
121
|
// Reasoning depth. `ReasoningPolicy` is the vendor-neutral name for how deep a
|
|
116
122
|
// model reasons; a composition root sets it per run through
|
|
117
123
|
// `RunAgentOptions.reasoning`. It defaults to `DEFAULT_REASONING` — `xhigh` —
|
|
@@ -237,9 +243,15 @@ export { createAnalysisPurge } from "./state/purge-analysis.js";
|
|
|
237
243
|
// the failure a fixed-width comparand exists to prevent.
|
|
238
244
|
export { computeInputSignature } from "./execution/input-signature.js";
|
|
239
245
|
export { createSandboxClient } from "./sandbox/create-sandbox.js";
|
|
246
|
+
// The throw of the client seam. An embedder matches the class and reads the
|
|
247
|
+
// typed variant on `error`, instead of a string match on the message.
|
|
248
|
+
export { describeSandboxError, SandboxFailure } from "./sandbox/sandbox-error.js";
|
|
240
249
|
// The farm contract: one `inflexa.lock` per farm. An embedder that composes
|
|
241
250
|
// farms writes this shape, and the mount gate reads it — one schema, no copy.
|
|
242
251
|
export { FARM_LOCK_FILE, FarmLockSchema, readFarmLock, readFarmLockFile } from "./sandbox/farm.js";
|
|
252
|
+
// The image record: one file name at the root of the store. An embedder joins
|
|
253
|
+
// it onto its own store root for `imagePackagesFile`.
|
|
254
|
+
export { IMAGE_PACKAGES_FILE } from "./sandbox/image-packages.js";
|
|
243
255
|
export { MachineBudgetSchema, ResourceLimitsSchema, ResourcePolicySchema, ResourceSpecSchema, parseResourcePolicy } from "./config/resource-limits.js";
|
|
244
256
|
export { createWorkspaceFilesystem } from "./workspace/filesystem.js";
|
|
245
257
|
// Exec-callback ingress building blocks. Delivery goes through
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -7,7 +7,7 @@ import { hintForZodIssue, repairToolInput } from "../lib/zod-issues.js";
|
|
|
7
7
|
import { markInterruptedMessage, syntheticUserMessage } from "../memory/ai-sdk-message-storage.js";
|
|
8
8
|
import { stripUnansweredToolCalls } from "../memory/tool-call-integrity.js";
|
|
9
9
|
import { classifyProviderError } from "../providers/errors.js";
|
|
10
|
-
import { DEFAULT_PROMPT_CACHE,
|
|
10
|
+
import { DEFAULT_PROMPT_CACHE, withPromptCacheBreakpoint } from "../providers/prompt-cache.js";
|
|
11
11
|
import { DEFAULT_REASONING } from "../providers/reasoning.js";
|
|
12
12
|
import { resultStep } from "./run-step.js";
|
|
13
13
|
import { AskRejectedError, UnavailableAsk } from "../tools/approval/contract.js";
|
|
@@ -82,10 +82,11 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
82
82
|
// `debug` records name the tools when that distinction is not enough.
|
|
83
83
|
let toolCallCount = 0;
|
|
84
84
|
let toolErrorCount = 0;
|
|
85
|
-
// Resolved once, not per iteration: an identical
|
|
86
|
-
//
|
|
87
|
-
// byte-identical to be read back.
|
|
88
|
-
|
|
85
|
+
// Resolved once, not per iteration: an identical policy across every call is
|
|
86
|
+
// itself part of the cache contract — the request prefix has to be
|
|
87
|
+
// byte-identical to be read back. The breakpoint it places is re-derived per
|
|
88
|
+
// call, because it rides the last message and the transcript grows.
|
|
89
|
+
const promptCache = opts.promptCache ?? DEFAULT_PROMPT_CACHE;
|
|
89
90
|
// The depth is a neutral name, and the provider package resolves it for the
|
|
90
91
|
// model. A vendor key here would turn that resolution off.
|
|
91
92
|
const reasoning = opts.reasoning ?? DEFAULT_REASONING;
|
|
@@ -232,10 +233,9 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
232
233
|
iterations = i + 1;
|
|
233
234
|
const request = {
|
|
234
235
|
system: agent.systemPrompt,
|
|
235
|
-
messages,
|
|
236
|
+
messages: withPromptCacheBreakpoint(messages, promptCache),
|
|
236
237
|
tools: toolDefs,
|
|
237
238
|
...(opts.toolChoice !== undefined ? { toolChoice: opts.toolChoice } : {}),
|
|
238
|
-
providerOptions,
|
|
239
239
|
reasoning,
|
|
240
240
|
};
|
|
241
241
|
const llmStepName = formatStepName.llm(i);
|
|
@@ -343,11 +343,11 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
343
343
|
// Cache defeater (known; not fixed here). Emptying the tool set changes the
|
|
344
344
|
// very front of the request prefix — tool definitions are cached ahead of
|
|
345
345
|
// system and history — so this call reads *nothing* back from the cache and
|
|
346
|
-
// rewrites the whole prefix from scratch. It still
|
|
346
|
+
// rewrites the whole prefix from scratch. It still places the breakpoint
|
|
347
347
|
// because it is the one call whose write is pure waste, and the
|
|
348
348
|
// cache_write_tokens counter is what makes that waste visible.
|
|
349
349
|
const wrapUpStepName = formatStepName.llm(agent.maxIterations);
|
|
350
|
-
const wrapUp = await resultStep(runStep)(wrapUpStepName, () => provider.chat({ system: agent.systemPrompt, messages, tools: {}, toolChoice: "none",
|
|
350
|
+
const wrapUp = await resultStep(runStep)(wrapUpStepName, () => provider.chat({ system: agent.systemPrompt, messages: withPromptCacheBreakpoint(messages, promptCache), tools: {}, toolChoice: "none", reasoning }, session, signal));
|
|
351
351
|
accountForCall(wrapUp, wrapUpStepName);
|
|
352
352
|
if (wrapUp.finishReason === "aborted") {
|
|
353
353
|
// An abort during the tool-less wrap-up is still the user cutting the turn — the
|
|
@@ -22,20 +22,20 @@
|
|
|
22
22
|
* `prompts/briefing.ts`), which is composed per dispatch and where per-step
|
|
23
23
|
* content belongs.
|
|
24
24
|
*
|
|
25
|
-
* The orient-core text keys on the
|
|
26
|
-
* (`sandboxOrientCorePromptFor`).
|
|
27
|
-
* untouched, thus an
|
|
28
|
-
* Both variants are composition-time constants, thus
|
|
29
|
-
* byte-stable across its own steps.
|
|
25
|
+
* The orient-core text keys on the `toolchainSource` of the sandbox client
|
|
26
|
+
* (`sandboxOrientCorePromptFor`). `"store"` gives the legacy constant
|
|
27
|
+
* untouched, thus an embedder that declares no toolchain keeps its cached
|
|
28
|
+
* prefix byte-identical. Both variants are composition-time constants, thus
|
|
29
|
+
* each composition stays byte-stable across its own steps.
|
|
30
30
|
*/
|
|
31
31
|
import type { ToolchainSource } from "../sandbox/types.js";
|
|
32
32
|
export declare const sandboxOrientCorePrompt = "# Sandbox Orient-Core\n\nThese apply to every agent that talks to a sandbox.\n\n## Your Briefing Is Authoritative\n\nYour first message is your briefing, composed for this step at the instant it was\ndispatched \u2014 after every step it depends on had finished. It names your task, your\nworking directory and the read-only analysis root, what the input dataset is\n(domain, organism, design, quality concerns, files), and what each upstream step\nproduced and where it put it. Work from it.\n\nDo NOT re-derive what it already gives you: no filesystem hunt for your inputs, no\nre-reading an upstream step whose summary you were handed, no re-deriving the\norganism or the dimensions from raw bytes. Reach further only where the briefing is\ngenuinely thin for what you must do:\n\n- `inspect_data_profile` \u2014 the full dataset profile: `scope:'overview'` for the\n identity and the file census, `scope:'groups'` for the groups and the dimensions\n that vary across them, `scope:'files'` for the members the profiler annotated\n individually. It is the ONLY record of what the input data IS; no file on disk\n carries it. Use it when the briefing's orientation is not enough, or when it\n carried none.\n- `read_file` on an upstream step's summary \u2014 its path is in your briefing \u2014 when\n the excerpt you were handed is not enough to build on.\n- `workspace_search({ query })` \u2014 natural-language search for a file the briefing\n does not name. Optional `type` filter (`input-group` | `input-dimension` |\n `input` | `output` | `summary` | `synthesis`) and `limit` (1-50, default 8).\n Returns paths, descriptions, and metadata \u2014 not file contents. Most input files\n carry no entry of their own; reach them through their group and `list_files`.\n\n## Your Workspace\n\nYour **working directory** \u2014 writable, and your cwd \u2014 and the read-only\n**analysis root** are named in your briefing. Use those paths exactly; do not\ninvent or guess one.\n\n- **Relative paths resolve against your working directory** \u2014 in `read_file`,\n `list_files`, `file_stat`, `grep`, `write_file`, `edit_file`,\n `execute_command`, and any script you run. `output/de.csv` means\n `<working directory>/output/de.csv` everywhere \u2014 a file you write at a\n relative path is read back at the same relative path.\n- **You may write only inside your working directory.** A write outside it\n comes back as an `out_of_prefix` result (no I/O performed); fix the path.\n- The rest of the analysis is mounted **read-only** at the analysis root. Reach\n input data (under `data/inputs/`) and other steps' outputs (under `runs/`)\n with an **absolute** path beneath it.\n- **Absolute analysis-root paths are canonical** \u2014 use one whenever you\n reference a file you didn't create in this step.\n\n`write_file` creates parent directories as needed \u2014 no separate mkdir.\n\n## Scope \u2014 Analysis Starts at Processed Data\n\nThis environment does **tertiary analysis**: turning processed data into\nbiological findings. Read processing \u2014 alignment, quantification, variant\ncalling, peak calling \u2014 belongs upstream, wherever the instrument output was\nhandled. No tool for it is installed, and none can be.\n\nYour inputs are count and expression matrices, called variants, peak sets,\nsignal tracks, and the metadata describing them. When a request arrives as raw\nreads or unprocessed instrument output, name the processed form you need and\nstop. Do not improvise a pipeline, do not substitute a Python reimplementation\nof an aligner or peak caller, and do not treat a missing upstream tool as an\nobstacle to route around \u2014 it is the boundary of the product, not a gap.\n\nData that has already been processed can still be **read**: `samtools`,\n`bcftools`, `tabix`, `bedtools` and `vcftools` are installed so you can\ninspect, subset, and convert existing BAM/CRAM, VCF and BED. Reading evidence at\nsites you already have is in scope; producing the calls is not.\n\n## Environment \u2014 No Network, No Installs\n\n**There is no egress from this sandbox.** Outbound connections are blocked at the\nfirewall, not merely discouraged: every download, API call, package install, and\n`ExperimentHub`/`AnnotationHub`/`models.download_*` style fetch fails outright.\nThere is no proxy, no mirror, and no retry that succeeds. Packages and reference\ndata were staged onto read-only mounts by the host **before** this container\nstarted, and that is the whole of what exists. Work that genuinely requires the\ninternet \u2014 fetching a dataset, querying an external API, literature lookup \u2014 is\ndone **outside** the sandbox through the host's tools, not from your scripts.\n\nSo: check what you need when you need it \u2014 a targeted lookup, never a catalog dump\nup front.\n\n- **Packages** \u2014 before importing a package you are not certain is present, look it\n up with `list_available_packages`, narrowed to the packages you actually intend\n to import. Importing one that isn't installed fails the script; the lookup costs\n one call.\n- **Reference data** \u2014 `list_available_refs` resolves reference data into a real\n path against the store mounted read-only at `/mnt/refs`. **Search by what the\n data IS, not by where you expect it** (\"TF-target regulons\", \"hallmark gene\n sets\", \"mouse\"): the store's directory layout is an installer detail, it differs\n between hosts, and no path you have seen before is guaranteed to exist here.\n Catalogued entries come back with the organism, the format, and the file's\n internal shape, so read the entry and use the reader and species it names rather\n than assuming \u2014 formats vary per dataset, and there is no guarantee any given\n collection ships in the format your tool prefers.\n The store is OPTIONAL and may be absent or hold none of what you want \u2014 a normal\n state, not an error. Never assume a reference path exists and never hardcode one:\n if a reference you need is not in the inventory, say so plainly and proceed with\n what you do have (or state what must be provisioned) rather than inventing a path\n or substituting something else unannounced. Pass reference paths EXPLICITLY to the\n library \u2014 nothing in the image points a library at the store, so a library that\n resolves data by name from an env var (CellTypist reads `$CELLTYPIST_FOLDER`)\n will not find it unless you export that variable yourself, in the same command,\n using a path the inventory actually returned.\n- Do NOT call a library's built-in data fetcher \u2014 `dc.op.collectri()`,\n `dc.op.progeny()`, `dc.op.msigdb()` and every other `dc.op.*()`, CellTypist's\n `models.download_models()`, gseapy's Enrichr library names, `ExperimentHub`/\n `AnnotationHub` \u2014 they all reach for the network and will fail. Load from an\n inventory path instead. Do NOT download data or install packages at runtime.\n\n## CPU Budget and Parallelism\n\nYour briefing names this step's CPU quota and memory limit. The container's\ncore-count calls (`parallel::detectCores()`, `os.cpu_count()`) report the\nquota, so worker counts sized from them are safe.\n\nBLAS/OpenMP thread pools default to **1 thread** (`OMP_NUM_THREADS`,\n`OPENBLAS_NUM_THREADS`, `MKL_NUM_THREADS`, \u2026). A forked worker inherits its\nparent's pool size, so this default is what keeps `mclapply` /\n`multiprocessing` from running workers \u00D7 threads on a quota sized for one of\nthem. The rule: **workers \u00D7 threads-per-worker must not exceed the quota.** A\nmix is fine while the product stays inside it.\n\n- **Fork/worker parallelism** (`mclapply`, `BiocParallel`, `joblib`,\n `multiprocessing`): size the workers from the reported core count and leave\n the thread defaults alone \u2014 there is nothing to raise. `joblib` also\n divides its own thread budget across its workers; do not manage it.\n- **Thread parallelism** (one process: xgboost, WGCNA, data.table, polars, a\n single large BLAS fit): raise the thread variables for that command through\n `execute_command`'s `env`, e.g.\n `env: { \"OMP_NUM_THREADS\": \"4\", \"OPENBLAS_NUM_THREADS\": \"4\" }` on a 4-CPU\n quota.\n\nExceeding the quota does not kill the step \u2014 the cgroup throttles it \u2014 but an\noversubscribed step runs slower and can starve the exec server that runs your\ncommands.\n\n## Output Contract \u2014 Persisted Files Are the Deliverable\n\nThis is a hard requirement, not a convention. **Your deliverable is persisted\nfiles**, nothing else:\n\n- the **script** you wrote, in `scripts/`,\n- the **data it computed** (derived from the input data), in `output/`,\n- any **figures**, in `figures/`.\n\nConclusions are drawn from those computed output files \u2014 never narrated from\n`execute_command` stdout. stdout/stderr are ephemeral; they are gone the\nmoment the command returns. **A step that ends without persisted\nscripts + outputs has produced nothing**, even if its transcript reads as if\nwork was done. In particular: running the analysis as an inline `python -c` /\n`Rscript -e` one-liner and reporting the numbers it printed produces nothing \u2014\nwrite the script to `scripts/` and persist what it computes to `output/`.\n\nA genuine verdict / QC / decision step that has no tabular result may instead\nwrite a short Markdown memo to `output/` (e.g. `output/qc-verdict.md`) stating\nthe finding and the evidence it rests on \u2014 a real persisted artifact. Do not\nfabricate a CSV just to have one.\n\nIf you genuinely cannot fulfill the step \u2014 required input data is missing, a\ntool you need is unavailable, or the environment is broken \u2014 call\n`report_blocker({ reason })` with a clear, specific reason. Do NOT improvise an\ninline result, fabricate outputs, or end on a prose narrative pretending the\nwork was done. `report_blocker` is the honest exit; an empty step that claims\nsuccess is not.\n\n## Skills \u2014 Method Selection and API Details\n\nSkills hold the full decision trees, API references, contrast syntax, worked\nexamples, and domain anti-patterns that do not fit in prompts. Use them\nactively \u2014 they are your authoritative source on methods.\n\n- `skill_search(query)` \u2014 keyword search across the skills available to you.\n Start here when picking a method or verifying an API detail. Examples:\n `skill_search(\"PyDESeq2 contrast syntax\")`,\n `skill_search(\"Leiden clustering resolution\")`.\n- `skill_read(skill, path)` \u2014 read a file from one of your skills, e.g.\n `skill_read(\"bulk-transcriptomics\", \"SKILL.md\")` or\n `skill_read(\"bulk-transcriptomics\", \"references/pydeseq2-api.md\")`.\n\nThe skills you have access to are listed in your agent instructions.\n\n## Context7 \u2014 Documentation Lookup\n\nLook up current documentation via context7 before writing non-trivial code:\n1. `resolve_library_id` with the package name to get the library ID.\n2. `query_docs` with the library ID and the specific function or method.\n\nCritical for rapidly-evolving packages (scvi-tools, spatialdata, cellrank,\npertpy, muon) but applies broadly. Do NOT guess API details from memory \u2014\nverify with context7 or skill references first.\n\n## Command Execution\n\n`execute_command` runs analysis work \u2014 scripts, bioinformatics CLI tools,\nshell pipes, and anything the workspace tools don't express. It starts in your\nworking directory; a relative `cwd` argument is resolved against it.\n\nFor these tasks, prefer the dedicated workspace tools \u2014 they are faster than\nshelling out and don't waste a turn on path discovery:\n\n| Task | Use this tool | Instead of |\n|-|-|-|\n| List files in a directory | `list_files` | `ls` / `ls -la` / `find -name` |\n| Read a text/source/result file | `read_file` | `cat` / `less` |\n| Search file contents by pattern | `grep` | shell `grep` / `rg` |\n| Size / type of a path | `file_stat` | `stat` / `wc -c` |\n\n**Use `execute_command` for everything else**, including:\n\n- **Running scripts** \u2014 `python scripts/run.py`, `Rscript scripts/de.R`,\n `ruff check scripts/foo.py`.\n- **Previewing huge / binary files** \u2014 `wc -l file.tsv`, `zcat file.gz | head`.\n For text files, prefer `read_file` with `headLines` / `tailLines`.\n- **Shell pipes / chaining** \u2014 `sort | uniq -c | sort -rn | head`.\n- **`find` with non-name predicates** \u2014 `find . -mtime -1`, `find . -size +10M`.\n- **Real CLI tools** \u2014 `samtools`, `bcftools`, `tabix`, `bedtools`,\n `vcftools`, version probes like `python -c \"import x; print(x.__version__)\"`.\n\n**Shell behavior to know:**\n\n- `cd` does NOT persist across calls \u2014 each exec starts in your working\n directory. For cross-call state, pass `cwd` or chain with `&&` in one exec.\n- Quote paths with spaces: `head \"data/inputs/My Folder/file.csv\"`. In Python,\n use `pathlib.Path` or `os.path`.\n\n## Editing Files\n\n`edit_file` replaces `old_string` with `new_string`. Read the file first to\nget the exact text. When `replace_all` is false (default), `old_string` must\noccur exactly once \u2014 include surrounding context to make it unique.\n";
|
|
33
33
|
/**
|
|
34
34
|
* The orient-core layer for one declared toolchain. `"image"` states that an
|
|
35
35
|
* acquisition is a host action and directs the agent to report a missing
|
|
36
|
-
* package.
|
|
36
|
+
* package. `"store"` gives the legacy constant untouched.
|
|
37
37
|
*/
|
|
38
|
-
export declare function sandboxOrientCorePromptFor(toolchainSource: ToolchainSource
|
|
38
|
+
export declare function sandboxOrientCorePromptFor(toolchainSource: ToolchainSource): string;
|
|
39
39
|
/**
|
|
40
40
|
* The package-link layer — appended to the sandbox system prompt only when
|
|
41
41
|
* the embedder binds the `ExtendAnalysisFarm` seam. A composition-time
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
* `prompts/briefing.ts`), which is composed per dispatch and where per-step
|
|
23
23
|
* content belongs.
|
|
24
24
|
*
|
|
25
|
-
* The orient-core text keys on the
|
|
26
|
-
* (`sandboxOrientCorePromptFor`).
|
|
27
|
-
* untouched, thus an
|
|
28
|
-
* Both variants are composition-time constants, thus
|
|
29
|
-
* byte-stable across its own steps.
|
|
25
|
+
* The orient-core text keys on the `toolchainSource` of the sandbox client
|
|
26
|
+
* (`sandboxOrientCorePromptFor`). `"store"` gives the legacy constant
|
|
27
|
+
* untouched, thus an embedder that declares no toolchain keeps its cached
|
|
28
|
+
* prefix byte-identical. Both variants are composition-time constants, thus
|
|
29
|
+
* each composition stays byte-stable across its own steps.
|
|
30
30
|
*/
|
|
31
31
|
export const sandboxOrientCorePrompt = `# Sandbox Orient-Core
|
|
32
32
|
|
|
@@ -291,7 +291,7 @@ sandbox.`), `- **Packages** — before importing a package you are not certain i
|
|
|
291
291
|
/**
|
|
292
292
|
* The orient-core layer for one declared toolchain. `"image"` states that an
|
|
293
293
|
* acquisition is a host action and directs the agent to report a missing
|
|
294
|
-
* package.
|
|
294
|
+
* package. `"store"` gives the legacy constant untouched.
|
|
295
295
|
*/
|
|
296
296
|
export function sandboxOrientCorePromptFor(toolchainSource) {
|
|
297
297
|
return toolchainSource === "image" ? imageToolchainOrientCorePrompt : sandboxOrientCorePrompt;
|
|
@@ -15,12 +15,53 @@
|
|
|
15
15
|
* error — on an OpenAI-compatible model, which is exactly what we want: that
|
|
16
16
|
* family does automatic server-side prefix caching and needs no directive.
|
|
17
17
|
*
|
|
18
|
-
* ## What the Anthropic namespace does
|
|
18
|
+
* ## What the Anthropic namespace does, and where the marker must go
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* The options go on the LAST MESSAGE of the request, never on the request
|
|
21
|
+
* itself. Both placements cache the same bytes — one breakpoint at the end of
|
|
22
|
+
* the messages covers the whole prefix (tools → system → history), because the
|
|
23
|
+
* cache keys on a prefix — but only the message placement survives an
|
|
24
|
+
* intermediary.
|
|
25
|
+
*
|
|
26
|
+
* A REQUEST-level `cacheControl` makes the provider emit a top-level
|
|
27
|
+
* `cache_control` field instead of a per-block marker, and the server then
|
|
28
|
+
* places the breakpoint itself. That is one fewer thing for the harness to get
|
|
29
|
+
* right, and it is why this module used to do it. It is also invisible: an
|
|
30
|
+
* intermediary that counts breakpoints to stay under Anthropic's cap of four
|
|
31
|
+
* counts BLOCKS, so a top-level field is a breakpoint nothing on the way
|
|
32
|
+
* upstream can see. CLIProxyAPI — what the OSS CLI routes through on the Claude
|
|
33
|
+
* OAuth path — injects up to four block markers of its own and trims to four by
|
|
34
|
+
* that blind count, so the harness's invisible fifth breakpoint made every
|
|
35
|
+
* request past a thread's first turn fail with `A maximum of 4 blocks with
|
|
36
|
+
* cache_control may be provided. Found 5.` (HTTP 400, non-retryable — the thread
|
|
37
|
+
* is wedged, because the next turn rebuilds the same shape).
|
|
38
|
+
*
|
|
39
|
+
* A marker on a message block is counted, trimmed, and reasoned about by every
|
|
40
|
+
* hop. Keep it there. {@link withPromptCacheBreakpoint} is the only writer.
|
|
41
|
+
*
|
|
42
|
+
* ## Which vendors need a marker at all
|
|
43
|
+
*
|
|
44
|
+
* Only two of them, and both take it on a message:
|
|
45
|
+
*
|
|
46
|
+
* - **Anthropic** — `anthropic.cacheControl`. Fully opt-in: no marker, no
|
|
47
|
+
* caching, at full price.
|
|
48
|
+
* - **Amazon Bedrock** — `bedrock.cachePoint`. Also fully opt-in.
|
|
49
|
+
*
|
|
50
|
+
* The OpenAI family caches prefixes server-side, unprompted, and exposes no
|
|
51
|
+
* breakpoint to place (`promptCacheKey` and `promptCacheRetention` are request
|
|
52
|
+
* settings for affinity and retention, not placement — a separate concern from
|
|
53
|
+
* this policy). Gemini caches implicitly by default, and its explicit mode is a
|
|
54
|
+
* cache RESOURCE created out of band and referenced by name, which is not a
|
|
55
|
+
* marker and does not fit a ttl policy.
|
|
56
|
+
*
|
|
57
|
+
* So both markers ride together on the one chosen message. A provider reads only
|
|
58
|
+
* its own namespace, thus the pair is free to whichever vendor is not serving
|
|
59
|
+
* the call, and the placement never learns which one that is.
|
|
60
|
+
*
|
|
61
|
+
* The asymmetry worth remembering: under-marking silently costs money, while
|
|
62
|
+
* over-marking hard-fails. Anthropic answers 400 past four breakpoints, and the
|
|
63
|
+
* legacy Bedrock integration answers 400 on a top-level `cache_control`. Every
|
|
64
|
+
* failure in this area comes from asking for too much.
|
|
24
65
|
*
|
|
25
66
|
* ## Cache defeaters — what silently kills the hit rate
|
|
26
67
|
*
|
|
@@ -43,15 +84,29 @@
|
|
|
43
84
|
* `prompts/briefing.ts`). Keep it that way — one interpolated id or path there
|
|
44
85
|
* makes every step's ~20k-char prefix unique.
|
|
45
86
|
*
|
|
46
|
-
* ##
|
|
87
|
+
* ## What the cache-token metrics do and do not prove
|
|
88
|
+
*
|
|
89
|
+
* Caching DOES engage on the Claude OAuth path the OSS CLI routes through a
|
|
90
|
+
* local CLIProxyAPI: measured, 2050-2151 cache-read tokens on the turns after
|
|
91
|
+
* the first. An earlier note here claimed that path ignores cache directives
|
|
92
|
+
* outright. It does not.
|
|
47
93
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* `
|
|
94
|
+
* But a read there is not evidence that OUR breakpoint earned it. While cloaking
|
|
95
|
+
* — which is what that path does — the proxy places breakpoints of its own on
|
|
96
|
+
* the system block, the first user message, the system message it relocates, and
|
|
97
|
+
* the last message. Those cache the prefix whatever the harness sends: with
|
|
98
|
+
* `promptCache: "off"` and no marker of ours on the wire, a repeat request still
|
|
99
|
+
* read 1902 tokens back.
|
|
100
|
+
*
|
|
101
|
+
* So on that path `cache_read_tokens` (`loop/metrics.ts`) reports whether the
|
|
102
|
+
* deployment gets caching at all, which is what the counter is for. It does not
|
|
103
|
+
* attribute the read, and it cannot tell a working harness breakpoint from a
|
|
104
|
+
* proxy that is quietly doing the work. Against a direct API key or a gateway,
|
|
105
|
+
* where nothing injects markers, the counter does mean exactly that — and a
|
|
106
|
+
* flat-zero read counter beside a non-zero write counter is still the signature
|
|
107
|
+
* of an endpoint billing for writes and serving no reads.
|
|
53
108
|
*/
|
|
54
|
-
import type { PromptCachePolicy, ProviderOptions } from "./types.js";
|
|
109
|
+
import type { ModelMessage, PromptCachePolicy, ProviderOptions } from "./types.js";
|
|
55
110
|
/**
|
|
56
111
|
* The default policy: cache with the 5-minute TTL.
|
|
57
112
|
*
|
|
@@ -68,7 +123,47 @@ export declare const DEFAULT_PROMPT_CACHE: PromptCachePolicy;
|
|
|
68
123
|
/**
|
|
69
124
|
* Translate a neutral cache policy into provider wire options.
|
|
70
125
|
*
|
|
71
|
-
* Returns `undefined` for `"off"` so the caller can leave
|
|
72
|
-
*
|
|
126
|
+
* Returns `undefined` for `"off"` so the caller can leave the bag entirely unset
|
|
127
|
+
* rather than sending an empty one.
|
|
128
|
+
*
|
|
129
|
+
* Both vendors that take an explicit marker get one, in their own namespace. A
|
|
130
|
+
* provider reads only its own key, so the pair costs nothing to whichever one is
|
|
131
|
+
* not serving the call, and the placement needs no idea which vendor it is
|
|
132
|
+
* talking to. The two shapes differ in more than spelling: Anthropic marks the
|
|
133
|
+
* last content block of the message, while Bedrock appends a `cachePoint` block
|
|
134
|
+
* after it. Both end up at the same position in the prefix.
|
|
135
|
+
*
|
|
136
|
+
* The `satisfies` clauses are the point of naming the vendor types at all. This
|
|
137
|
+
* bag is a `Record<string, JSONObject>`, thus nothing here is checked by
|
|
138
|
+
* default, and a renamed key would compile and silently stop caching —
|
|
139
|
+
* `cacheControl` has already been renamed once, and the provider still accepts
|
|
140
|
+
* `cache_control` as its alias.
|
|
141
|
+
*
|
|
142
|
+
* CAUTION: this is the marker's VALUE, not its placement. Attaching it to a
|
|
143
|
+
* `ChatRequest` emits the invisible top-level field the module header warns
|
|
144
|
+
* about. Place it with {@link withPromptCacheBreakpoint}.
|
|
73
145
|
*/
|
|
74
146
|
export declare function promptCacheProviderOptions(policy: PromptCachePolicy): ProviderOptions | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* Place the request's ONE cache breakpoint, on the last message that can host it.
|
|
149
|
+
*
|
|
150
|
+
* Returns a copy — the caller's array is the transcript the loop keeps pushing
|
|
151
|
+
* onto and the host later persists, and a marker written into it would ride into
|
|
152
|
+
* the stored thread and come back on every later turn, one more breakpoint per
|
|
153
|
+
* turn until the request is refused. Only the request-shaped copy carries one.
|
|
154
|
+
*
|
|
155
|
+
* The placement is deliberately ROLLING, not pinned to the end of the stable
|
|
156
|
+
* prefix: within one run the loop appends an assistant reply and its tool results
|
|
157
|
+
* on every iteration, and a breakpoint that advances with them lets iteration N+1
|
|
158
|
+
* read back everything iteration N wrote. A pinned breakpoint would re-process
|
|
159
|
+
* the whole tool transcript uncached on every iteration, which is the opposite of
|
|
160
|
+
* what an agent loop needs.
|
|
161
|
+
*
|
|
162
|
+
* A marker already on any other message is REMOVED — the invariant is exactly one
|
|
163
|
+
* breakpoint per request, and a stored message that carries one from an older
|
|
164
|
+
* build (`memory/ai-sdk-message-storage.ts` reads `cache_control` back off a
|
|
165
|
+
* stored block) would otherwise spend a slot that the harness never budgeted.
|
|
166
|
+
* `"off"` strips and places nothing, so turning caching off really does send no
|
|
167
|
+
* directive at all.
|
|
168
|
+
*/
|
|
169
|
+
export declare function withPromptCacheBreakpoint<M extends ModelMessage>(messages: readonly M[], policy: PromptCachePolicy): readonly M[];
|