@inflexa-ai/harness 0.40.0 → 0.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/chat-turn.d.ts +42 -13
- package/dist/app/chat-turn.js +47 -16
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/prompts/briefing.d.ts +28 -1
- package/dist/prompts/briefing.js +44 -1
- package/dist/prompts/compaction.d.ts +2 -2
- package/dist/prompts/compaction.js +9 -5
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +4 -0
- package/dist/runtime/assemble.d.ts +4 -2
- package/dist/runtime/assemble.js +8 -1
- package/dist/schemas/file-metadata.js +9 -9
- package/dist/schemas/plan-schemas.d.ts +3 -3
- package/dist/tools/ad-hoc-router.js +9 -4
- package/dist/tools/bio/chembl.js +8 -5
- package/dist/tools/bio/comptox.js +4 -4
- package/dist/tools/bio/drug-gene-interactions.js +5 -4
- package/dist/tools/bio/gene-disease-evidence.js +4 -4
- package/dist/tools/bio/lookup-annotation.js +7 -3
- package/dist/tools/bio/opentargets.js +8 -6
- package/dist/tools/bio/pubchem.js +4 -2
- package/dist/tools/bio/pubmed.d.ts +1 -1
- package/dist/tools/bio/pubmed.js +10 -6
- package/dist/tools/bio/search-clinical-trials.js +2 -1
- package/dist/tools/bio/search-gene.js +2 -1
- package/dist/tools/bio/search-interactions.js +8 -4
- package/dist/tools/bio/target-safety.js +2 -1
- package/dist/tools/display/show-user.js +1 -1
- package/dist/tools/execute-analysis.js +14 -4
- package/dist/tools/lib/opentargets-client.d.ts +8 -3
- package/dist/tools/lib/opentargets-client.js +12 -3
- package/dist/tools/read-tool-output.js +7 -4
- package/dist/tools/report-authoring/authoring-tools.js +22 -15
- package/dist/tools/report-session/derive-table.js +5 -1
- package/dist/tools/report-session/examine-page.js +5 -4
- package/dist/tools/report-session/preview-report.js +6 -1
- package/dist/tools/report-session/record-version.js +2 -1
- package/dist/tools/research/context7-docs.js +8 -2
- package/dist/tools/research/generate-analogy-report.js +15 -15
- package/dist/tools/research/generate-plan.js +12 -7
- package/dist/tools/research/inspect-data-profile.js +7 -5
- package/dist/tools/research/inspect-run.js +6 -2
- package/dist/tools/research/literature-reviewer.js +11 -6
- package/dist/tools/research/search-arxiv.js +2 -1
- package/dist/tools/research/search-github-repos.js +9 -3
- package/dist/tools/sandbox/list-available-refs.js +3 -2
- package/dist/tools/sandbox/report-blocker.js +6 -1
- package/dist/tools/sandbox/skills.js +4 -2
- package/dist/tools/start-report-session.js +4 -1
- package/dist/tools/workspace/edit-file.js +4 -3
- package/dist/tools/workspace/execute-command.js +6 -5
- package/dist/tools/workspace/grep.js +9 -7
- package/dist/tools/workspace/list-files.js +1 -1
- package/dist/tools/workspace/read-file.js +2 -1
- package/dist/tools/workspace/scan-inputs.js +3 -1
- package/dist/tools/workspace/show-file.js +2 -0
- package/dist/tools/workspace/workspace-search.js +3 -1
- package/dist/tools/workspace/write-file.js +5 -3
- package/dist/workflows/execute-analysis.d.ts +28 -5
- package/dist/workflows/execute-analysis.js +34 -10
- package/package.json +1 -1
package/dist/app/chat-turn.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The chat turn of a host: a turn is `runChatTurn`,
|
|
3
|
-
*
|
|
2
|
+
* The chat turn of a host: a turn is `runChatTurn`, which is `openChatTurn` and then the `run` of the
|
|
3
|
+
* open turn. `prepareChatTurn` is the first step of the open. The host gives only its transport
|
|
4
|
+
* values, and the harness stores the opening, each round, and the outcome.
|
|
4
5
|
*/
|
|
5
6
|
import type { Pool } from "pg";
|
|
6
|
-
import type {
|
|
7
|
+
import type { RequestSession } from "../auth/types.js";
|
|
7
8
|
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
8
9
|
import type { TokenUsageRollup } from "../contracts/usage.js";
|
|
9
10
|
import type { DbError } from "../lib/db-result.js";
|
|
10
11
|
import { type AgentFinish } from "../loop/run-agent.js";
|
|
11
|
-
import type { EmitFn } from "../loop/types.js";
|
|
12
|
+
import type { AgentDefinition, EmitFn } from "../loop/types.js";
|
|
12
13
|
import { type ThreadType } from "../memory/thread-store.js";
|
|
13
14
|
import type { AgentChat, PromptCachePolicy } from "../providers/types.js";
|
|
14
15
|
import type { ThreadAgentResolver } from "../runtime/assemble.js";
|
|
@@ -54,12 +55,20 @@ export interface RunChatTurnDeps extends PrepareChatTurnDeps {
|
|
|
54
55
|
/** The agent of a turn comes from the thread type, which only the preparation knows. */
|
|
55
56
|
readonly agents: ThreadAgentResolver;
|
|
56
57
|
}
|
|
57
|
-
/**
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The session of a turn without its provenance. The harness sets the provenance from the agent that
|
|
60
|
+
* it resolves, because only the preparation knows the thread type.
|
|
61
|
+
*/
|
|
62
|
+
export type ChatTurnSession = Omit<RequestSession, "provenance">;
|
|
63
|
+
/** What opens a turn: the thread and the user input. */
|
|
64
|
+
export interface OpenChatTurnParams {
|
|
59
65
|
readonly analysisId: string;
|
|
60
66
|
readonly threadId: string;
|
|
61
67
|
readonly userInput: string;
|
|
62
|
-
|
|
68
|
+
}
|
|
69
|
+
/** The transport values of a turn that is open. */
|
|
70
|
+
export interface RunOpenChatTurnParams {
|
|
71
|
+
readonly session: ChatTurnSession;
|
|
63
72
|
/** Makes the provider of the turn over the emit sink of the display recorder. */
|
|
64
73
|
readonly chat: (emit: EmitFn) => AgentChat;
|
|
65
74
|
readonly emit: EmitFn;
|
|
@@ -69,13 +78,15 @@ export interface RunChatTurnParams {
|
|
|
69
78
|
readonly ask?: (request: AskRequest, emit: EmitFn) => Promise<AskApproval>;
|
|
70
79
|
/** Who sent the user message. */
|
|
71
80
|
readonly author?: string;
|
|
72
|
-
/** When the host opened the turn. Absent counts from the call. */
|
|
81
|
+
/** When the host opened the turn. Absent counts from the call to {@link openChatTurn}. */
|
|
73
82
|
readonly startedAtMs?: number;
|
|
74
83
|
/** The cache policy of the root loop. Absent gives {@link CONVERSATION_PROMPT_CACHE}. */
|
|
75
84
|
readonly promptCache?: PromptCachePolicy;
|
|
76
85
|
/** The budget of the view of the root loop. Absent gives {@link DEFAULT_CONVERSATION_BUDGET}. */
|
|
77
86
|
readonly conversationBudget?: number;
|
|
78
87
|
}
|
|
88
|
+
/** The values of one whole turn. */
|
|
89
|
+
export type RunChatTurnParams = OpenChatTurnParams & RunOpenChatTurnParams;
|
|
79
90
|
export type ChatTurnOutcome = {
|
|
80
91
|
readonly status: "done";
|
|
81
92
|
readonly finish: AgentFinish;
|
|
@@ -87,7 +98,8 @@ export type ChatTurnOutcome = {
|
|
|
87
98
|
readonly reason: string;
|
|
88
99
|
readonly cause: unknown;
|
|
89
100
|
};
|
|
90
|
-
|
|
101
|
+
/** A turn that ended before its run. It wrote no row and no turn record. */
|
|
102
|
+
export type ChatTurnRefusal = {
|
|
91
103
|
readonly kind: "prepare_failed";
|
|
92
104
|
readonly cause: unknown;
|
|
93
105
|
} | {
|
|
@@ -95,7 +107,8 @@ export type ChatTurnResult = {
|
|
|
95
107
|
} | {
|
|
96
108
|
readonly kind: "agent_unresolved";
|
|
97
109
|
readonly threadType: ThreadType;
|
|
98
|
-
}
|
|
110
|
+
};
|
|
111
|
+
export interface ChatTurnRan {
|
|
99
112
|
readonly kind: "ran";
|
|
100
113
|
readonly outcome: ChatTurnOutcome;
|
|
101
114
|
/** True when the opening of the turn landed. */
|
|
@@ -106,9 +119,25 @@ export type ChatTurnResult = {
|
|
|
106
119
|
readonly turnUsage?: TokenUsageRollup;
|
|
107
120
|
/** The final text of a run that returned. */
|
|
108
121
|
readonly fallbackText?: string;
|
|
109
|
-
}
|
|
122
|
+
}
|
|
123
|
+
export type ChatTurnResult = ChatTurnRefusal | ChatTurnRan;
|
|
124
|
+
/**
|
|
125
|
+
* A turn that is prepared, with its agent resolved. Call `run` one time: each call stores a new
|
|
126
|
+
* opening of the same user message.
|
|
127
|
+
*/
|
|
128
|
+
export interface OpenChatTurn {
|
|
129
|
+
readonly kind: "ready";
|
|
130
|
+
/** The agent that runs the turn. The harness stamps its provenance on the session of the run. */
|
|
131
|
+
readonly agent: AgentDefinition;
|
|
132
|
+
readonly run: (params: RunOpenChatTurnParams) => Promise<ChatTurnRan>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Open one chat turn: prepare it and resolve its agent. A refusal comes back before any row of the
|
|
136
|
+
* turn exists, thus a host can answer it before it starts a stream.
|
|
137
|
+
*/
|
|
138
|
+
export declare function openChatTurn(deps: RunChatTurnDeps, params: OpenChatTurnParams): Promise<OpenChatTurn | ChatTurnRefusal>;
|
|
110
139
|
/**
|
|
111
|
-
* Run one chat turn:
|
|
112
|
-
*
|
|
140
|
+
* Run one chat turn: open it, store its opening, run the root loop with a round sink that stores
|
|
141
|
+
* each round, and close the turn with its outcome.
|
|
113
142
|
*/
|
|
114
143
|
export declare function runChatTurn(deps: RunChatTurnDeps, params: RunChatTurnParams): Promise<ChatTurnResult>;
|
package/dist/app/chat-turn.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The chat turn of a host: a turn is `runChatTurn`,
|
|
3
|
-
*
|
|
2
|
+
* The chat turn of a host: a turn is `runChatTurn`, which is `openChatTurn` and then the `run` of the
|
|
3
|
+
* open turn. `prepareChatTurn` is the first step of the open. The host gives only its transport
|
|
4
|
+
* values, and the harness stores the opening, each round, and the outcome.
|
|
4
5
|
*/
|
|
5
6
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
6
7
|
import { finalText, runAgent } from "../loop/run-agent.js";
|
|
@@ -100,15 +101,14 @@ async function readTurnContext(pool, analysisId) {
|
|
|
100
101
|
/** The root loop of a chat turn compacts its view past this estimate, in tokens. */
|
|
101
102
|
export const DEFAULT_CONVERSATION_BUDGET = 150_000;
|
|
102
103
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
104
|
+
* Open one chat turn: prepare it and resolve its agent. A refusal comes back before any row of the
|
|
105
|
+
* turn exists, thus a host can answer it before it starts a stream.
|
|
105
106
|
*/
|
|
106
|
-
export async function
|
|
107
|
-
const
|
|
108
|
-
const startedAtMs = params.startedAtMs ?? Date.now();
|
|
107
|
+
export async function openChatTurn(deps, params) {
|
|
108
|
+
const openedAtMs = Date.now();
|
|
109
109
|
let prepared;
|
|
110
110
|
try {
|
|
111
|
-
prepared = await prepareChatTurn(deps,
|
|
111
|
+
prepared = await prepareChatTurn(deps, params);
|
|
112
112
|
}
|
|
113
113
|
catch (cause) {
|
|
114
114
|
return { kind: "prepare_failed", cause };
|
|
@@ -118,8 +118,33 @@ export async function runChatTurn(deps, params) {
|
|
|
118
118
|
const agent = deps.agents.forThread(prepared.threadType);
|
|
119
119
|
if (agent.isErr())
|
|
120
120
|
return { kind: "agent_unresolved", threadType: prepared.threadType };
|
|
121
|
+
const ready = prepared;
|
|
122
|
+
return {
|
|
123
|
+
kind: "ready",
|
|
124
|
+
agent: agent.value,
|
|
125
|
+
run: (run) => runOpenTurn(deps, params, ready, agent.value, { ...run, startedAtMs: run.startedAtMs ?? openedAtMs }),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Run one chat turn: open it, store its opening, run the root loop with a round sink that stores
|
|
130
|
+
* each round, and close the turn with its outcome.
|
|
131
|
+
*/
|
|
132
|
+
export async function runChatTurn(deps, params) {
|
|
133
|
+
const { analysisId, threadId, userInput, ...run } = params;
|
|
134
|
+
const opened = await openChatTurn(deps, { analysisId, threadId, userInput });
|
|
135
|
+
if (opened.kind !== "ready")
|
|
136
|
+
return opened;
|
|
137
|
+
return opened.run(run);
|
|
138
|
+
}
|
|
139
|
+
async function runOpenTurn(deps, turn, prepared, agent, params) {
|
|
140
|
+
const logger = (deps.logger ?? createNoopLogger()).named("harness.chat");
|
|
141
|
+
const session = { ...params.session, provenance: { agentId: agent.id, callPath: [agent.id] } };
|
|
121
142
|
const history = createThreadHistory(deps.pool, deps.logger);
|
|
122
|
-
const recorder = createConversationDisplayRecorder({
|
|
143
|
+
const recorder = createConversationDisplayRecorder({
|
|
144
|
+
userText: userTextOf(prepared.userMessage),
|
|
145
|
+
topLevelCallPath: session.provenance.callPath,
|
|
146
|
+
sink: params.emit,
|
|
147
|
+
});
|
|
123
148
|
// The groups that did not land yet, in the order of the turn. A failed write keeps them for the next write.
|
|
124
149
|
let pending = [
|
|
125
150
|
{
|
|
@@ -136,13 +161,13 @@ export async function runChatTurn(deps, params) {
|
|
|
136
161
|
return;
|
|
137
162
|
const closing = close === undefined ? {} : { close };
|
|
138
163
|
const write = startSeq === undefined ? { opening: pending[0], rounds: pending.slice(1), ...closing } : { startSeq, rounds: pending, ...closing };
|
|
139
|
-
(await history.writeTurn(
|
|
164
|
+
(await history.writeTurn(turn.threadId, write)).match((written) => {
|
|
140
165
|
pending = [];
|
|
141
166
|
startSeq = written.startSeq;
|
|
142
167
|
storeError = undefined;
|
|
143
168
|
}, (error) => {
|
|
144
169
|
storeError = error;
|
|
145
|
-
logger.warn("chat turn write failed", { threadId:
|
|
170
|
+
logger.warn("chat turn write failed", { threadId: turn.threadId, op: error.op, ...logger.errorFields(error.cause) });
|
|
146
171
|
});
|
|
147
172
|
};
|
|
148
173
|
await flush();
|
|
@@ -150,7 +175,7 @@ export async function runChatTurn(deps, params) {
|
|
|
150
175
|
let outcome;
|
|
151
176
|
let fallbackText;
|
|
152
177
|
try {
|
|
153
|
-
const run = await runAgent(agent
|
|
178
|
+
const run = await runAgent(agent, prepared.messages, session, {
|
|
154
179
|
provider: params.chat(recorder.emit),
|
|
155
180
|
signal: params.signal,
|
|
156
181
|
emit: recorder.emit,
|
|
@@ -158,7 +183,7 @@ export async function runChatTurn(deps, params) {
|
|
|
158
183
|
usageRecorder: params.usageRecorder,
|
|
159
184
|
toolOutputStore: createToolOutputStore(deps.pool),
|
|
160
185
|
promptCache: params.promptCache ?? CONVERSATION_PROMPT_CACHE,
|
|
161
|
-
compaction: compactionPolicy(deps, params, prepared.threadType, agent
|
|
186
|
+
compaction: compactionPolicy(deps, turn.analysisId, params, prepared.threadType, agent, recorder.emit),
|
|
162
187
|
...(deps.logger === undefined ? {} : { logger: deps.logger }),
|
|
163
188
|
...(ask === undefined ? {} : { ask: (request) => ask(request, recorder.emit) }),
|
|
164
189
|
onRound: async (round) => {
|
|
@@ -175,7 +200,7 @@ export async function runChatTurn(deps, params) {
|
|
|
175
200
|
const aborted = params.signal.aborted && cause instanceof Error && cause.name === "AbortError";
|
|
176
201
|
outcome = aborted ? { status: "aborted" } : { status: "failed", reason: failureReasonOf(cause), cause };
|
|
177
202
|
}
|
|
178
|
-
const durationMs = Date.now() - startedAtMs;
|
|
203
|
+
const durationMs = Date.now() - params.startedAtMs;
|
|
179
204
|
const turnUsage = outcome.status === "failed" ? undefined : outcome.finish?.turnUsage;
|
|
180
205
|
await flush({
|
|
181
206
|
status: outcome.status,
|
|
@@ -193,8 +218,14 @@ export async function runChatTurn(deps, params) {
|
|
|
193
218
|
...(fallbackText === undefined ? {} : { fallbackText }),
|
|
194
219
|
};
|
|
195
220
|
}
|
|
221
|
+
/** The sanitized text of the user message, thus the display never holds a secret that the model does not see. */
|
|
222
|
+
function userTextOf(message) {
|
|
223
|
+
if (typeof message.content === "string")
|
|
224
|
+
return message.content;
|
|
225
|
+
return message.content.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
226
|
+
}
|
|
196
227
|
const WORKING_MEMORY_TOOL_ID = "update_working_memory";
|
|
197
|
-
function compactionPolicy(deps, params, threadType, agent, emit) {
|
|
228
|
+
function compactionPolicy(deps, analysisId, params, threadType, agent, emit) {
|
|
198
229
|
// A report thread reads a frozen copy of the working memory, thus its agent declares no memory tool.
|
|
199
230
|
const remembers = agent.tools.some((tool) => tool.id === WORKING_MEMORY_TOOL_ID);
|
|
200
231
|
const workingMemory = createWorkingMemory(deps.pool);
|
|
@@ -205,7 +236,7 @@ function compactionPolicy(deps, params, threadType, agent, emit) {
|
|
|
205
236
|
request: remembers ? MEMORY_COMPACTION_REQUEST : SUMMARY_COMPACTION_REQUEST,
|
|
206
237
|
mask: remembers ? { allow: [WORKING_MEMORY_TOOL_ID] } : "none",
|
|
207
238
|
keepFirstTurn: threadType === "report",
|
|
208
|
-
recordsAfter: async (view) => contextRecordsFor({ threadType, analysisId
|
|
239
|
+
recordsAfter: async (view) => contextRecordsFor({ threadType, analysisId, workingMemory, ...(await readTurnContext(deps.pool, analysisId)) }, view),
|
|
209
240
|
};
|
|
210
241
|
}
|
|
211
242
|
/** The reason of a failed turn. It never holds the error message, because each later turn sends the note to the vendor. */
|
package/dist/index.d.ts
CHANGED
|
@@ -82,8 +82,8 @@ export type { SuspendOn } from "./providers/errors.js";
|
|
|
82
82
|
export { suspensionOfFailure } from "./workflows/suspension.js";
|
|
83
83
|
export type { Suspension } from "./workflows/suspension.js";
|
|
84
84
|
export { createStreamingChat } from "./providers/streaming-chat.js";
|
|
85
|
-
export { DEFAULT_CONVERSATION_BUDGET, prepareChatTurn, runChatTurn } from "./app/chat-turn.js";
|
|
86
|
-
export type { ChatTurnOutcome, ChatTurnResult, PrepareChatTurnDeps, PrepareChatTurnParams, PrepareChatTurnResult, RunChatTurnDeps, RunChatTurnParams, } from "./app/chat-turn.js";
|
|
85
|
+
export { DEFAULT_CONVERSATION_BUDGET, openChatTurn, prepareChatTurn, runChatTurn } from "./app/chat-turn.js";
|
|
86
|
+
export type { ChatTurnOutcome, ChatTurnRan, ChatTurnRefusal, ChatTurnResult, ChatTurnSession, OpenChatTurn, OpenChatTurnParams, PrepareChatTurnDeps, PrepareChatTurnParams, PrepareChatTurnResult, RunChatTurnDeps, RunChatTurnParams, RunOpenChatTurnParams, } from "./app/chat-turn.js";
|
|
87
87
|
export { createThreadStore } from "./memory/thread-store.js";
|
|
88
88
|
export type { ThreadStore, Thread, ThreadType, ThreadInputError, CreateThreadInput, ListThreadsInput, ThreadPage } from "./memory/thread-store.js";
|
|
89
89
|
export { createThreadHistory, conversationRecordTurn } from "./memory/thread-history.js";
|
package/dist/index.js
CHANGED
|
@@ -130,7 +130,7 @@ export { createStreamingChat } from "./providers/streaming-chat.js";
|
|
|
130
130
|
// and gives only its transport values: the harness stores the opening, each round,
|
|
131
131
|
// and the outcome. `appendTurn` stays for a record of a host. `createThreadStore`
|
|
132
132
|
// owns thread metadata (create/list/title).
|
|
133
|
-
export { DEFAULT_CONVERSATION_BUDGET, prepareChatTurn, runChatTurn } from "./app/chat-turn.js";
|
|
133
|
+
export { DEFAULT_CONVERSATION_BUDGET, openChatTurn, prepareChatTurn, runChatTurn } from "./app/chat-turn.js";
|
|
134
134
|
export { createThreadStore } from "./memory/thread-store.js";
|
|
135
135
|
export { createThreadHistory, conversationRecordTurn } from "./memory/thread-history.js";
|
|
136
136
|
// The synthetic-message primitives. A host appends a record of out-of-band work (an analysis run's
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* field so the field-coverage guard test fails when a new field is added to the
|
|
22
22
|
* schema without a deliberate decision about whether the agent needs to see it.
|
|
23
23
|
*/
|
|
24
|
+
import { type WorkingMemory } from "../memory/working-memory.js";
|
|
24
25
|
import type { DataProfileResult } from "../state/data-profile.js";
|
|
25
26
|
import type { AnalysisStep } from "../schemas/workflow-state.js";
|
|
26
27
|
/** Completed dependencies rendered as upstream blocks; the tail is counted, not rendered. */
|
|
@@ -50,6 +51,30 @@ export declare const STEP_NON_TASK_FIELDS: readonly ["id", "track", "step_type",
|
|
|
50
51
|
* improvises the rest.
|
|
51
52
|
*/
|
|
52
53
|
export declare function renderTask(step: AnalysisStep): string;
|
|
54
|
+
/**
|
|
55
|
+
* The heading of the analysis-memory section. The orient core of the sandbox
|
|
56
|
+
* prompt names this same text, thus a test pins the two together.
|
|
57
|
+
*/
|
|
58
|
+
export declare const ANALYSIS_MEMORY_HEADING = "Analysis memory (read only)";
|
|
59
|
+
/**
|
|
60
|
+
* The part of the working memory a step agent sees: the goal and the
|
|
61
|
+
* constraints. Hypotheses and findings stay out — they are the interpretive
|
|
62
|
+
* state of the conversation, not a rule for a step.
|
|
63
|
+
*/
|
|
64
|
+
export type StepMemory = Pick<WorkingMemory, "goal" | "constraints">;
|
|
65
|
+
/** A memory with no goal and no constraints — the section renders nothing. */
|
|
66
|
+
export declare function emptyStepMemory(): StepMemory;
|
|
67
|
+
/**
|
|
68
|
+
* The goal and the constraints of the analysis, frozen at dispatch. The heading
|
|
69
|
+
* differs from the "Constraints" section of {@link renderTask}: those are the
|
|
70
|
+
* requirements of this plan step, these are the rules of the whole analysis.
|
|
71
|
+
*
|
|
72
|
+
* Bounded to `WORKING_MEMORY_LIMITS` the same way the chat render is: a row
|
|
73
|
+
* written before the caps existed degrades to its newest constraints, and each
|
|
74
|
+
* text is clamped. No id is rendered, because the step agent cannot address an
|
|
75
|
+
* entry — it has no memory tool.
|
|
76
|
+
*/
|
|
77
|
+
export declare function renderMemory(memory: StepMemory): string;
|
|
53
78
|
/**
|
|
54
79
|
* What anchors every path the agent writes or reads: the id the tree is mounted
|
|
55
80
|
* at, and the step's writable directory.
|
|
@@ -109,10 +134,12 @@ export interface StepBriefing {
|
|
|
109
134
|
readonly profile: DataProfileResult | null;
|
|
110
135
|
/** The step's completed dependencies, in the plan's declared `depends_on` order. */
|
|
111
136
|
readonly upstream: readonly UpstreamHandoff[];
|
|
137
|
+
/** The goal and the constraints of the analysis, read from the working memory at dispatch. */
|
|
138
|
+
readonly memory: StepMemory;
|
|
112
139
|
}
|
|
113
140
|
/**
|
|
114
141
|
* Compose the seed. Absent sections collapse out entirely — an independent step
|
|
115
|
-
* with no profile yields exactly the task and workspace sections, byte-identical
|
|
142
|
+
* with no profile and an empty memory yields exactly the task and workspace sections, byte-identical
|
|
116
143
|
* to what a step got before any of this existed plus its paths.
|
|
117
144
|
*
|
|
118
145
|
* Pure: the same briefing composes to the same string, which is what makes the
|
package/dist/prompts/briefing.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* schema without a deliberate decision about whether the agent needs to see it.
|
|
23
23
|
*/
|
|
24
24
|
import { DATA_PROFILE_ORIENTATION_MAX_CHARS, buildDataProfileOrientation } from "../app/data-profile-orientation.js";
|
|
25
|
+
import { WORKING_MEMORY_LIMITS } from "../memory/working-memory.js";
|
|
25
26
|
// ── Bounds ────────────────────────────────────────────────────────────
|
|
26
27
|
/** Completed dependencies rendered as upstream blocks; the tail is counted, not rendered. */
|
|
27
28
|
export const MAX_UPSTREAM_DEPS = 5;
|
|
@@ -113,6 +114,47 @@ export function renderTask(step) {
|
|
|
113
114
|
}
|
|
114
115
|
return parts.join("\n\n");
|
|
115
116
|
}
|
|
117
|
+
// ── (1b) Analysis memory ──────────────────────────────────────────────
|
|
118
|
+
/**
|
|
119
|
+
* The heading of the analysis-memory section. The orient core of the sandbox
|
|
120
|
+
* prompt names this same text, thus a test pins the two together.
|
|
121
|
+
*/
|
|
122
|
+
export const ANALYSIS_MEMORY_HEADING = "Analysis memory (read only)";
|
|
123
|
+
/** A memory with no goal and no constraints — the section renders nothing. */
|
|
124
|
+
export function emptyStepMemory() {
|
|
125
|
+
return { goal: "", constraints: [] };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The goal and the constraints of the analysis, frozen at dispatch. The heading
|
|
129
|
+
* differs from the "Constraints" section of {@link renderTask}: those are the
|
|
130
|
+
* requirements of this plan step, these are the rules of the whole analysis.
|
|
131
|
+
*
|
|
132
|
+
* Bounded to `WORKING_MEMORY_LIMITS` the same way the chat render is: a row
|
|
133
|
+
* written before the caps existed degrades to its newest constraints, and each
|
|
134
|
+
* text is clamped. No id is rendered, because the step agent cannot address an
|
|
135
|
+
* entry — it has no memory tool.
|
|
136
|
+
*/
|
|
137
|
+
export function renderMemory(memory) {
|
|
138
|
+
const goal = clamp(memory.goal.trim(), WORKING_MEMORY_LIMITS.goalChars);
|
|
139
|
+
const all = memory.constraints.filter((c) => c.text.trim().length > 0);
|
|
140
|
+
const shown = all.slice(Math.max(0, all.length - WORKING_MEMORY_LIMITS.constraints));
|
|
141
|
+
if (goal.length === 0 && shown.length === 0)
|
|
142
|
+
return "";
|
|
143
|
+
const parts = [
|
|
144
|
+
"These entries come from the working memory of the analysis, read when this step was dispatched. " +
|
|
145
|
+
"A constraint from the user is binding. A constraint from the agent is context.",
|
|
146
|
+
];
|
|
147
|
+
if (goal.length > 0)
|
|
148
|
+
parts.push(`Goal: ${goal}`);
|
|
149
|
+
if (shown.length > 0) {
|
|
150
|
+
const lines = shown.map((c) => `(${c.origin}) ${clamp(c.text.trim(), WORKING_MEMORY_LIMITS.entryChars)}`);
|
|
151
|
+
const omitted = all.length - shown.length;
|
|
152
|
+
if (omitted > 0)
|
|
153
|
+
lines.push(`(+${omitted} older ${omitted === 1 ? "constraint" : "constraints"} not shown)`);
|
|
154
|
+
parts.push(`Constraints:\n${bullets(lines)}`);
|
|
155
|
+
}
|
|
156
|
+
return section(ANALYSIS_MEMORY_HEADING, parts.join("\n\n"));
|
|
157
|
+
}
|
|
116
158
|
/** The in-sandbox analysis root of a frame — the read-only mount of the whole tree. */
|
|
117
159
|
export function analysisRootOf(frame) {
|
|
118
160
|
return `/${frame.analysisId}`;
|
|
@@ -190,7 +232,7 @@ function renderHandoff(handoff) {
|
|
|
190
232
|
}
|
|
191
233
|
/**
|
|
192
234
|
* Compose the seed. Absent sections collapse out entirely — an independent step
|
|
193
|
-
* with no profile yields exactly the task and workspace sections, byte-identical
|
|
235
|
+
* with no profile and an empty memory yields exactly the task and workspace sections, byte-identical
|
|
194
236
|
* to what a step got before any of this existed plus its paths.
|
|
195
237
|
*
|
|
196
238
|
* Pure: the same briefing composes to the same string, which is what makes the
|
|
@@ -199,6 +241,7 @@ function renderHandoff(handoff) {
|
|
|
199
241
|
export function composeStepBriefing(briefing) {
|
|
200
242
|
return [
|
|
201
243
|
renderTask(briefing.step),
|
|
244
|
+
renderMemory(briefing.memory),
|
|
202
245
|
renderWorkspace(briefing.workspace),
|
|
203
246
|
renderResources(briefing.step.resources),
|
|
204
247
|
renderOrientation(briefing.profile, briefing.workspace.analysisId),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
/** The compaction request of a thread whose agent maintains the working memory. */
|
|
2
|
-
export declare const MEMORY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary.\n\nFirst, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. No other tool can run now.\n\nThen reply in plain text with the summary. Leave out
|
|
2
|
+
export declare const MEMORY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary.\n\nFirst, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. Keep each constraint that the user gave as it is, because the user set it. No other tool can run now. Send all the memory edits in at most 3 replies, and put many calls in one reply. The reply after the edits must be the summary: if it calls a tool, the harness gets no summary.\n\nThen reply in plain text with the summary. Leave out each fact that a memory edit accepted. If the memory refused an edit, put that fact in the summary. Give:\n- the current request of the user in its exact words\n- the decisions, with their reasons\n- the approaches that failed or that the user ruled out, with the reasons\n- the open questions\n- the file paths, the run ids, and the other exact values that the work uses\n- the state of the work in progress\n\nThe conversation continues from your summary, the working memory, and the messages after them.";
|
|
3
3
|
/** The compaction request of a thread that reads a frozen copy of the working memory, for example a report thread. */
|
|
4
|
-
export declare const SUMMARY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary. No tool can run now.\n\nReply in plain text with the summary. Leave out what the working memory holds. Give:\n- the goals, and the current request of the user in its exact words\n- the decisions, with their reasons\n- the open questions\n- the file paths
|
|
4
|
+
export declare const SUMMARY_COMPACTION_REQUEST = "The conversation is too long for the context window. The harness replaces the conversation above with your summary. No tool can run now.\n\nReply in plain text with the summary. Leave out what the working memory holds. Give:\n- the goals, and the current request of the user in its exact words\n- each change that the user made to the brief\n- the decisions, with their reasons\n- the approaches that failed or that the user ruled out, with the reasons\n- the open questions\n- the file paths, the run ids, and the other exact values that the work uses\n- the state of the work in progress\n\nThe conversation continues from your summary, the working memory, and the messages after them.";
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { COMPACTION_MAX_REQUESTS } from "../loop/compaction.js";
|
|
1
2
|
/** The compaction request of a thread whose agent maintains the working memory. */
|
|
2
3
|
export const MEMORY_COMPACTION_REQUEST = `The conversation is too long for the context window. The harness replaces the conversation above with your summary.
|
|
3
4
|
|
|
4
|
-
First, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. No other tool can run now.
|
|
5
|
+
First, move each lasting fact into the working memory with update_working_memory: the goal, each constraint, each hypothesis, and each finding with its run id. Keep each constraint that the user gave as it is, because the user set it. No other tool can run now. Send all the memory edits in at most ${COMPACTION_MAX_REQUESTS - 1} replies, and put many calls in one reply. The reply after the edits must be the summary: if it calls a tool, the harness gets no summary.
|
|
5
6
|
|
|
6
|
-
Then reply in plain text with the summary. Leave out
|
|
7
|
-
- the
|
|
7
|
+
Then reply in plain text with the summary. Leave out each fact that a memory edit accepted. If the memory refused an edit, put that fact in the summary. Give:
|
|
8
|
+
- the current request of the user in its exact words
|
|
8
9
|
- the decisions, with their reasons
|
|
10
|
+
- the approaches that failed or that the user ruled out, with the reasons
|
|
9
11
|
- the open questions
|
|
10
|
-
- the file paths
|
|
12
|
+
- the file paths, the run ids, and the other exact values that the work uses
|
|
11
13
|
- the state of the work in progress
|
|
12
14
|
|
|
13
15
|
The conversation continues from your summary, the working memory, and the messages after them.`;
|
|
@@ -16,9 +18,11 @@ export const SUMMARY_COMPACTION_REQUEST = `The conversation is too long for the
|
|
|
16
18
|
|
|
17
19
|
Reply in plain text with the summary. Leave out what the working memory holds. Give:
|
|
18
20
|
- the goals, and the current request of the user in its exact words
|
|
21
|
+
- each change that the user made to the brief
|
|
19
22
|
- the decisions, with their reasons
|
|
23
|
+
- the approaches that failed or that the user ruled out, with the reasons
|
|
20
24
|
- the open questions
|
|
21
|
-
- the file paths
|
|
25
|
+
- the file paths, the run ids, and the other exact values that the work uses
|
|
22
26
|
- the state of the work in progress
|
|
23
27
|
|
|
24
28
|
The conversation continues from your summary, the working memory, and the messages after them.`;
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* each composition stays byte-stable across its own steps.
|
|
30
30
|
*/
|
|
31
31
|
import type { ToolchainSource } from "../sandbox/types.js";
|
|
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 set that variable in `execute_command`'s `env`\n for the command that needs it, 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## Context7 \u2014 Documentation Lookup\n\nWhen you are not certain of a function's signature or behavior, look it up\nwith context7 before you write the call: `resolve_library_id` with the\npackage name, then `query_docs` with the library ID and the function.\nRapidly-evolving packages (scvi-tools, spatialdata, cellrank, pertpy, muon)\nchange their APIs between releases, so check them against the docs or the\nskill references rather than memory.\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.\nIts `command` is an argv array. A one-element array runs through `sh -c`,\nso pipes, `&&`, redirection, quoting, and globbing work there. An array of\ntwo or more elements runs directly, with no shell.\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` |\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`** \u2014 a recursive search by name, age, or size: `find . -name '*.h5ad'`,\n `find . -size +10M`. `list_files` lists one directory only.\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";
|
|
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\nWhen the analysis has a recorded goal or constraints, the briefing carries them\nunder \"Analysis memory (read only)\": obey each constraint from the user, and treat\neach constraint from the agent as context. You cannot change that memory.\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 set that variable in `execute_command`'s `env`\n for the command that needs it, 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## Context7 \u2014 Documentation Lookup\n\nWhen you are not certain of a function's signature or behavior, look it up\nwith context7 before you write the call: `resolve_library_id` with the\npackage name, then `query_docs` with the library ID and the function.\nRapidly-evolving packages (scvi-tools, spatialdata, cellrank, pertpy, muon)\nchange their APIs between releases, so check them against the docs or the\nskill references rather than memory.\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.\nIts `command` is an argv array. A one-element array runs through `sh -c`,\nso pipes, `&&`, redirection, quoting, and globbing work there. An array of\ntwo or more elements runs directly, with no shell.\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` |\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`** \u2014 a recursive search by name, age, or size: `find . -name '*.h5ad'`,\n `find . -size +10M`. `list_files` lists one directory only.\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
|
|
@@ -40,6 +40,10 @@ working directory and the read-only analysis root, what the input dataset is
|
|
|
40
40
|
(domain, organism, design, quality concerns, files), and what each upstream step
|
|
41
41
|
produced and where it put it. Work from it.
|
|
42
42
|
|
|
43
|
+
When the analysis has a recorded goal or constraints, the briefing carries them
|
|
44
|
+
under "Analysis memory (read only)": obey each constraint from the user, and treat
|
|
45
|
+
each constraint from the agent as context. You cannot change that memory.
|
|
46
|
+
|
|
43
47
|
Do NOT re-derive what it already gives you: no filesystem hunt for your inputs, no
|
|
44
48
|
re-reading an upstream step whose summary you were handed, no re-deriving the
|
|
45
49
|
organism or the dimensions from raw bytes. Reach further only where the briefing is
|
|
@@ -50,11 +50,13 @@ export type SandboxStepCallable = (input: SandboxStepInput) => Promise<SandboxSt
|
|
|
50
50
|
* conversation agent does not — a half-wired ledger that reads as a complete
|
|
51
51
|
* one. The protection has to hold on both sides of the assembly, not just the
|
|
52
52
|
* conversation side. Each bag omits `toolOutputStore` for the same reason: each
|
|
53
|
-
* loop must keep its texts in the one store that each read tool reads.
|
|
53
|
+
* loop must keep its texts in the one store that each read tool reads. The
|
|
54
|
+
* `executeAnalysis` bag omits `workingMemory`, because the step seeds must read
|
|
55
|
+
* the same memory that the conversation agent writes.
|
|
54
56
|
*/
|
|
55
57
|
export interface CoreWorkflowDeps {
|
|
56
58
|
readonly sandboxStep: Omit<SandboxStepDeps, "usageRecorder" | "citationResolver" | "toolOutputStore">;
|
|
57
|
-
readonly buildExecuteAnalysis: (sandboxStep: SandboxStepCallable) => Omit<ExecuteAnalysisDeps, "usageRecorder" | "citationResolver" | "toolOutputStore">;
|
|
59
|
+
readonly buildExecuteAnalysis: (sandboxStep: SandboxStepCallable) => Omit<ExecuteAnalysisDeps, "usageRecorder" | "citationResolver" | "toolOutputStore" | "workingMemory">;
|
|
58
60
|
readonly dataProfile: Omit<DataProfileDeps, "usageRecorder" | "toolOutputStore">;
|
|
59
61
|
}
|
|
60
62
|
/** The registered, callable workflow handles. */
|
package/dist/runtime/assemble.js
CHANGED
|
@@ -28,6 +28,7 @@ import { bindExtractionTrigger, createExtractionArm, registerExtractValuesWorkfl
|
|
|
28
28
|
import { registerDeriveTableExecWorkflow, triggerDeriveTableExec } from "../tasks/derive-table-exec.js";
|
|
29
29
|
import { createCitationResolver } from "../citations/resolve.js";
|
|
30
30
|
import { createThreadStore } from "../memory/thread-store.js";
|
|
31
|
+
import { createWorkingMemory } from "../memory/working-memory.js";
|
|
31
32
|
import { createArtifactReadStore, createProductionResolver } from "../report-model/production-resolver.js";
|
|
32
33
|
import { createReportVersionStore } from "../state/report-versions.js";
|
|
33
34
|
import { createToolOutputStore } from "../state/tool-outputs.js";
|
|
@@ -86,7 +87,13 @@ export function assembleCoreRuntime(deps) {
|
|
|
86
87
|
});
|
|
87
88
|
const toolOutputStore = createToolOutputStore(conversation.pool);
|
|
88
89
|
const sandboxStep = registerSandboxStep({ ...wf.sandboxStep, citationResolver, usageRecorder, toolOutputStore });
|
|
89
|
-
const executeAnalysis = registerExecuteAnalysis({
|
|
90
|
+
const executeAnalysis = registerExecuteAnalysis({
|
|
91
|
+
...wf.buildExecuteAnalysis(sandboxStep),
|
|
92
|
+
citationResolver,
|
|
93
|
+
usageRecorder,
|
|
94
|
+
toolOutputStore,
|
|
95
|
+
workingMemory: createWorkingMemory(conversation.pool),
|
|
96
|
+
});
|
|
90
97
|
const dataProfile = registerDataProfileWorkflow({ ...wf.dataProfile, usageRecorder, toolOutputStore });
|
|
91
98
|
// The extraction workflow shares the profile's sandbox and authorization rails, thus it draws the same
|
|
92
99
|
// three seams from the profile deps. The report resolver factory binds the extraction arm over this
|
|
@@ -24,16 +24,16 @@ export const FileMetadataSchema = z
|
|
|
24
24
|
*/
|
|
25
25
|
export const SubmittedFileDescriptionSchema = z.object({
|
|
26
26
|
/** Exact path of the file being described, copied from the prompt's list. */
|
|
27
|
-
path: z.string(),
|
|
28
|
-
description: z.string(),
|
|
29
|
-
dataType: z.string(),
|
|
30
|
-
format: z.string(),
|
|
31
|
-
rows: z.number().nullish(),
|
|
32
|
-
cols: z.number().nullish(),
|
|
33
|
-
tags: z.array(z.string()).optional(),
|
|
34
|
-
warnings: z.array(z.string()).optional(),
|
|
27
|
+
path: z.string().describe("The path of the file, copied exactly from the list of files that the harness gave."),
|
|
28
|
+
description: z.string().describe("One sentence that says what the file contains. A semantic search of the workspace later finds the file by this text."),
|
|
29
|
+
dataType: z.string().describe('The semantic type of the data, for example "count matrix" or "QC report".'),
|
|
30
|
+
format: z.string().describe('The file format, for example "csv", "tsv", "h5ad", or "png".'),
|
|
31
|
+
rows: z.number().nullish().describe("The count of rows, when you know it."),
|
|
32
|
+
cols: z.number().nullish().describe("The count of columns, when you know it."),
|
|
33
|
+
tags: z.array(z.string()).optional().describe("Short labels that help a search find the file."),
|
|
34
|
+
warnings: z.array(z.string()).optional().describe("Known problems of the file that a reader must know before use."),
|
|
35
35
|
});
|
|
36
36
|
/** Input contract for the `submit_file_metadata` tool. */
|
|
37
37
|
export const SubmitFileMetadataInputSchema = z.object({
|
|
38
|
-
files: z.array(SubmittedFileDescriptionSchema),
|
|
38
|
+
files: z.array(SubmittedFileDescriptionSchema).describe("One entry per output file. You can cover the files in one call or in several."),
|
|
39
39
|
});
|
|
@@ -11,10 +11,10 @@ export declare const PlanStepSchema: z.ZodObject<{
|
|
|
11
11
|
context: z.ZodOptional<z.ZodString>;
|
|
12
12
|
description: z.ZodOptional<z.ZodString>;
|
|
13
13
|
id: z.ZodString;
|
|
14
|
+
constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
14
15
|
track: z.ZodString;
|
|
15
16
|
step_type: z.ZodString;
|
|
16
17
|
question: z.ZodString;
|
|
17
|
-
constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
18
18
|
acceptance_criteria: z.ZodArray<z.ZodString>;
|
|
19
19
|
caveats: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
20
20
|
packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
@@ -44,10 +44,10 @@ export declare const PlannerPlanSchema: z.ZodObject<{
|
|
|
44
44
|
context: z.ZodOptional<z.ZodString>;
|
|
45
45
|
description: z.ZodOptional<z.ZodString>;
|
|
46
46
|
id: z.ZodString;
|
|
47
|
+
constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
47
48
|
track: z.ZodString;
|
|
48
49
|
step_type: z.ZodString;
|
|
49
50
|
question: z.ZodString;
|
|
50
|
-
constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
51
51
|
acceptance_criteria: z.ZodArray<z.ZodString>;
|
|
52
52
|
caveats: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
53
53
|
packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
@@ -86,10 +86,10 @@ export declare const PlanningAgentOutputSchema: z.ZodObject<{
|
|
|
86
86
|
context: z.ZodOptional<z.ZodString>;
|
|
87
87
|
description: z.ZodOptional<z.ZodString>;
|
|
88
88
|
id: z.ZodString;
|
|
89
|
+
constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
89
90
|
track: z.ZodString;
|
|
90
91
|
step_type: z.ZodString;
|
|
91
92
|
question: z.ZodString;
|
|
92
|
-
constraints: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
93
93
|
acceptance_criteria: z.ZodArray<z.ZodString>;
|
|
94
94
|
caveats: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
95
95
|
packages: z.ZodNonOptional<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|