@inflexa-ai/harness 0.45.0 → 0.45.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/agents/conversation-agent.d.ts +10 -0
- package/dist/agents/conversation-agent.js +19 -1
- package/dist/agents/sandbox/shared.d.ts +23 -2
- package/dist/agents/sandbox/shared.js +51 -18
- package/dist/agents/sandbox/translational-safety-agent.js +26 -0
- package/dist/agents/sandbox/types.d.ts +24 -1
- package/dist/agents/sandbox-catalog.d.ts +14 -0
- package/dist/agents/sandbox-catalog.js +19 -0
- package/dist/app/approve-plan.d.ts +5 -2
- package/dist/app/approve-plan.js +1 -1
- package/dist/app/chat-turn.js +6 -3
- package/dist/config/environment-stores.d.ts +4 -2
- package/dist/contracts/chat-events.d.ts +12 -2
- package/dist/contracts/chat-events.js +1 -1
- package/dist/contracts/chat-frame.js +6 -2
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/schemas/chat-events.d.ts +25 -0
- package/dist/contracts/schemas/chat-events.js +7 -0
- package/dist/contracts/schemas/chat-parts.d.ts +39 -0
- package/dist/contracts/schemas/chat-parts.js +17 -0
- package/dist/contracts/schemas/index.d.ts +1 -1
- package/dist/contracts/schemas/index.js +1 -1
- package/dist/execution/dbos-workflow-purger.d.ts +2 -0
- package/dist/execution/dbos-workflow-purger.js +21 -14
- package/dist/execution/run-canceler.d.ts +33 -11
- package/dist/execution/run-canceler.js +129 -27
- package/dist/execution/run-end-record.d.ts +30 -0
- package/dist/execution/run-end-record.js +27 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +11 -1
- package/dist/loop/call-guard.d.ts +50 -0
- package/dist/loop/call-guard.js +90 -0
- package/dist/loop/compaction.d.ts +5 -0
- package/dist/loop/compaction.js +17 -0
- package/dist/loop/run-agent.d.ts +9 -0
- package/dist/loop/run-agent.js +15 -1
- package/dist/loop/run-to-terminal.js +4 -1
- package/dist/loop/types.d.ts +6 -0
- package/dist/memory/conversation-display-recorder.d.ts +0 -2
- package/dist/memory/conversation-display-recorder.js +28 -3
- package/dist/memory/thread-history.d.ts +34 -7
- package/dist/memory/thread-history.js +151 -42
- package/dist/prompts/briefing.d.ts +57 -3
- package/dist/prompts/briefing.js +129 -2
- package/dist/prompts/planner.js +13 -2
- package/dist/provenance/seam.d.ts +23 -15
- package/dist/provenance/seam.js +7 -5
- package/dist/providers/ai-sdk.js +87 -7
- package/dist/providers/types.d.ts +6 -0
- package/dist/reference-data/catalog.js +141 -2
- package/dist/report-model/authoring-grammar.d.ts +26 -26
- package/dist/sandbox/image-packages.d.ts +26 -4
- package/dist/sandbox/image-packages.js +18 -4
- package/dist/schemas/data-profile-schemas.d.ts +12 -12
- package/dist/schemas/plan-schemas.d.ts +57 -3
- package/dist/schemas/plan-schemas.js +1 -1
- package/dist/schemas/workflow-state.d.ts +113 -0
- package/dist/schemas/workflow-state.js +54 -0
- package/dist/state/artifacts.d.ts +17 -1
- package/dist/state/artifacts.js +21 -3
- package/dist/state/index.d.ts +2 -2
- package/dist/state/index.js +1 -1
- package/dist/state/runs.d.ts +43 -9
- package/dist/state/runs.js +85 -14
- package/dist/state/step-executions.d.ts +9 -0
- package/dist/state/step-executions.js +22 -0
- package/dist/tools/ad-hoc-router.d.ts +2 -0
- package/dist/tools/ad-hoc-router.js +18 -2
- package/dist/tools/capability-data.d.ts +36 -0
- package/dist/tools/capability-data.js +73 -0
- package/dist/tools/execute-analysis.d.ts +37 -9
- package/dist/tools/execute-analysis.js +131 -21
- package/dist/tools/knowledge/check.d.ts +60 -0
- package/dist/tools/knowledge/check.js +92 -0
- package/dist/tools/knowledge/client.d.ts +382 -0
- package/dist/tools/knowledge/client.js +302 -0
- package/dist/tools/knowledge/environment.d.ts +78 -0
- package/dist/tools/knowledge/environment.js +105 -0
- package/dist/tools/knowledge/index.d.ts +49 -0
- package/dist/tools/knowledge/index.js +70 -0
- package/dist/tools/knowledge/local-slots.d.ts +62 -0
- package/dist/tools/knowledge/local-slots.js +177 -0
- package/dist/tools/knowledge/pin.d.ts +14 -0
- package/dist/tools/knowledge/pin.js +17 -0
- package/dist/tools/knowledge/recommend.d.ts +92 -0
- package/dist/tools/knowledge/recommend.js +98 -0
- package/dist/tools/knowledge/situation.d.ts +143 -0
- package/dist/tools/knowledge/situation.js +164 -0
- package/dist/tools/knowledge/skeleton.d.ts +69 -0
- package/dist/tools/knowledge/skeleton.js +202 -0
- package/dist/tools/knowledge/template.d.ts +137 -0
- package/dist/tools/knowledge/template.js +267 -0
- package/dist/tools/research/generate-plan.d.ts +23 -0
- package/dist/tools/research/generate-plan.js +203 -12
- package/dist/tools/research/guarded-address.d.ts +20 -0
- package/dist/tools/research/guarded-address.js +148 -0
- package/dist/tools/research/searxng-engine.d.ts +40 -0
- package/dist/tools/research/searxng-engine.js +120 -0
- package/dist/tools/research/web-fetch.d.ts +124 -0
- package/dist/tools/research/web-fetch.js +627 -0
- package/dist/tools/research/web-search.d.ts +75 -0
- package/dist/tools/research/web-search.js +112 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +7 -4
- package/dist/tools/sandbox/list-available-packages.js +12 -4
- package/dist/tools/sandbox/list-available-refs.d.ts +21 -0
- package/dist/tools/sandbox/list-available-refs.js +60 -3
- package/dist/tools/sandbox/report-blocker.js +5 -1
- package/dist/tools/terminal-text.d.ts +11 -0
- package/dist/tools/terminal-text.js +19 -0
- package/dist/tools/workspace/decision-record.d.ts +81 -0
- package/dist/tools/workspace/decision-record.js +117 -0
- package/dist/tools/workspace/edit-file.d.ts +3 -1
- package/dist/tools/workspace/edit-file.js +20 -2
- package/dist/tools/workspace/execute-command.d.ts +17 -0
- package/dist/tools/workspace/execute-command.js +28 -1
- package/dist/tools/workspace/index.d.ts +1 -0
- package/dist/tools/workspace/index.js +1 -0
- package/dist/tools/workspace/mutator.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +58 -18
- package/dist/workflows/execute-analysis.js +273 -58
- package/dist/workflows/sandbox-step.d.ts +12 -0
- package/package.json +5 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plan skeleton: the procedure of the service folded into the steps of a
|
|
3
|
+
* plan, with the agent, the packages, the dependencies, the caveats, the
|
|
4
|
+
* alternatives, the disputed sides, the forbidden methods, the environment,
|
|
5
|
+
* and the grounding of each step filled from the answer. It is
|
|
6
|
+
* the one representation the planner receives: the procedure itself never
|
|
7
|
+
* reaches the model, thus nothing the procedure holds may be lost here. A
|
|
8
|
+
* small model edits a skeleton where it fails to compose a plan from a
|
|
9
|
+
* procedure; a frontier model pays nothing for it. The skeleton carries only
|
|
10
|
+
* the fields the answer can fill. The question, the acceptance criteria, the
|
|
11
|
+
* resources, and the step budget come from the data profile, and the planner
|
|
12
|
+
* adds them.
|
|
13
|
+
*
|
|
14
|
+
* The fold is fixed: one cohort assembly step when the procedure holds one
|
|
15
|
+
* (the sample exclusions and the sum of technical replicates, before any
|
|
16
|
+
* other step reads the counts), one QC step, one differential expression
|
|
17
|
+
* step that holds the filter, the normalization, the design, the test, the
|
|
18
|
+
* shrinkage, and the multiple testing (the templates cover the same span),
|
|
19
|
+
* one enrichment step on its own track, and one report step. A group with
|
|
20
|
+
* no step in the procedure is absent from the skeleton.
|
|
21
|
+
*
|
|
22
|
+
* The central step of a group names the method of the skeleton step, and it
|
|
23
|
+
* gives the alternatives, the template, and the environment: an alternative
|
|
24
|
+
* of a folded step (a normalization in the differential expression group)
|
|
25
|
+
* is not a permitted replacement of the step method, and it stays out. The
|
|
26
|
+
* settings, the caveats, and the forbidden methods come from every step of
|
|
27
|
+
* the group, each setting with the step it belongs to. A parameter is a
|
|
28
|
+
* setting of the grounding, never a constraint of the step: the value is the
|
|
29
|
+
* vetted default, and the agent can state another value with its reason.
|
|
30
|
+
*/
|
|
31
|
+
const GROUPS = [
|
|
32
|
+
{ id: "T0S1", track: "T1", step_type: "data_preparation", agent: "bulk-transcriptomics-agent", steps: ["cohort_assembly"], name: "Cohort assembly" },
|
|
33
|
+
{ id: "T1S1", track: "T1", step_type: "qc", agent: "bulk-transcriptomics-agent", steps: ["qc_sample_structure"], name: "Sample structure QC" },
|
|
34
|
+
{
|
|
35
|
+
id: "T1S2",
|
|
36
|
+
track: "T1",
|
|
37
|
+
step_type: "analysis",
|
|
38
|
+
agent: "bulk-transcriptomics-agent",
|
|
39
|
+
steps: ["filter_low_counts", "normalize", "model_design", "differential_expression", "shrink_lfc", "multiple_testing"],
|
|
40
|
+
name: "Differential expression",
|
|
41
|
+
},
|
|
42
|
+
{ id: "T2S1", track: "T2", step_type: "enrichment", agent: "enrichment-agent", steps: ["enrichment"], name: "Gene set enrichment" },
|
|
43
|
+
{
|
|
44
|
+
id: "T2S2",
|
|
45
|
+
track: "T2",
|
|
46
|
+
step_type: "activity",
|
|
47
|
+
agent: "enrichment-agent",
|
|
48
|
+
steps: ["tf_activity", "pathway_activity"],
|
|
49
|
+
name: "Regulator and pathway activity",
|
|
50
|
+
},
|
|
51
|
+
{ id: "T1S4", track: "T1", step_type: "analysis", agent: "bulk-transcriptomics-agent", steps: ["variance_partition"], name: "Variance partition" },
|
|
52
|
+
{ id: "T3S1", track: "T3", step_type: "analysis", agent: "bulk-transcriptomics-agent", steps: ["signature_scoring"], name: "Signature scoring" },
|
|
53
|
+
{ id: "T3S2", track: "T3", step_type: "analysis", agent: "bulk-transcriptomics-agent", steps: ["deconvolution"], name: "Cell type deconvolution" },
|
|
54
|
+
{ id: "T3S3", track: "T3", step_type: "analysis", agent: "bulk-transcriptomics-agent", steps: ["coexpression"], name: "Co-expression modules" },
|
|
55
|
+
{ id: "T3S4", track: "T3", step_type: "analysis", agent: "bulk-transcriptomics-agent", steps: ["clustering"], name: "Sample clustering" },
|
|
56
|
+
{ id: "T3S5", track: "T3", step_type: "analysis", agent: "bulk-transcriptomics-agent", steps: ["survival"], name: "Outcome association" },
|
|
57
|
+
{ id: "T1S5", track: "T1", step_type: "analysis", agent: "bulk-transcriptomics-agent", steps: ["transcript_level"], name: "Transcript-level analysis" },
|
|
58
|
+
{ id: "T1S6", track: "T1", step_type: "analysis", agent: "bulk-transcriptomics-agent", steps: ["annotation"], name: "Identifier annotation" },
|
|
59
|
+
{ id: "T1S3", track: "T1", step_type: "report", agent: "bulk-transcriptomics-agent", steps: ["report"], name: "Report" },
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* The dependencies of each group. The QC depends on the cohort assembly when
|
|
63
|
+
* one is present, a group depends on the QC, on the differential expression
|
|
64
|
+
* when its input is a results table, and the report depends on every group
|
|
65
|
+
* that is present.
|
|
66
|
+
*/
|
|
67
|
+
const DEPENDS = {
|
|
68
|
+
T0S1: [],
|
|
69
|
+
T1S1: ["T0S1"],
|
|
70
|
+
T1S2: ["T1S1"],
|
|
71
|
+
T2S1: ["T1S2"],
|
|
72
|
+
T2S2: ["T1S2", "T1S1"],
|
|
73
|
+
T1S4: ["T1S1"],
|
|
74
|
+
T3S1: ["T1S1"],
|
|
75
|
+
T3S2: ["T1S1"],
|
|
76
|
+
T3S3: ["T1S1"],
|
|
77
|
+
T3S4: ["T1S1"],
|
|
78
|
+
T3S5: ["T3S1", "T1S1"],
|
|
79
|
+
T1S5: ["T1S1"],
|
|
80
|
+
T1S6: ["T1S1"],
|
|
81
|
+
T1S3: ["T1S2", "T2S1", "T2S2", "T1S4", "T3S1", "T3S2", "T3S3", "T3S4", "T3S5", "T1S5", "T1S6"],
|
|
82
|
+
};
|
|
83
|
+
/** A parameter value as a setting: a scalar stays, a list becomes a list of strings, anything else its text. */
|
|
84
|
+
function settingValue(value) {
|
|
85
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
86
|
+
return value;
|
|
87
|
+
if (Array.isArray(value))
|
|
88
|
+
return value.map(String);
|
|
89
|
+
return String(value);
|
|
90
|
+
}
|
|
91
|
+
/** The settings of the steps of a group: one entry per parameter, with the step it belongs to. */
|
|
92
|
+
function stepSettings(steps) {
|
|
93
|
+
return steps.flatMap((step) => (step.parameters ?? []).map((parameter) => ({
|
|
94
|
+
step: step.step,
|
|
95
|
+
name: parameter.name,
|
|
96
|
+
value: settingValue(parameter.value),
|
|
97
|
+
...(parameter.default_source ? { source: parameter.default_source } : {}),
|
|
98
|
+
})));
|
|
99
|
+
}
|
|
100
|
+
/** The forbidden method ids of a step. The client schema reads the field through the loose object. */
|
|
101
|
+
function forbidsOf(step) {
|
|
102
|
+
const forbids = step.forbids;
|
|
103
|
+
return Array.isArray(forbids) ? forbids.filter((method) => typeof method === "string") : [];
|
|
104
|
+
}
|
|
105
|
+
/** `a and b`, or `a, b and c` for a longer list. */
|
|
106
|
+
function joinWithAnd(items) {
|
|
107
|
+
if (items.length <= 1)
|
|
108
|
+
return items.join("");
|
|
109
|
+
return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The caveats a step carries beside its warn flags. A parameter conflict, a
|
|
113
|
+
* substitution, and a language limit each render as a caveat, never as a
|
|
114
|
+
* setting: a setting is a value, and none of the three is a value.
|
|
115
|
+
*/
|
|
116
|
+
function stepCaveats(step) {
|
|
117
|
+
const caveats = [];
|
|
118
|
+
for (const conflict of step.conflicts ?? []) {
|
|
119
|
+
caveats.push(`${step.step}: ${conflict.parameter} conflicts between ${joinWithAnd(conflict.entries.map((entry) => entry.rule))}`);
|
|
120
|
+
}
|
|
121
|
+
if (step.substitution && step.method) {
|
|
122
|
+
caveats.push(`${step.method.label} stands in for ${step.substitution.label}`);
|
|
123
|
+
}
|
|
124
|
+
if (step.limit) {
|
|
125
|
+
// Only two languages exist, thus the named template is in the other one. A slot that refuses a value of the
|
|
126
|
+
// rules is not a fact of the design, thus with a refusal the caveat names the cause of each skipped template.
|
|
127
|
+
const named = step.limit.requested_language === "python" ? "R" : "Python";
|
|
128
|
+
const refused = step.limit.skipped.some((entry) => entry.refused !== undefined);
|
|
129
|
+
const causes = step.limit.skipped.map((entry) => entry.refused !== undefined ? `${entry.template}: ${entry.refused}` : `${entry.template} does not honor ${entry.missing.join(", ")}`);
|
|
130
|
+
const cause = refused ? `because ${causes.join("; ")}` : "for this design";
|
|
131
|
+
caveats.push(`the requested language has no template that realizes ${step.method?.label ?? step.step} ${cause}; the ${named} template is named`);
|
|
132
|
+
}
|
|
133
|
+
if (step.unrealized) {
|
|
134
|
+
// The knowledge covers the step and no vetted script does: the plan says so, and it invents no template.
|
|
135
|
+
const why = step.unrealized.templates.map((entry) => `${entry.template}: ${entry.why}`).join("; ");
|
|
136
|
+
caveats.push(`no vetted template realizes ${step.method?.label ?? step.step} for this design (${why || "the method lists no template for the step"}); the step runs without a template of the service`);
|
|
137
|
+
}
|
|
138
|
+
return caveats;
|
|
139
|
+
}
|
|
140
|
+
/** The present dependencies of a group; a group whose own dependency is absent falls back to the QC, and the two first groups to nothing. */
|
|
141
|
+
function dependsOn(id, kept) {
|
|
142
|
+
const present = (DEPENDS[id] ?? []).filter((dependency) => kept.has(dependency));
|
|
143
|
+
if (present.length > 0)
|
|
144
|
+
return [...new Set(present)];
|
|
145
|
+
return id !== "T0S1" && id !== "T1S1" && kept.has("T1S1") ? ["T1S1"] : [];
|
|
146
|
+
}
|
|
147
|
+
export function buildPlanSkeleton(answer) {
|
|
148
|
+
const byStep = new Map(answer.procedure.map((step) => [step.step, step]));
|
|
149
|
+
const present = new Set(answer.procedure.map((step) => step.step));
|
|
150
|
+
const kept = GROUPS.filter((group) => group.steps.some((step) => present.has(step)));
|
|
151
|
+
const keptIds = new Set(kept.map((group) => group.id));
|
|
152
|
+
return kept.map((group) => {
|
|
153
|
+
const steps = group.steps.map((step) => byStep.get(step)).filter((step) => step !== undefined);
|
|
154
|
+
const central = steps.find((step) => step.step === "differential_expression") ?? steps.find((step) => step.method !== undefined) ?? steps[0];
|
|
155
|
+
const flags = steps.flatMap((step) => step.flags ?? []);
|
|
156
|
+
const hardFlag = flags.find((flag) => flag.severity === "flag");
|
|
157
|
+
const claims = [...new Set(steps.flatMap((step) => step.rules))];
|
|
158
|
+
const packages = [
|
|
159
|
+
...new Set(steps.map((step) => step.package?.name).filter((name) => name !== undefined)),
|
|
160
|
+
];
|
|
161
|
+
const caveats = [...flags.filter((flag) => flag.severity === "warn").map((flag) => flag.message), ...steps.flatMap(stepCaveats)];
|
|
162
|
+
const method = central?.method;
|
|
163
|
+
// A rule covers the group when a step of the group carries one. A method
|
|
164
|
+
// is not necessary: the report step carries the reporting rules and no
|
|
165
|
+
// method, and it is grounded by those rules.
|
|
166
|
+
const covered = method !== undefined || claims.length > 0;
|
|
167
|
+
const status = hardFlag ? "flagged" : covered ? "grounded" : "ungrounded";
|
|
168
|
+
const name = method && group.id !== "T1S1" && group.id !== "T1S3" ? method.label : group.name;
|
|
169
|
+
const alternatives = (central?.alternatives ?? []).map(({ method: id, label, when, rules }) => ({ method: id, label, when, rules }));
|
|
170
|
+
// One dispute per skeleton step: the central step first, else the first folded step that carries one.
|
|
171
|
+
const disputed = central?.disputed ?? steps.find((step) => step.disputed !== undefined)?.disputed;
|
|
172
|
+
const forbids = [...new Set(steps.flatMap(forbidsOf))];
|
|
173
|
+
return {
|
|
174
|
+
id: group.id,
|
|
175
|
+
name,
|
|
176
|
+
track: group.track,
|
|
177
|
+
step_type: group.step_type,
|
|
178
|
+
agent: group.agent,
|
|
179
|
+
packages,
|
|
180
|
+
depends_on: dependsOn(group.id, keptIds),
|
|
181
|
+
caveats: hardFlag ? [hardFlag.message, ...caveats] : caveats,
|
|
182
|
+
alternatives,
|
|
183
|
+
...(disputed ? { disputed: { rule: disputed.rule, sides: disputed.sides } } : {}),
|
|
184
|
+
forbids,
|
|
185
|
+
...(central?.environment ? { environment: central.environment } : {}),
|
|
186
|
+
grounding: {
|
|
187
|
+
status,
|
|
188
|
+
snapshot: answer.snapshot.digest,
|
|
189
|
+
claims,
|
|
190
|
+
...(central?.template ? { template: central.template } : {}),
|
|
191
|
+
settings: stepSettings(steps),
|
|
192
|
+
reason: hardFlag
|
|
193
|
+
? `flagged by ${hardFlag.rule}`
|
|
194
|
+
: method
|
|
195
|
+
? `${method.label} per ${claims[0] ?? "the procedure"}`
|
|
196
|
+
: covered
|
|
197
|
+
? `${group.name} per ${claims[0]}`
|
|
198
|
+
: "no rule covers this step",
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
});
|
|
202
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `knowledge_template` — the model emits slot values, and the tool writes the
|
|
3
|
+
* rendered script and the decision record into the step workspace through
|
|
4
|
+
* the same `WorkspaceMutator` as `write_file`. The script is never output tokens.
|
|
5
|
+
*
|
|
6
|
+
* The installed versions ride from the tool, not from the model: the tool
|
|
7
|
+
* reads the `inflexa.lock` of the farm and the `image-packages.json` of the
|
|
8
|
+
* store when the host names them (`installedPackages`), and the service
|
|
9
|
+
* answers with the environment match, which the decision record keeps.
|
|
10
|
+
*
|
|
11
|
+
* The plan settings ride from the host, not from the model: the step input
|
|
12
|
+
* carries a `TemplateBinding` when the plan step grounds on a template, and
|
|
13
|
+
* the tool merges the bound slots under the model values. A model value that
|
|
14
|
+
* differs from a bound value is refused before the service call unless an
|
|
15
|
+
* `overrides` entry names the slot with a reason, and the decision record
|
|
16
|
+
* keeps the bound slots and the overrides beside the script path.
|
|
17
|
+
*
|
|
18
|
+
* The facts of the machine never ride at all: a local slot of the template
|
|
19
|
+
* (a path, a column name, a level label, a design formula) is split off the
|
|
20
|
+
* request, the service renders it as its marker, and the tool binds the
|
|
21
|
+
* value on the machine before it writes the file. The request carries the
|
|
22
|
+
* step, its claims, and the release digest the plan pinned, thus the record
|
|
23
|
+
* binds the cited decision to the bytes, and a service on another release
|
|
24
|
+
* refuses the render instead of moving the plan in silence. The call runs as
|
|
25
|
+
* one durable step, thus a replay returns the cached result and renders
|
|
26
|
+
* nothing again.
|
|
27
|
+
*/
|
|
28
|
+
import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
|
|
29
|
+
import { type WithToolCallRecord } from "../define-tool.js";
|
|
30
|
+
import type { WorkspaceMutator, WriteFileResult, WriteRecord } from "../workspace/mutator.js";
|
|
31
|
+
import type { KnowledgeClient, KnowledgeRejected, KnowledgeSnapshotMismatch, KnowledgeUnavailable, TemplateParameter } from "./client.js";
|
|
32
|
+
/** A slot value the plan binds: a scalar, or a list of strings. JSON-serialisable, as the durable step input needs. */
|
|
33
|
+
export type TemplateBindingValue = string | number | boolean | readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* The plan settings bound to the template of one step, composed by the host
|
|
36
|
+
* at dispatch and carried on the durable step input. `slots` holds the bound
|
|
37
|
+
* value per slot name, and `sources` the source of each value (a doi, a
|
|
38
|
+
* document, or the plan) under the same name. The rest binds the render to
|
|
39
|
+
* the plan step: its id, its claims, the release the plan pinned, and the
|
|
40
|
+
* local slots of the contract that the tool binds on the machine.
|
|
41
|
+
*/
|
|
42
|
+
export interface TemplateBinding {
|
|
43
|
+
/** The template reference of the plan step, with its version, for example `tpl-deseq2-two-group@1.0.0`. */
|
|
44
|
+
readonly template: string;
|
|
45
|
+
readonly slots: Readonly<Record<string, TemplateBindingValue>>;
|
|
46
|
+
readonly sources: Readonly<Record<string, string>>;
|
|
47
|
+
/** The id of the plan step. */
|
|
48
|
+
readonly step?: string;
|
|
49
|
+
/** The claims of the plan step, as `knowledge_recommend` returned them. */
|
|
50
|
+
readonly claims?: readonly string[];
|
|
51
|
+
/** The release digest the plan pinned. Absent when the step is ungrounded. */
|
|
52
|
+
readonly snapshot?: string;
|
|
53
|
+
/** The local slots of the served contract. Absent on a binding made before the contract carried the flag. */
|
|
54
|
+
readonly local?: readonly TemplateParameter[];
|
|
55
|
+
/** The names of every adaptable slot of the served contract, thus the tool refuses an unknown name before anything leaves the machine. */
|
|
56
|
+
readonly adaptable?: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
export interface KnowledgeTemplateDeps {
|
|
59
|
+
readonly client: KnowledgeClient;
|
|
60
|
+
readonly mutator: WorkspaceMutator;
|
|
61
|
+
/** Host path of the farm `inflexa.lock`, or its resolver for each analysis. Absent with no image record, the environment match reads as unknown. */
|
|
62
|
+
readonly farmLockFile?: EnvironmentStorePaths["farmLockFile"];
|
|
63
|
+
/** Host path of the `image-packages.json` of the store: the packages the sandbox image ships, among them the R packages of the R runtime. */
|
|
64
|
+
readonly imagePackagesFile?: string;
|
|
65
|
+
/** The plan settings bound to the template of the step. Absent, the model values ride alone. */
|
|
66
|
+
readonly binding?: TemplateBinding;
|
|
67
|
+
}
|
|
68
|
+
/** One bound slot as the decision record keeps it. */
|
|
69
|
+
export interface BoundSlotRecord {
|
|
70
|
+
readonly slot: string;
|
|
71
|
+
readonly value: TemplateBindingValue;
|
|
72
|
+
readonly source: string;
|
|
73
|
+
}
|
|
74
|
+
/** One change of a bound value that an `overrides` entry declared, as the decision record keeps it. */
|
|
75
|
+
export interface SettingsOverrideRecord {
|
|
76
|
+
readonly slot: string;
|
|
77
|
+
readonly plan_value: TemplateBindingValue;
|
|
78
|
+
readonly new_value: unknown;
|
|
79
|
+
readonly reason: string;
|
|
80
|
+
}
|
|
81
|
+
export type KnowledgeTemplateOutput = {
|
|
82
|
+
readonly status: "ok";
|
|
83
|
+
readonly script_path: string;
|
|
84
|
+
readonly decision_record_path: string;
|
|
85
|
+
readonly template: {
|
|
86
|
+
readonly id: string;
|
|
87
|
+
readonly version: string;
|
|
88
|
+
readonly label: string;
|
|
89
|
+
readonly method: {
|
|
90
|
+
readonly id: string;
|
|
91
|
+
readonly label: string;
|
|
92
|
+
};
|
|
93
|
+
readonly substitute_for?: {
|
|
94
|
+
readonly id: string;
|
|
95
|
+
readonly label: string;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
readonly snapshot: {
|
|
99
|
+
readonly date: string;
|
|
100
|
+
readonly digest: string;
|
|
101
|
+
};
|
|
102
|
+
readonly slots: readonly {
|
|
103
|
+
readonly name: string;
|
|
104
|
+
readonly value?: unknown;
|
|
105
|
+
readonly source: string;
|
|
106
|
+
readonly adaptable: boolean;
|
|
107
|
+
readonly lines: readonly number[];
|
|
108
|
+
}[];
|
|
109
|
+
/** The local slots the tool bound on this machine. */
|
|
110
|
+
readonly local_slots: readonly string[];
|
|
111
|
+
/** The digest of the script as written. */
|
|
112
|
+
readonly written_sha256: string;
|
|
113
|
+
readonly environment_match: string;
|
|
114
|
+
readonly syntax: string;
|
|
115
|
+
readonly expected_outputs: readonly {
|
|
116
|
+
readonly name: string;
|
|
117
|
+
readonly path: string;
|
|
118
|
+
readonly description?: string;
|
|
119
|
+
}[];
|
|
120
|
+
readonly run_with: string;
|
|
121
|
+
} | {
|
|
122
|
+
readonly status: "write_refused";
|
|
123
|
+
readonly path: string;
|
|
124
|
+
readonly reason: Exclude<WriteFileResult["status"], "ok">;
|
|
125
|
+
} | KnowledgeUnavailable | KnowledgeRejected | KnowledgeSnapshotMismatch;
|
|
126
|
+
/** The result of a `knowledge_template` call: each landed write rides as the call record, for the fold. */
|
|
127
|
+
type KnowledgeTemplateResult = KnowledgeTemplateOutput | WithToolCallRecord<KnowledgeTemplateOutput, readonly WriteRecord[]>;
|
|
128
|
+
export declare function createKnowledgeTemplateTool(deps: KnowledgeTemplateDeps): import("../define-tool.js").Tool<{
|
|
129
|
+
template: string;
|
|
130
|
+
slots: Record<string, unknown>;
|
|
131
|
+
overrides?: {
|
|
132
|
+
slot: string;
|
|
133
|
+
reason: string;
|
|
134
|
+
}[] | undefined;
|
|
135
|
+
script_name?: string | undefined;
|
|
136
|
+
}, KnowledgeTemplateResult>;
|
|
137
|
+
export {};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `knowledge_template` — the model emits slot values, and the tool writes the
|
|
3
|
+
* rendered script and the decision record into the step workspace through
|
|
4
|
+
* the same `WorkspaceMutator` as `write_file`. The script is never output tokens.
|
|
5
|
+
*
|
|
6
|
+
* The installed versions ride from the tool, not from the model: the tool
|
|
7
|
+
* reads the `inflexa.lock` of the farm and the `image-packages.json` of the
|
|
8
|
+
* store when the host names them (`installedPackages`), and the service
|
|
9
|
+
* answers with the environment match, which the decision record keeps.
|
|
10
|
+
*
|
|
11
|
+
* The plan settings ride from the host, not from the model: the step input
|
|
12
|
+
* carries a `TemplateBinding` when the plan step grounds on a template, and
|
|
13
|
+
* the tool merges the bound slots under the model values. A model value that
|
|
14
|
+
* differs from a bound value is refused before the service call unless an
|
|
15
|
+
* `overrides` entry names the slot with a reason, and the decision record
|
|
16
|
+
* keeps the bound slots and the overrides beside the script path.
|
|
17
|
+
*
|
|
18
|
+
* The facts of the machine never ride at all: a local slot of the template
|
|
19
|
+
* (a path, a column name, a level label, a design formula) is split off the
|
|
20
|
+
* request, the service renders it as its marker, and the tool binds the
|
|
21
|
+
* value on the machine before it writes the file. The request carries the
|
|
22
|
+
* step, its claims, and the release digest the plan pinned, thus the record
|
|
23
|
+
* binds the cited decision to the bytes, and a service on another release
|
|
24
|
+
* refuses the render instead of moving the plan in silence. The call runs as
|
|
25
|
+
* one durable step, thus a replay returns the cached result and renders
|
|
26
|
+
* nothing again.
|
|
27
|
+
*/
|
|
28
|
+
import { ok } from "neverthrow";
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
import { defineTool, withToolCallRecord } from "../define-tool.js";
|
|
31
|
+
import { decisionRecordPath, scriptSha256 } from "../workspace/decision-record.js";
|
|
32
|
+
import { installedPackages } from "./environment.js";
|
|
33
|
+
import { bindLocalSlots, localParameters, validateLocalSlot } from "./local-slots.js";
|
|
34
|
+
const TEMPLATE_REF = /^tpl-[a-z0-9-]+(@\d+\.\d+\.\d+)?$/;
|
|
35
|
+
/** A model value equals a bound value when the scalars are identical, or when two lists hold the same strings in order. */
|
|
36
|
+
function sameValue(bound, value) {
|
|
37
|
+
if (Array.isArray(bound)) {
|
|
38
|
+
return Array.isArray(value) && value.length === bound.length && bound.every((item, index) => item === value[index]);
|
|
39
|
+
}
|
|
40
|
+
return bound === value;
|
|
41
|
+
}
|
|
42
|
+
/** The source of a bound slot as the issue names it. A binding without a source for the slot reads as the plan. */
|
|
43
|
+
function sourceOf(binding, slot) {
|
|
44
|
+
return binding.sources[slot] ?? "plan";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The refusal of a render request against the binding, or `undefined` when
|
|
48
|
+
* the request obeys it. A different template reference is one issue on the
|
|
49
|
+
* field `template`. Each bound slot whose model value differs without an
|
|
50
|
+
* override is one issue that names the slot, the bound value, and its source.
|
|
51
|
+
*/
|
|
52
|
+
function refusedByBinding(binding, template, slots, overridden) {
|
|
53
|
+
if (template !== binding.template) {
|
|
54
|
+
const message = `the plan binds the template ${binding.template}; this step renders that reference only`;
|
|
55
|
+
return { match: "rejected", message, issues: [{ field: "template", message, permitted: [binding.template] }] };
|
|
56
|
+
}
|
|
57
|
+
const issues = [];
|
|
58
|
+
for (const [slot, bound] of Object.entries(binding.slots)) {
|
|
59
|
+
const value = slots[slot];
|
|
60
|
+
if (value === undefined || sameValue(bound, value) || overridden.has(slot))
|
|
61
|
+
continue;
|
|
62
|
+
const shown = JSON.stringify(bound);
|
|
63
|
+
issues.push({
|
|
64
|
+
slot,
|
|
65
|
+
reason: "bound by the plan",
|
|
66
|
+
message: `the plan binds ${slot} = ${shown} (${sourceOf(binding, slot)}); send that value, or name the slot in overrides with the reason for the change`,
|
|
67
|
+
permitted: [shown],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (issues.length === 0)
|
|
71
|
+
return undefined;
|
|
72
|
+
return { match: "rejected", message: "one or more slot values differ from the plan settings without an override", issues };
|
|
73
|
+
}
|
|
74
|
+
/** A contract answer that is a refusal. The contract itself is a loose object, thus `in` alone cannot tell the two apart. */
|
|
75
|
+
function isContractRefusal(answer) {
|
|
76
|
+
return "match" in answer && (answer.match === "unavailable" || answer.match === "rejected");
|
|
77
|
+
}
|
|
78
|
+
/** The slots of `slots` whose names are in `names`, and the rest, as two records. */
|
|
79
|
+
function splitSlots(slots, names) {
|
|
80
|
+
const local = {};
|
|
81
|
+
const remote = {};
|
|
82
|
+
for (const [name, value] of Object.entries(slots))
|
|
83
|
+
(names.has(name) ? local : remote)[name] = value;
|
|
84
|
+
return { local, remote };
|
|
85
|
+
}
|
|
86
|
+
export function createKnowledgeTemplateTool(deps) {
|
|
87
|
+
return defineTool({
|
|
88
|
+
id: "knowledge_template",
|
|
89
|
+
description: "Render a tested analysis script from a knowledge template and write it into your working directory. " +
|
|
90
|
+
"Use it for a step whose briefing names a template in its Grounding (for example `tpl-deseq2-two-group@1.0.0`). " +
|
|
91
|
+
"The briefing lists the slots of the template and the values the plan binds. " +
|
|
92
|
+
"Send the template id and the slot values only: the file paths of your inputs (absolute `/<analysisId>/...` paths), the column names, the levels of the contrast, and the design. " +
|
|
93
|
+
"A slot the briefing marks local (a path, a column name, a level, a formula) is bound on this machine and never sent to the service; send it as any other slot. " +
|
|
94
|
+
"A bound value rides into the script as it is; send it unchanged, or omit it. To change a bound value, send the new value and add an `overrides` entry with the slot and the reason. A changed value without an override is refused before the service call. " +
|
|
95
|
+
"Prefer the template over a script of your own: it is tested, and it carries the settings of the plan. When the template does not fit the step (a design it does not cover, or an input it cannot read), write the script yourself with `write_file`, and state the reason in your summary. " +
|
|
96
|
+
"The tool writes `scripts/<template>.R` or `scripts/<template>.py` and `output/decision_record_<script>.json`, one record per rendered script (the step, its claims, the template, the snapshot, each slot with its source, the bound slots, the overrides, the environment match, the citations, and the digest of the script as written), then you run the script with `execute_command` using the command in `run_with`. " +
|
|
97
|
+
"A slot value the template refuses comes back as `match: rejected` with the slot and the permitted values; correct it and call again. " +
|
|
98
|
+
"A change the slots do not cover: use `edit_file` on a line marked `# [adaptable: ...]` in the rendered script, and keep the edit small; the record lists each such edit. " +
|
|
99
|
+
"`match: unavailable` means the service did not answer, and `match: snapshot_mismatch` means the service moved to another release than the plan pinned; in both cases write the script yourself as you would without this tool, and state it in your summary.",
|
|
100
|
+
inputSchema: z.object({
|
|
101
|
+
template: z
|
|
102
|
+
.string()
|
|
103
|
+
.regex(TEMPLATE_REF, "a template id, optionally with @version")
|
|
104
|
+
.describe("The template id from the Grounding of the step, with its version, for example `tpl-deseq2-two-group@1.0.0`."),
|
|
105
|
+
slots: z
|
|
106
|
+
.record(z.string(), z.unknown())
|
|
107
|
+
.describe("The slot values, by slot name. Only the adaptable slots of the template; a pinned slot is refused. Strings, numbers, booleans, and lists of strings."),
|
|
108
|
+
overrides: z
|
|
109
|
+
.array(z.object({
|
|
110
|
+
slot: z.string().min(1).describe("The name of the bound slot whose value you change."),
|
|
111
|
+
reason: z.string().min(1).describe("Why the plan value does not hold for this step, in one sentence."),
|
|
112
|
+
}))
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("One entry per bound slot whose value you change from the plan value. Absent when you send every bound value as it is."),
|
|
115
|
+
script_name: z
|
|
116
|
+
.string()
|
|
117
|
+
.regex(/^[A-Za-z0-9_.-]+$/)
|
|
118
|
+
.optional()
|
|
119
|
+
.describe("The file name under `scripts/`. Defaults to `<template id>.R` or `<template id>.py` by the language of the template."),
|
|
120
|
+
}),
|
|
121
|
+
describeCall: ({ template }) => template,
|
|
122
|
+
describeResult: (_input, result) => "status" in result ? (result.status === "ok" ? `${result.script_path} (${result.environment_match})` : result.status) : result.match,
|
|
123
|
+
execute: async ({ template, slots, overrides, script_name }, ctx) => {
|
|
124
|
+
const binding = deps.binding;
|
|
125
|
+
const reasons = new Map((overrides ?? []).map((entry) => [entry.slot, entry.reason]));
|
|
126
|
+
if (binding) {
|
|
127
|
+
const refused = refusedByBinding(binding, template, slots, new Set(reasons.keys()));
|
|
128
|
+
if (refused)
|
|
129
|
+
return ok(refused);
|
|
130
|
+
}
|
|
131
|
+
// The local slots and the adaptable names come from the binding the host composed at dispatch, else from
|
|
132
|
+
// the contract itself. Without them no free-text value can be told from a fact of the machine, and an
|
|
133
|
+
// unknown name could carry one, thus no render happens without them.
|
|
134
|
+
let locals;
|
|
135
|
+
let adaptable;
|
|
136
|
+
if (binding?.local && binding.adaptable) {
|
|
137
|
+
locals = binding.local;
|
|
138
|
+
adaptable = binding.adaptable;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const contract = await deps.client.contract(template);
|
|
142
|
+
if (isContractRefusal(contract))
|
|
143
|
+
return ok(contract);
|
|
144
|
+
locals = localParameters(contract.parameters);
|
|
145
|
+
adaptable = contract.parameters.filter((parameter) => parameter.adaptable).map((parameter) => parameter.name);
|
|
146
|
+
}
|
|
147
|
+
const unknown = Object.keys(slots).filter((name) => !adaptable.includes(name));
|
|
148
|
+
if (unknown.length > 0) {
|
|
149
|
+
return ok({
|
|
150
|
+
match: "rejected",
|
|
151
|
+
message: "one or more slot names are not adaptable slots of this template",
|
|
152
|
+
issues: unknown.map((slot) => ({ slot, reason: "the template has no such adaptable slot", permitted: [...adaptable] })),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const localNames = new Set(locals.map((parameter) => parameter.name));
|
|
156
|
+
const split = splitSlots(slots, localNames);
|
|
157
|
+
const boundSplit = splitSlots(binding?.slots ?? {}, localNames);
|
|
158
|
+
const merged = { ...boundSplit.remote, ...split.remote };
|
|
159
|
+
// A bound local slot is bound on the machine like a model value, under the model value.
|
|
160
|
+
const localValues = { ...boundSplit.local, ...split.local };
|
|
161
|
+
const localByName = new Map(locals.map((parameter) => [parameter.name, parameter]));
|
|
162
|
+
// A local value the contract refuses is refused before the service call, thus a bad path costs no render.
|
|
163
|
+
const localIssues = Object.entries(localValues).flatMap(([name, value]) => {
|
|
164
|
+
const issue = validateLocalSlot(localByName.get(name), value);
|
|
165
|
+
return issue ? [issue] : [];
|
|
166
|
+
});
|
|
167
|
+
if (localIssues.length > 0) {
|
|
168
|
+
return ok({ match: "rejected", message: "one or more local slot values are not valid for this template", issues: localIssues });
|
|
169
|
+
}
|
|
170
|
+
const farmLockFile = typeof deps.farmLockFile === "function" ? deps.farmLockFile(ctx.session.scope.analysisId) : deps.farmLockFile;
|
|
171
|
+
const installed = installedPackages({
|
|
172
|
+
...(farmLockFile ? { farmLockFile } : {}),
|
|
173
|
+
...(deps.imagePackagesFile ? { imagePackagesFile: deps.imagePackagesFile } : {}),
|
|
174
|
+
});
|
|
175
|
+
const answer = await deps.client.render(template, merged, installed.packages, {
|
|
176
|
+
...(binding?.step ? { step: binding.step } : {}),
|
|
177
|
+
...(binding?.claims ? { claims: binding.claims } : {}),
|
|
178
|
+
...(binding?.snapshot ? { expectedSnapshot: binding.snapshot } : {}),
|
|
179
|
+
});
|
|
180
|
+
// A rendered answer carries `ok: true`; the refusals carry `match` and no `ok`.
|
|
181
|
+
if (!("ok" in answer))
|
|
182
|
+
return ok(answer);
|
|
183
|
+
const bound = bindLocalSlots({ language: answer.template.language, parameters: locals }, answer.script, localValues, answer.slots);
|
|
184
|
+
if (!bound.ok) {
|
|
185
|
+
return ok({ match: "rejected", message: "one or more local slot values are not valid for this template", issues: bound.issues });
|
|
186
|
+
}
|
|
187
|
+
const scriptFile = script_name ?? `${answer.template.id}.${answer.template.language === "R" ? "R" : "py"}`;
|
|
188
|
+
const scriptPath = `scripts/${scriptFile}`;
|
|
189
|
+
// Each landed write rides the call record, also when a later write is refused.
|
|
190
|
+
const records = [];
|
|
191
|
+
const write = async (path, content) => {
|
|
192
|
+
const outcome = await deps.mutator.writeFile({
|
|
193
|
+
path,
|
|
194
|
+
content,
|
|
195
|
+
toolName: "knowledge_template",
|
|
196
|
+
invocationId: ctx.invocationId,
|
|
197
|
+
session: ctx.session,
|
|
198
|
+
});
|
|
199
|
+
if (outcome.status === "ok")
|
|
200
|
+
records.push(outcome.record);
|
|
201
|
+
return outcome;
|
|
202
|
+
};
|
|
203
|
+
const scriptWrite = await write(scriptPath, bound.script);
|
|
204
|
+
if (scriptWrite.status !== "ok")
|
|
205
|
+
return ok({ status: "write_refused", path: scriptWrite.path, reason: scriptWrite.status });
|
|
206
|
+
const boundSlots = Object.entries(binding?.slots ?? {}).map(([slot, value]) => ({
|
|
207
|
+
slot,
|
|
208
|
+
value: value,
|
|
209
|
+
source: binding ? sourceOf(binding, slot) : "plan",
|
|
210
|
+
}));
|
|
211
|
+
const settingsOverrides = boundSlots.flatMap(({ slot, value }) => {
|
|
212
|
+
const reason = reasons.get(slot);
|
|
213
|
+
const sent = slots[slot];
|
|
214
|
+
if (reason === undefined || sent === undefined || sameValue(value, sent))
|
|
215
|
+
return [];
|
|
216
|
+
return [{ slot, plan_value: value, new_value: sent, reason }];
|
|
217
|
+
});
|
|
218
|
+
// The slot report of the record: the service entries, with each local marker entry replaced by the slot as
|
|
219
|
+
// bound here. An optional local slot with no value has no entry, as in a full render.
|
|
220
|
+
const boundByName = new Map(bound.slots.map((slot) => [slot.name, slot]));
|
|
221
|
+
const reportedSlots = answer.slots.flatMap((slot) => {
|
|
222
|
+
if (slot.source !== "local")
|
|
223
|
+
return [slot];
|
|
224
|
+
const local = boundByName.get(slot.name);
|
|
225
|
+
return local ? [local] : [];
|
|
226
|
+
});
|
|
227
|
+
const record = {
|
|
228
|
+
...answer.decision_record,
|
|
229
|
+
slots: reportedSlots,
|
|
230
|
+
script_path: scriptWrite.path,
|
|
231
|
+
written_sha256: scriptSha256(bound.script),
|
|
232
|
+
bound_slots: boundSlots,
|
|
233
|
+
settings_overrides: settingsOverrides,
|
|
234
|
+
};
|
|
235
|
+
const recordWrite = await write(decisionRecordPath(scriptFile), `${JSON.stringify(record, null, 2)}\n`);
|
|
236
|
+
if (recordWrite.status !== "ok") {
|
|
237
|
+
return ok(withToolCallRecord({ status: "write_refused", path: recordWrite.path, reason: recordWrite.status }, records));
|
|
238
|
+
}
|
|
239
|
+
return ok(withToolCallRecord({
|
|
240
|
+
status: "ok",
|
|
241
|
+
script_path: scriptWrite.path,
|
|
242
|
+
decision_record_path: recordWrite.path,
|
|
243
|
+
template: {
|
|
244
|
+
id: answer.template.id,
|
|
245
|
+
version: answer.template.version,
|
|
246
|
+
label: answer.template.label,
|
|
247
|
+
method: { id: answer.template.method.id, label: answer.template.method.label },
|
|
248
|
+
...(answer.template.substitute_for
|
|
249
|
+
? { substitute_for: { id: answer.template.substitute_for.id, label: answer.template.substitute_for.label } }
|
|
250
|
+
: {}),
|
|
251
|
+
},
|
|
252
|
+
snapshot: answer.snapshot,
|
|
253
|
+
slots: reportedSlots,
|
|
254
|
+
local_slots: bound.slots.map((slot) => slot.name),
|
|
255
|
+
written_sha256: record.written_sha256,
|
|
256
|
+
environment_match: answer.environment.match,
|
|
257
|
+
syntax: answer.syntax.status,
|
|
258
|
+
expected_outputs: answer.outputs,
|
|
259
|
+
run_with: `${answer.template.language === "R" ? "Rscript" : "python3"} ${scriptPath}`,
|
|
260
|
+
}, records));
|
|
261
|
+
},
|
|
262
|
+
foldCallRecord: (records) => {
|
|
263
|
+
for (const record of records)
|
|
264
|
+
deps.mutator.recordWrite(record);
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
}
|
|
@@ -30,6 +30,7 @@ import type { UsageRecorder } from "../../billing/usage-recorder.js";
|
|
|
30
30
|
import { type Tool } from "../define-tool.js";
|
|
31
31
|
import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
|
|
32
32
|
import { type BioToolKeys } from "../bio/keys.js";
|
|
33
|
+
import { type KnowledgeClient, type KnowledgeRecommendAnswer } from "../knowledge/index.js";
|
|
33
34
|
import { type ResourcePolicy } from "../../config/resource-limits.js";
|
|
34
35
|
import type { Logger } from "../../lib/logger.js";
|
|
35
36
|
export interface GeneratePlanDeps extends EnvironmentStorePaths {
|
|
@@ -58,6 +59,28 @@ export interface GeneratePlanDeps extends EnvironmentStorePaths {
|
|
|
58
59
|
readonly toolOutputStore?: ToolOutputStore;
|
|
59
60
|
/** API keys for the search tools the planner uses to ground a plan. */
|
|
60
61
|
readonly bioKeys: BioToolKeys;
|
|
62
|
+
/**
|
|
63
|
+
* The knowledge service client. Bound, the planner gains `knowledge_recommend`
|
|
64
|
+
* and `knowledge_check`. Absent, no knowledge tool attaches and no
|
|
65
|
+
* description of one enters the context, which is the default state of
|
|
66
|
+
* the open-source host.
|
|
67
|
+
*/
|
|
68
|
+
readonly knowledge?: KnowledgeClient;
|
|
61
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* The search tools of the planner — built one time, not per invocation.
|
|
72
|
+
*
|
|
73
|
+
* The set answers the two questions that a seed cannot. "What did a study of
|
|
74
|
+
* this kind do before?" reaches the public dataset records and the literature.
|
|
75
|
+
* "What does this package actually give me?" reaches the developer docs. The
|
|
76
|
+
* planner reads its reference and package inventories from the seed, thus the
|
|
77
|
+
* docs pair is what turns a package name into a real function.
|
|
78
|
+
*
|
|
79
|
+
* A tool here never writes and never computes. Thus the worst outcome of a
|
|
80
|
+
* needless call is latency, and the prompt is what bounds that.
|
|
81
|
+
*/
|
|
82
|
+
export declare function buildPlannerSearchTools(deps: GeneratePlanDeps, hooks?: {
|
|
83
|
+
readonly onRecommend?: (answer: KnowledgeRecommendAnswer) => void;
|
|
84
|
+
}): Tool[];
|
|
62
85
|
/** Build the `generate_plan` tool bound to its provider and pool. */
|
|
63
86
|
export declare function createGeneratePlanTool(deps: GeneratePlanDeps): Tool;
|