@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,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment join of a procedure: which package of each step the farm
|
|
3
|
+
* holds, at which version, and which collection of the reference store the
|
|
4
|
+
* enrichment step names. The service knows the methods and the collections
|
|
5
|
+
* by name; the host knows what is installed. Joined here, the answer of
|
|
6
|
+
* `knowledge_recommend` carries the environment, and a planner has no reason
|
|
7
|
+
* to call the listing tools for the packages and the collection it was told
|
|
8
|
+
* to use. A model that hunts for what it already has was the cost of the
|
|
9
|
+
* Phase 0 campaigns on a small model.
|
|
10
|
+
*
|
|
11
|
+
* The installed packages come from two records the host reads, never from a
|
|
12
|
+
* list in the code: the `inflexa.lock` of the farm names the packages the
|
|
13
|
+
* store links, and the `image-packages.json` of the store names what the
|
|
14
|
+
* sandbox image itself ships, among them the R packages of the R runtime
|
|
15
|
+
* (`stats`, `survival`, `MASS`), which no farm lock lists. Both records are
|
|
16
|
+
* written by their producers and validated here.
|
|
17
|
+
*
|
|
18
|
+
* Absence is data: a package neither record names reads `present: false`,
|
|
19
|
+
* and a collection the store lacks reads `present: false`. The join never
|
|
20
|
+
* fills a gap and never names a path it did not read.
|
|
21
|
+
*/
|
|
22
|
+
import type { FarmPackage, RecommendResponse } from "./client.js";
|
|
23
|
+
export interface EnvironmentPaths {
|
|
24
|
+
/** Host path of the farm `inflexa.lock`. Absent, the packages read as unknown. */
|
|
25
|
+
readonly farmLockFile?: string;
|
|
26
|
+
/** Host path of the `image-packages.json` of the store. Absent, the packages the image ships are unknown. */
|
|
27
|
+
readonly imagePackagesFile?: string;
|
|
28
|
+
/** Host path of the reference store. Absent, the collection reads as unknown. */
|
|
29
|
+
readonly refStorePath?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface StepEnvironment {
|
|
32
|
+
readonly package?: {
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly present: boolean;
|
|
35
|
+
readonly version?: string;
|
|
36
|
+
};
|
|
37
|
+
readonly collection?: {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly present: boolean;
|
|
40
|
+
readonly path?: string;
|
|
41
|
+
readonly title?: string;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** Where each fact of the environment came from: a record the host read, or nothing. */
|
|
45
|
+
export interface EnvironmentSource {
|
|
46
|
+
readonly farm: "lock" | "unknown";
|
|
47
|
+
/** `record` when the store carries the `image-packages.json` of the sandbox image. */
|
|
48
|
+
readonly image: "record" | "unknown";
|
|
49
|
+
readonly references: "store" | "unknown";
|
|
50
|
+
}
|
|
51
|
+
/** The recommend answer with an `environment` per step. */
|
|
52
|
+
export type RecommendWithEnvironment = RecommendResponse & {
|
|
53
|
+
readonly environment_source?: EnvironmentSource;
|
|
54
|
+
readonly procedure: readonly (RecommendResponse["procedure"][number] & {
|
|
55
|
+
readonly environment?: StepEnvironment;
|
|
56
|
+
})[];
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The packages installed in the sandbox, as the two records report them, with
|
|
60
|
+
* one entry per name. The farm lock comes first and it wins a collision,
|
|
61
|
+
* because a farm that links its own copy of a package is what a step loads.
|
|
62
|
+
* `packages` is absent when neither record is readable, thus a caller sends
|
|
63
|
+
* nothing to the knowledge service rather than an empty farm, and the answer
|
|
64
|
+
* reads `unknown` instead of "absent".
|
|
65
|
+
*
|
|
66
|
+
* Of the image record only the R runtime track joins the list. The conda
|
|
67
|
+
* tools and the Node packages of the image are not of the package tracks a
|
|
68
|
+
* template pins, and a name of one of them must not answer for a package.
|
|
69
|
+
*
|
|
70
|
+
* The same list serves the environment join of the recommend answer and the
|
|
71
|
+
* environment match of a rendered template, thus the two agree.
|
|
72
|
+
*/
|
|
73
|
+
export declare function installedPackages(paths: Pick<EnvironmentPaths, "farmLockFile" | "imagePackagesFile">): {
|
|
74
|
+
readonly packages?: readonly FarmPackage[];
|
|
75
|
+
readonly farm: EnvironmentSource["farm"];
|
|
76
|
+
readonly image: EnvironmentSource["image"];
|
|
77
|
+
};
|
|
78
|
+
export declare function joinEnvironment(answer: RecommendResponse, paths: EnvironmentPaths): Promise<RecommendWithEnvironment>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The environment join of a procedure: which package of each step the farm
|
|
3
|
+
* holds, at which version, and which collection of the reference store the
|
|
4
|
+
* enrichment step names. The service knows the methods and the collections
|
|
5
|
+
* by name; the host knows what is installed. Joined here, the answer of
|
|
6
|
+
* `knowledge_recommend` carries the environment, and a planner has no reason
|
|
7
|
+
* to call the listing tools for the packages and the collection it was told
|
|
8
|
+
* to use. A model that hunts for what it already has was the cost of the
|
|
9
|
+
* Phase 0 campaigns on a small model.
|
|
10
|
+
*
|
|
11
|
+
* The installed packages come from two records the host reads, never from a
|
|
12
|
+
* list in the code: the `inflexa.lock` of the farm names the packages the
|
|
13
|
+
* store links, and the `image-packages.json` of the store names what the
|
|
14
|
+
* sandbox image itself ships, among them the R packages of the R runtime
|
|
15
|
+
* (`stats`, `survival`, `MASS`), which no farm lock lists. Both records are
|
|
16
|
+
* written by their producers and validated here.
|
|
17
|
+
*
|
|
18
|
+
* Absence is data: a package neither record names reads `present: false`,
|
|
19
|
+
* and a collection the store lacks reads `present: false`. The join never
|
|
20
|
+
* fills a gap and never names a path it did not read.
|
|
21
|
+
*/
|
|
22
|
+
import { readFarmLockFile } from "../../sandbox/farm.js";
|
|
23
|
+
import { readImagePackagesFile } from "../../sandbox/image-packages.js";
|
|
24
|
+
import { readReferenceInventory } from "../sandbox/list-available-refs.js";
|
|
25
|
+
/**
|
|
26
|
+
* The packages installed in the sandbox, as the two records report them, with
|
|
27
|
+
* one entry per name. The farm lock comes first and it wins a collision,
|
|
28
|
+
* because a farm that links its own copy of a package is what a step loads.
|
|
29
|
+
* `packages` is absent when neither record is readable, thus a caller sends
|
|
30
|
+
* nothing to the knowledge service rather than an empty farm, and the answer
|
|
31
|
+
* reads `unknown` instead of "absent".
|
|
32
|
+
*
|
|
33
|
+
* Of the image record only the R runtime track joins the list. The conda
|
|
34
|
+
* tools and the Node packages of the image are not of the package tracks a
|
|
35
|
+
* template pins, and a name of one of them must not answer for a package.
|
|
36
|
+
*
|
|
37
|
+
* The same list serves the environment join of the recommend answer and the
|
|
38
|
+
* environment match of a rendered template, thus the two agree.
|
|
39
|
+
*/
|
|
40
|
+
export function installedPackages(paths) {
|
|
41
|
+
const lock = paths.farmLockFile ? readFarmLockFile(paths.farmLockFile).unwrapOr(undefined) : undefined;
|
|
42
|
+
const record = paths.imagePackagesFile ? readImagePackagesFile(paths.imagePackagesFile).unwrapOr(undefined) : undefined;
|
|
43
|
+
if (!lock && !record)
|
|
44
|
+
return { farm: "unknown", image: "unknown" };
|
|
45
|
+
const packages = [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
for (const pkg of [...(lock?.packages ?? []), ...(record?.r_base ?? [])]) {
|
|
48
|
+
const key = pkg.name.toLowerCase();
|
|
49
|
+
if (seen.has(key))
|
|
50
|
+
continue;
|
|
51
|
+
seen.add(key);
|
|
52
|
+
packages.push({ name: pkg.name, version: pkg.version });
|
|
53
|
+
}
|
|
54
|
+
return { packages, farm: lock ? "lock" : "unknown", image: record ? "record" : "unknown" };
|
|
55
|
+
}
|
|
56
|
+
function tokens(text) {
|
|
57
|
+
return text
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.split(/[^a-z0-9]+/)
|
|
60
|
+
.filter((token) => token.length > 1);
|
|
61
|
+
}
|
|
62
|
+
/** A collection term of the tree (`msigdb_hallmark_human`) matches an entry whose labels hold every token of the term. */
|
|
63
|
+
function findCollection(term, entries) {
|
|
64
|
+
const wanted = tokens(term);
|
|
65
|
+
if (wanted.length === 0)
|
|
66
|
+
return undefined;
|
|
67
|
+
const candidates = entries.filter((entry) => {
|
|
68
|
+
const haystack = tokens([entry.path, entry.metadata?.datasetId ?? "", entry.metadata?.title ?? ""].join(" "));
|
|
69
|
+
return wanted.every((token) => haystack.includes(token));
|
|
70
|
+
});
|
|
71
|
+
// A directory entry names the dataset; a file inside it is the concrete artifact. Prefer the file.
|
|
72
|
+
return candidates.find((entry) => entry.kind === "file") ?? candidates[0];
|
|
73
|
+
}
|
|
74
|
+
export async function joinEnvironment(answer, paths) {
|
|
75
|
+
if (answer.match !== "applicable" && answer.match !== "flag")
|
|
76
|
+
return answer;
|
|
77
|
+
const installed = installedPackages(paths);
|
|
78
|
+
const farm = installed.packages ? new Map(installed.packages.map((pkg) => [pkg.name.toLowerCase(), pkg.version])) : undefined;
|
|
79
|
+
const inventory = paths.refStorePath ? await readReferenceInventory(paths.refStorePath).catch(() => undefined) : undefined;
|
|
80
|
+
const entries = inventory?.available ? inventory.entries : undefined;
|
|
81
|
+
const procedure = answer.procedure.map((step) => {
|
|
82
|
+
const environment = {};
|
|
83
|
+
const pkg = step.package;
|
|
84
|
+
if (pkg && farm) {
|
|
85
|
+
const version = farm.get(pkg.name.toLowerCase());
|
|
86
|
+
environment.package = { name: pkg.name, present: version !== undefined, ...(version !== undefined ? { version } : {}) };
|
|
87
|
+
}
|
|
88
|
+
const collection = step.parameters?.find((parameter) => parameter.name === "gene_set_collection");
|
|
89
|
+
if (collection && typeof collection.value === "string" && entries) {
|
|
90
|
+
const found = findCollection(collection.value, entries);
|
|
91
|
+
environment.collection = {
|
|
92
|
+
name: collection.value,
|
|
93
|
+
present: found !== undefined,
|
|
94
|
+
...(found ? { path: found.path } : {}),
|
|
95
|
+
...(found?.metadata?.title ? { title: found.metadata.title } : {}),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return Object.keys(environment).length > 0 ? { ...step, environment } : step;
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
...answer,
|
|
102
|
+
procedure,
|
|
103
|
+
environment_source: { farm: installed.farm, image: installed.image, references: entries ? "store" : "unknown" },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge barrel — the three tools of the knowledge plane and the client
|
|
3
|
+
* interface they share.
|
|
4
|
+
*
|
|
5
|
+
* `createKnowledgeTools` gives the two planner tools when a client is bound,
|
|
6
|
+
* and nothing when it is not. Absence is the default state of the open-source
|
|
7
|
+
* host, and it is a normal condition: no tool attaches, no description enters
|
|
8
|
+
* the context, and the planner works from the prose skills as it does today.
|
|
9
|
+
* The template tool binds per step, because it needs the mutator of the step.
|
|
10
|
+
*/
|
|
11
|
+
import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
|
|
12
|
+
import type { Tool } from "../define-tool.js";
|
|
13
|
+
import type { KnowledgeClient } from "./client.js";
|
|
14
|
+
import { type KnowledgeRecommendDeps } from "./recommend.js";
|
|
15
|
+
export * from "./client.js";
|
|
16
|
+
export * from "./environment.js";
|
|
17
|
+
export * from "./local-slots.js";
|
|
18
|
+
export * from "./pin.js";
|
|
19
|
+
export * from "./skeleton.js";
|
|
20
|
+
export * from "./check.js";
|
|
21
|
+
export * from "./recommend.js";
|
|
22
|
+
export * from "./situation.js";
|
|
23
|
+
export * from "./template.js";
|
|
24
|
+
export interface KnowledgeToolsDeps {
|
|
25
|
+
readonly client?: KnowledgeClient;
|
|
26
|
+
/** Host path of the farm `inflexa.lock`, or its resolver for each analysis, for the environment join of the recommend answer. */
|
|
27
|
+
readonly farmLockFile?: EnvironmentStorePaths["farmLockFile"];
|
|
28
|
+
/** Host path of the `image-packages.json` of the store, for the same join: the packages the sandbox image ships. */
|
|
29
|
+
readonly imagePackagesFile?: string;
|
|
30
|
+
/** Host path of the reference store, for the same join. */
|
|
31
|
+
readonly refStorePath?: string;
|
|
32
|
+
/** Receives each recommend answer the planner sees; see `KnowledgeRecommendDeps.onAnswer`. */
|
|
33
|
+
readonly onRecommend?: KnowledgeRecommendDeps["onAnswer"];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The planner tools: `knowledge_recommend` and `knowledge_check`, or an empty
|
|
37
|
+
* list with no client. The two share one pin: the first recommend answer
|
|
38
|
+
* fixes the release of the plan, and each later call carries it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createKnowledgeTools(deps: KnowledgeToolsDeps): Tool[];
|
|
41
|
+
/**
|
|
42
|
+
* The digest of the three tool definitions as this host registers them: the
|
|
43
|
+
* id, the description, and the input schema of each, in id order. The
|
|
44
|
+
* snapshot of the service pins its wire contract under its own tool
|
|
45
|
+
* definition hash; this one names what the model saw, and a run records it,
|
|
46
|
+
* thus a changed description or schema is a visible change of the run and
|
|
47
|
+
* not a silent one.
|
|
48
|
+
*/
|
|
49
|
+
export declare function knowledgeToolDefinitionHash(): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge barrel — the three tools of the knowledge plane and the client
|
|
3
|
+
* interface they share.
|
|
4
|
+
*
|
|
5
|
+
* `createKnowledgeTools` gives the two planner tools when a client is bound,
|
|
6
|
+
* and nothing when it is not. Absence is the default state of the open-source
|
|
7
|
+
* host, and it is a normal condition: no tool attaches, no description enters
|
|
8
|
+
* the context, and the planner works from the prose skills as it does today.
|
|
9
|
+
* The template tool binds per step, because it needs the mutator of the step.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { createKnowledgeCheckTool } from "./check.js";
|
|
13
|
+
import { createSnapshotPin } from "./pin.js";
|
|
14
|
+
import { createKnowledgeRecommendTool } from "./recommend.js";
|
|
15
|
+
import { createKnowledgeTemplateTool } from "./template.js";
|
|
16
|
+
export * from "./client.js";
|
|
17
|
+
export * from "./environment.js";
|
|
18
|
+
export * from "./local-slots.js";
|
|
19
|
+
export * from "./pin.js";
|
|
20
|
+
export * from "./skeleton.js";
|
|
21
|
+
export * from "./check.js";
|
|
22
|
+
export * from "./recommend.js";
|
|
23
|
+
export * from "./situation.js";
|
|
24
|
+
export * from "./template.js";
|
|
25
|
+
/**
|
|
26
|
+
* The planner tools: `knowledge_recommend` and `knowledge_check`, or an empty
|
|
27
|
+
* list with no client. The two share one pin: the first recommend answer
|
|
28
|
+
* fixes the release of the plan, and each later call carries it.
|
|
29
|
+
*/
|
|
30
|
+
export function createKnowledgeTools(deps) {
|
|
31
|
+
if (!deps.client)
|
|
32
|
+
return [];
|
|
33
|
+
const pin = createSnapshotPin();
|
|
34
|
+
return [
|
|
35
|
+
createKnowledgeRecommendTool({
|
|
36
|
+
client: deps.client,
|
|
37
|
+
pin,
|
|
38
|
+
...(deps.farmLockFile ? { farmLockFile: deps.farmLockFile } : {}),
|
|
39
|
+
...(deps.imagePackagesFile ? { imagePackagesFile: deps.imagePackagesFile } : {}),
|
|
40
|
+
...(deps.refStorePath ? { refStorePath: deps.refStorePath } : {}),
|
|
41
|
+
...(deps.onRecommend ? { onAnswer: deps.onRecommend } : {}),
|
|
42
|
+
}),
|
|
43
|
+
createKnowledgeCheckTool({ client: deps.client, pin }),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
/** A client that answers nothing: the definitions of the tools do not depend on the answers. */
|
|
47
|
+
const NO_CLIENT = {
|
|
48
|
+
recommend: async () => ({ match: "unavailable", reason: "no service" }),
|
|
49
|
+
check: async () => ({ match: "unavailable", reason: "no service" }),
|
|
50
|
+
render: async () => ({ match: "unavailable", reason: "no service" }),
|
|
51
|
+
contract: async () => ({ match: "unavailable", reason: "no service" }),
|
|
52
|
+
};
|
|
53
|
+
/** A mutator that writes nothing: the definition of the template tool does not depend on the workspace. */
|
|
54
|
+
const NO_MUTATOR = {
|
|
55
|
+
writeFile: async ({ path }) => ({ status: "out_of_scope", path }),
|
|
56
|
+
recordWrite: () => undefined,
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The digest of the three tool definitions as this host registers them: the
|
|
60
|
+
* id, the description, and the input schema of each, in id order. The
|
|
61
|
+
* snapshot of the service pins its wire contract under its own tool
|
|
62
|
+
* definition hash; this one names what the model saw, and a run records it,
|
|
63
|
+
* thus a changed description or schema is a visible change of the run and
|
|
64
|
+
* not a silent one.
|
|
65
|
+
*/
|
|
66
|
+
export function knowledgeToolDefinitionHash() {
|
|
67
|
+
const tools = [...createKnowledgeTools({ client: NO_CLIENT }), createKnowledgeTemplateTool({ client: NO_CLIENT, mutator: NO_MUTATOR })];
|
|
68
|
+
const definitions = tools.map((tool) => ({ id: tool.id, description: tool.description, schema: tool.jsonSchema })).sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
69
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(definitions), "utf8").digest("hex")}`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local slots of a template: the slots whose value is a fact of the
|
|
3
|
+
* machine (a path, a column name, a level label, a design formula). The
|
|
4
|
+
* service renders such a slot as its marker `{{name}}` and never sees its
|
|
5
|
+
* value; the host binds the value here, after the render and before the
|
|
6
|
+
* write, thus the value never leaves the machine.
|
|
7
|
+
*
|
|
8
|
+
* `bindLocalSlots` is a copy of the reference implementation the knowledge
|
|
9
|
+
* service exports (`src/render/render.ts` of the service). The two must give
|
|
10
|
+
* the same bytes: a marked render bound here equals a full render of the
|
|
11
|
+
* service for the same values. Keep the literal rules in step with the
|
|
12
|
+
* service when a slot type is added.
|
|
13
|
+
*/
|
|
14
|
+
import type { TemplateParameter } from "./client.js";
|
|
15
|
+
/** A slot value: a scalar, or a list of strings. */
|
|
16
|
+
export type LocalSlotValue = string | number | boolean | readonly string[];
|
|
17
|
+
/** One refusal of a local value, in the shape of a render rejection issue. */
|
|
18
|
+
export interface LocalSlotIssue {
|
|
19
|
+
readonly slot: string;
|
|
20
|
+
readonly reason: string;
|
|
21
|
+
readonly permitted?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
/** One bound slot as the decision record keeps it: the value, its source, and the lines that carried its marker. */
|
|
24
|
+
export interface BoundLocalSlot {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly value: LocalSlotValue;
|
|
27
|
+
readonly source: "caller" | "default";
|
|
28
|
+
readonly adaptable: true;
|
|
29
|
+
readonly default_source?: string;
|
|
30
|
+
readonly lines: readonly number[];
|
|
31
|
+
/** The `read` flag the service computed for the slot on the marked render, when it has a `when` condition. */
|
|
32
|
+
readonly read?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export type BindLocalSlotsResult = {
|
|
35
|
+
readonly ok: false;
|
|
36
|
+
readonly issues: readonly LocalSlotIssue[];
|
|
37
|
+
} | {
|
|
38
|
+
readonly ok: true;
|
|
39
|
+
readonly script: string;
|
|
40
|
+
readonly slots: readonly BoundLocalSlot[];
|
|
41
|
+
};
|
|
42
|
+
/** The local slots of a contract, in the order the template declares them. */
|
|
43
|
+
export declare function localParameters(parameters: readonly TemplateParameter[]): TemplateParameter[];
|
|
44
|
+
/** Check one caller value against the contract of its slot, as the renderer of the service does. */
|
|
45
|
+
export declare function validateLocalSlot(parameter: TemplateParameter, value: unknown): LocalSlotIssue | undefined;
|
|
46
|
+
/** The marker of a slot as the service leaves it in a marked render. */
|
|
47
|
+
export declare function slotMarker(name: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Bind the local values into a marked script. Each local marker takes the
|
|
50
|
+
* literal of its value, else of the default of the slot, else the absent
|
|
51
|
+
* literal when the slot is optional. A required local slot with no value
|
|
52
|
+
* and no default is an issue, and so is a value the contract refuses. The
|
|
53
|
+
* report names the source of each bound slot and keeps the `read` flag of
|
|
54
|
+
* the marked report of the service.
|
|
55
|
+
*/
|
|
56
|
+
export declare function bindLocalSlots(template: {
|
|
57
|
+
readonly language: string;
|
|
58
|
+
readonly parameters: readonly TemplateParameter[];
|
|
59
|
+
}, script: string, localValues: Readonly<Record<string, unknown>>, marked?: readonly {
|
|
60
|
+
readonly name: string;
|
|
61
|
+
readonly read?: boolean;
|
|
62
|
+
}[]): BindLocalSlotsResult;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local slots of a template: the slots whose value is a fact of the
|
|
3
|
+
* machine (a path, a column name, a level label, a design formula). The
|
|
4
|
+
* service renders such a slot as its marker `{{name}}` and never sees its
|
|
5
|
+
* value; the host binds the value here, after the render and before the
|
|
6
|
+
* write, thus the value never leaves the machine.
|
|
7
|
+
*
|
|
8
|
+
* `bindLocalSlots` is a copy of the reference implementation the knowledge
|
|
9
|
+
* service exports (`src/render/render.ts` of the service). The two must give
|
|
10
|
+
* the same bytes: a marked render bound here equals a full render of the
|
|
11
|
+
* service for the same values. Keep the literal rules in step with the
|
|
12
|
+
* service when a slot type is added.
|
|
13
|
+
*/
|
|
14
|
+
/** The local slots of a contract, in the order the template declares them. */
|
|
15
|
+
export function localParameters(parameters) {
|
|
16
|
+
return parameters.filter((parameter) => parameter.local === true);
|
|
17
|
+
}
|
|
18
|
+
const FORMULA_PATTERN = /^~\s*[A-Za-z0-9_.+*:()\s-]+$/;
|
|
19
|
+
/** Check one caller value against the contract of its slot, as the renderer of the service does. */
|
|
20
|
+
export function validateLocalSlot(parameter, value) {
|
|
21
|
+
const slot = parameter.name;
|
|
22
|
+
const permitted = parameter.enum;
|
|
23
|
+
switch (parameter.type) {
|
|
24
|
+
case "string":
|
|
25
|
+
case "formula": {
|
|
26
|
+
if (typeof value !== "string" || value.length === 0)
|
|
27
|
+
return { slot, reason: "a non-empty string is required" };
|
|
28
|
+
if (parameter.type === "formula" && !FORMULA_PATTERN.test(value))
|
|
29
|
+
return { slot, reason: "a formula must start with ~ and hold only names, +, :, *, and parentheses" };
|
|
30
|
+
if (permitted && !permitted.includes(value))
|
|
31
|
+
return { slot, reason: `the value "${value}" is not permitted`, permitted };
|
|
32
|
+
if (parameter.pattern && !new RegExp(parameter.pattern).test(value))
|
|
33
|
+
return { slot, reason: `the value must match ${parameter.pattern}` };
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
case "number":
|
|
37
|
+
case "integer": {
|
|
38
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
39
|
+
return { slot, reason: "a number is required" };
|
|
40
|
+
if (parameter.type === "integer" && !Number.isInteger(value))
|
|
41
|
+
return { slot, reason: "an integer is required" };
|
|
42
|
+
if (parameter.minimum !== undefined && value < parameter.minimum)
|
|
43
|
+
return { slot, reason: `the value must be at least ${parameter.minimum}` };
|
|
44
|
+
if (parameter.maximum !== undefined && value > parameter.maximum)
|
|
45
|
+
return { slot, reason: `the value must be at most ${parameter.maximum}` };
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
case "boolean":
|
|
49
|
+
return typeof value === "boolean" ? undefined : { slot, reason: "a boolean is required" };
|
|
50
|
+
case "string_list": {
|
|
51
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string"))
|
|
52
|
+
return { slot, reason: "a list of strings is required" };
|
|
53
|
+
if (permitted) {
|
|
54
|
+
const bad = value.find((item) => !permitted.includes(item));
|
|
55
|
+
if (bad !== undefined)
|
|
56
|
+
return { slot, reason: `the value "${bad}" is not permitted`, permitted };
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
default:
|
|
61
|
+
// The contract is a loose object of the wire, thus a type this host does not know is possible. It is
|
|
62
|
+
// refused, never rendered as text, because a wrong literal is a wrong script.
|
|
63
|
+
return { slot, reason: `the slot type ${parameter.type} is not one this host renders` };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** A slot value as a literal of the language: R gives `TRUE` and `c(...)`, Python gives `True` and `[...]`. */
|
|
67
|
+
function literal(language, parameter, value) {
|
|
68
|
+
const python = language === "python";
|
|
69
|
+
switch (parameter.type) {
|
|
70
|
+
case "string":
|
|
71
|
+
return JSON.stringify(String(value));
|
|
72
|
+
case "formula":
|
|
73
|
+
return String(value);
|
|
74
|
+
case "number":
|
|
75
|
+
case "integer":
|
|
76
|
+
return String(value);
|
|
77
|
+
case "boolean":
|
|
78
|
+
return python ? (value ? "True" : "False") : value ? "TRUE" : "FALSE";
|
|
79
|
+
case "string_list": {
|
|
80
|
+
const items = Array.isArray(value) ? value : [String(value)];
|
|
81
|
+
const rendered = items.map((item) => JSON.stringify(String(item))).join(", ");
|
|
82
|
+
return python ? `[${rendered}]` : `c(${rendered})`;
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
throw new Error(`unhandled slot type: ${parameter.type}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** The literal of an optional slot that has no value: R keeps the type of the slot, Python has one absent scalar and an empty list. */
|
|
89
|
+
function absentLiteral(language, type) {
|
|
90
|
+
if (language === "python")
|
|
91
|
+
return type === "string_list" ? "[]" : "None";
|
|
92
|
+
switch (type) {
|
|
93
|
+
case "string":
|
|
94
|
+
return "NA_character_";
|
|
95
|
+
case "integer":
|
|
96
|
+
return "NA_integer_";
|
|
97
|
+
case "number":
|
|
98
|
+
return "NA_real_";
|
|
99
|
+
case "boolean":
|
|
100
|
+
return "NA";
|
|
101
|
+
case "string_list":
|
|
102
|
+
return "character(0)";
|
|
103
|
+
default:
|
|
104
|
+
return "NULL";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** The marker of a slot as the service leaves it in a marked render. */
|
|
108
|
+
export function slotMarker(name) {
|
|
109
|
+
return `{{${name}}}`;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Bind the local values into a marked script. Each local marker takes the
|
|
113
|
+
* literal of its value, else of the default of the slot, else the absent
|
|
114
|
+
* literal when the slot is optional. A required local slot with no value
|
|
115
|
+
* and no default is an issue, and so is a value the contract refuses. The
|
|
116
|
+
* report names the source of each bound slot and keeps the `read` flag of
|
|
117
|
+
* the marked report of the service.
|
|
118
|
+
*/
|
|
119
|
+
export function bindLocalSlots(template, script, localValues, marked = []) {
|
|
120
|
+
const issues = [];
|
|
121
|
+
const locals = localParameters(template.parameters);
|
|
122
|
+
const byName = new Map(locals.map((parameter) => [parameter.name, parameter]));
|
|
123
|
+
for (const name of Object.keys(localValues)) {
|
|
124
|
+
if (!byName.has(name))
|
|
125
|
+
issues.push({ slot: name, reason: "the template has no such local slot", permitted: [...byName.keys()] });
|
|
126
|
+
}
|
|
127
|
+
const values = new Map();
|
|
128
|
+
const slots = [];
|
|
129
|
+
for (const parameter of locals) {
|
|
130
|
+
const given = localValues[parameter.name];
|
|
131
|
+
if (given !== undefined) {
|
|
132
|
+
const issue = validateLocalSlot(parameter, given);
|
|
133
|
+
if (issue) {
|
|
134
|
+
issues.push(issue);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Validated above: a string, a finite number, a boolean, or a list of strings.
|
|
138
|
+
const value = given;
|
|
139
|
+
values.set(parameter.name, value);
|
|
140
|
+
slots.push({ name: parameter.name, value, source: "caller", adaptable: true, lines: [] });
|
|
141
|
+
}
|
|
142
|
+
else if (parameter.default !== undefined) {
|
|
143
|
+
// The contract is loose on the wire; a default is a scalar or a list of strings by the contract of the service,
|
|
144
|
+
// and the service renders whatever it holds, thus only an absent default is absent here too.
|
|
145
|
+
const value = parameter.default;
|
|
146
|
+
values.set(parameter.name, value);
|
|
147
|
+
slots.push({
|
|
148
|
+
name: parameter.name,
|
|
149
|
+
value,
|
|
150
|
+
source: "default",
|
|
151
|
+
adaptable: true,
|
|
152
|
+
...(parameter.default_source ? { default_source: parameter.default_source } : {}),
|
|
153
|
+
lines: [],
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
else if (parameter.required !== false) {
|
|
157
|
+
issues.push({ slot: parameter.name, reason: "the local slot is required and has no value" });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (issues.length > 0)
|
|
161
|
+
return { ok: false, issues };
|
|
162
|
+
const bound = script.replace(/\{\{([a-z][a-z0-9_]*)\}\}/g, (whole, name) => {
|
|
163
|
+
const parameter = byName.get(name);
|
|
164
|
+
if (!parameter)
|
|
165
|
+
return whole;
|
|
166
|
+
const value = values.get(name);
|
|
167
|
+
return value === undefined ? absentLiteral(template.language, parameter.type) : literal(template.language, parameter, value);
|
|
168
|
+
});
|
|
169
|
+
const lines = bound.split("\n");
|
|
170
|
+
const readByName = new Map(marked.flatMap((entry) => (entry.read === undefined ? [] : [[entry.name, entry.read]])));
|
|
171
|
+
const located = slots.map((entry) => ({
|
|
172
|
+
...entry,
|
|
173
|
+
lines: script.split("\n").flatMap((line, index) => (line.includes(slotMarker(entry.name)) && index < lines.length ? [index + 1] : [])),
|
|
174
|
+
...(readByName.has(entry.name) ? { read: readByName.get(entry.name) } : {}),
|
|
175
|
+
}));
|
|
176
|
+
return { ok: true, script: bound, slots: located };
|
|
177
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release a plan pins. The planner tools of one plan generation share
|
|
3
|
+
* one pin: the first recommend answer sets it, and every later call of the
|
|
4
|
+
* service inside that plan carries it as `expected_snapshot`. A service that
|
|
5
|
+
* moved to another release between two calls then refuses the call, and the
|
|
6
|
+
* plan never mixes two releases in silence.
|
|
7
|
+
*/
|
|
8
|
+
export interface SnapshotPin {
|
|
9
|
+
/** The pinned release digest, or `undefined` before the first answer. */
|
|
10
|
+
get(): string | undefined;
|
|
11
|
+
/** Pin the release of the first answer. A later call with another digest changes nothing: the first release is the plan's. */
|
|
12
|
+
set(digest: string): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function createSnapshotPin(): SnapshotPin;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The release a plan pins. The planner tools of one plan generation share
|
|
3
|
+
* one pin: the first recommend answer sets it, and every later call of the
|
|
4
|
+
* service inside that plan carries it as `expected_snapshot`. A service that
|
|
5
|
+
* moved to another release between two calls then refuses the call, and the
|
|
6
|
+
* plan never mixes two releases in silence.
|
|
7
|
+
*/
|
|
8
|
+
export function createSnapshotPin() {
|
|
9
|
+
let pinned;
|
|
10
|
+
return {
|
|
11
|
+
get: () => pinned,
|
|
12
|
+
set: (digest) => {
|
|
13
|
+
if (pinned === undefined)
|
|
14
|
+
pinned = digest;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|