@inflexa-ai/harness 0.35.4 → 0.35.6
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.js +10 -2
- package/dist/lib/metrics.d.ts +57 -6
- package/dist/lib/metrics.js +105 -8
- package/dist/prompts/briefing.js +6 -3
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +16 -8
- package/dist/prompts/planner.d.ts +4 -0
- package/dist/prompts/planner.js +22 -16
- package/dist/sandbox/create-sandbox.js +23 -1
- package/dist/sandbox/exec-outcome.d.ts +45 -0
- package/dist/sandbox/exec-outcome.js +65 -0
- package/dist/sandbox/k8s-client.d.ts +10 -0
- package/dist/sandbox/k8s-client.js +40 -5
- package/dist/sandbox/liveness.d.ts +3 -1
- package/dist/sandbox/types.d.ts +24 -0
- package/dist/sandbox/types.js +24 -0
- package/dist/tools/ad-hoc-router.d.ts +23 -0
- package/dist/tools/ad-hoc-router.js +93 -5
- package/dist/tools/bio/alphafold-prediction.d.ts +19 -0
- package/dist/tools/bio/alphafold-prediction.js +40 -0
- package/dist/tools/bio/index.d.ts +2 -0
- package/dist/tools/bio/index.js +3 -0
- package/dist/tools/bio/search-protein.d.ts +20 -0
- package/dist/tools/bio/search-protein.js +61 -0
- package/dist/tools/execute-analysis.d.ts +2 -1
- package/dist/tools/execute-analysis.js +66 -5
- package/dist/tools/lib/alphafold-client.d.ts +55 -0
- package/dist/tools/lib/alphafold-client.js +72 -0
- package/dist/tools/lib/api-utils.d.ts +25 -0
- package/dist/tools/lib/api-utils.js +45 -3
- package/dist/tools/lib/uniprot-client.d.ts +131 -0
- package/dist/tools/lib/uniprot-client.js +190 -0
- package/dist/tools/research/literature-reviewer.js +2 -0
- package/dist/tools/sandbox/list-available-packages.d.ts +25 -0
- package/dist/tools/sandbox/list-available-packages.js +51 -29
- package/dist/workflows/sandbox-step.js +10 -1
- package/package.json +1 -1
|
@@ -105,3 +105,193 @@ export async function getReactomePathwaysByUniProt(accession) {
|
|
|
105
105
|
const rec = await getUniProtRecord(accession);
|
|
106
106
|
return rec?.reactomePathwayIds ?? [];
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The field list of the search. It is separate from `FIELDS` on purpose:
|
|
110
|
+
* `FIELDS` feeds `getUniProtRecord` and the target-assessment dossier, thus a
|
|
111
|
+
* widening of it would change what that workflow reads.
|
|
112
|
+
*/
|
|
113
|
+
const SEARCH_FIELDS = ["accession", "id", "protein_name", "gene_names", "length", "cc_function", "cc_subcellular_location"].join(",");
|
|
114
|
+
/** A FUNCTION comment runs to several hundred words; this is what one answer carries. */
|
|
115
|
+
const MAX_FUNCTION_CHARS = 1200;
|
|
116
|
+
/**
|
|
117
|
+
* `entryType` reads `UniProtKB reviewed (Swiss-Prot)` or
|
|
118
|
+
* `UniProtKB unreviewed (TrEMBL)`. The unreviewed value HOLDS the reviewed one
|
|
119
|
+
* as a substring, thus the negative test must run first. A test for `reviewed`
|
|
120
|
+
* alone answers true for every entry of either kind.
|
|
121
|
+
*/
|
|
122
|
+
const REVIEWED_ENTRY_TYPE = "reviewed";
|
|
123
|
+
const UNREVIEWED_ENTRY_TYPE = "unreviewed";
|
|
124
|
+
/** Is the entry a curated Swiss-Prot record, as opposed to a machine-annotated TrEMBL one? */
|
|
125
|
+
function isReviewedEntry(entryType) {
|
|
126
|
+
const value = (entryType ?? "").toLowerCase();
|
|
127
|
+
if (value.includes(UNREVIEWED_ENTRY_TYPE))
|
|
128
|
+
return false;
|
|
129
|
+
return value.includes(REVIEWED_ENTRY_TYPE);
|
|
130
|
+
}
|
|
131
|
+
const SearchCommentSchema = z.object({
|
|
132
|
+
commentType: z.string().optional(),
|
|
133
|
+
texts: z.array(z.object({ value: z.string().optional() })).optional(),
|
|
134
|
+
subcellularLocations: z.array(z.object({ location: z.object({ value: z.string().optional() }).optional() })).optional(),
|
|
135
|
+
});
|
|
136
|
+
const ProteinNameSchema = z.object({ fullName: z.object({ value: z.string().optional() }).optional() });
|
|
137
|
+
/**
|
|
138
|
+
* One result row of `uniprotkb/search`, with the same all-optional discipline as
|
|
139
|
+
* `UniProtRecordSchema`: UniProt omits the key of an absent value, thus every
|
|
140
|
+
* field carries `.optional()` and none carries `.nullable()`.
|
|
141
|
+
*
|
|
142
|
+
* A TrEMBL row can carry `submissionNames` in place of `recommendedName`
|
|
143
|
+
* (`X5D778` is one), thus the name reader falls back rather than reporting the
|
|
144
|
+
* protein as unnamed.
|
|
145
|
+
*/
|
|
146
|
+
export const UniProtSearchResultSchema = z.object({
|
|
147
|
+
primaryAccession: z.string().optional(),
|
|
148
|
+
uniProtkbId: z.string().optional(),
|
|
149
|
+
entryType: z.string().optional(),
|
|
150
|
+
proteinDescription: z
|
|
151
|
+
.object({
|
|
152
|
+
recommendedName: ProteinNameSchema.optional(),
|
|
153
|
+
submissionNames: z.array(ProteinNameSchema).optional(),
|
|
154
|
+
})
|
|
155
|
+
.optional(),
|
|
156
|
+
genes: z.array(z.object({ geneName: z.object({ value: z.string().optional() }).optional() })).optional(),
|
|
157
|
+
sequence: z.object({ length: z.number().optional() }).optional(),
|
|
158
|
+
comments: z.array(SearchCommentSchema).optional(),
|
|
159
|
+
});
|
|
160
|
+
/** The search envelope. It is exported so that the golden-fixture table drives it. */
|
|
161
|
+
export const UniProtSearchResponseSchema = z.object({
|
|
162
|
+
results: z.array(UniProtSearchResultSchema).optional(),
|
|
163
|
+
});
|
|
164
|
+
/** Read the protein name, preferring the recommended name over a submitted one. */
|
|
165
|
+
function extractProteinName(raw) {
|
|
166
|
+
const description = raw.proteinDescription;
|
|
167
|
+
const recommended = description?.recommendedName?.fullName?.value?.trim();
|
|
168
|
+
if (recommended)
|
|
169
|
+
return recommended;
|
|
170
|
+
for (const submitted of description?.submissionNames ?? []) {
|
|
171
|
+
const value = submitted.fullName?.value?.trim();
|
|
172
|
+
if (value)
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/** Read the first FUNCTION comment, capped at {@link MAX_FUNCTION_CHARS}. */
|
|
178
|
+
function extractFunctionText(raw) {
|
|
179
|
+
for (const comment of raw.comments ?? []) {
|
|
180
|
+
if (comment.commentType !== "FUNCTION")
|
|
181
|
+
continue;
|
|
182
|
+
for (const text of comment.texts ?? []) {
|
|
183
|
+
const value = text.value?.trim();
|
|
184
|
+
if (value)
|
|
185
|
+
return value.length > MAX_FUNCTION_CHARS ? `${value.slice(0, MAX_FUNCTION_CHARS)}…` : value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Read the distinct subcellular locations. UniProt splits them over more than
|
|
192
|
+
* one SUBCELLULAR LOCATION comment, and it repeats a location across them, thus
|
|
193
|
+
* the reader collects across every comment and keeps the first occurrence only.
|
|
194
|
+
*/
|
|
195
|
+
function extractSubcellularLocations(raw) {
|
|
196
|
+
const locations = [];
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
for (const comment of raw.comments ?? []) {
|
|
199
|
+
if (comment.commentType !== "SUBCELLULAR LOCATION")
|
|
200
|
+
continue;
|
|
201
|
+
for (const entry of comment.subcellularLocations ?? []) {
|
|
202
|
+
const value = entry.location?.value?.trim();
|
|
203
|
+
if (!value || seen.has(value))
|
|
204
|
+
continue;
|
|
205
|
+
seen.add(value);
|
|
206
|
+
locations.push(value);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return locations;
|
|
210
|
+
}
|
|
211
|
+
function toProtein(raw) {
|
|
212
|
+
return {
|
|
213
|
+
accession: raw.primaryAccession ?? "",
|
|
214
|
+
uniProtkbId: raw.uniProtkbId ?? null,
|
|
215
|
+
proteinName: extractProteinName(raw),
|
|
216
|
+
geneNames: (raw.genes ?? []).map((gene) => gene.geneName?.value).filter((name) => Boolean(name)),
|
|
217
|
+
sequenceLength: raw.sequence?.length ?? null,
|
|
218
|
+
function: extractFunctionText(raw),
|
|
219
|
+
subcellularLocations: extractSubcellularLocations(raw),
|
|
220
|
+
reviewed: isReviewedEntry(raw.entryType),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The accession forms that UniProt itself documents: the six-character form and
|
|
225
|
+
* the ten-character form, with an optional isoform suffix.
|
|
226
|
+
*
|
|
227
|
+
* The shape does NOT tell an accession from a gene symbol, because the two
|
|
228
|
+
* spaces overlap: `P2RY12`, `B3GAT1`, and their siblings are real gene symbols
|
|
229
|
+
* that match this form. Thus the match decides only whether the `accession:`
|
|
230
|
+
* clause is SAFE to send, never which space the query names.
|
|
231
|
+
*/
|
|
232
|
+
const ACCESSION_RE = /^(?:[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9](?:[A-Z][A-Z0-9]{2}[0-9]){1,2})(?:-\d+)?$/i;
|
|
233
|
+
/**
|
|
234
|
+
* Can the query ride in an `accession:` clause?
|
|
235
|
+
*
|
|
236
|
+
* UniProt validates the value of that clause, and it answers HTTP 400 for a
|
|
237
|
+
* value that is not accession-shaped. Thus a plain symbol such as `BRCA1` must
|
|
238
|
+
* never reach one, because the 400 fails the whole request.
|
|
239
|
+
*/
|
|
240
|
+
export function isUniProtAccession(query) {
|
|
241
|
+
return ACCESSION_RE.test(query.trim());
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Build the UniProtKB query.
|
|
245
|
+
*
|
|
246
|
+
* The organism filter and the reviewed filter narrow an ambiguous SYMBOL
|
|
247
|
+
* search. An accession is a unique key, thus the filters must not reach it: a
|
|
248
|
+
* lookup of `P02769` under the human default would otherwise answer nothing,
|
|
249
|
+
* although the accession names bovine serum albumin.
|
|
250
|
+
*
|
|
251
|
+
* An accession-shaped input is ambiguous, because the two identifier spaces
|
|
252
|
+
* overlap. Such an input searches both, and the unfiltered accession clause is
|
|
253
|
+
* OR-ed with the filtered symbol clause. An input that is not
|
|
254
|
+
* accession-shaped searches the symbol space alone.
|
|
255
|
+
*
|
|
256
|
+
* The accession side carries `active:true`, and that is the one filter it
|
|
257
|
+
* takes. A deleted accession still answers an `accession:` query, as an
|
|
258
|
+
* `Inactive` row with no name and no function, and `UniProtProtein` has no
|
|
259
|
+
* field for the deletion reason. `B3GAT1` is such a case: the gene symbol also
|
|
260
|
+
* names a deleted TrEMBL accession, thus without the filter the answer holds
|
|
261
|
+
* the human protein and a hollow second row.
|
|
262
|
+
*/
|
|
263
|
+
function buildSearchQuery(query, organismId, reviewedOnly) {
|
|
264
|
+
const symbolClauses = [`gene_exact:${query}`];
|
|
265
|
+
if (organismId !== undefined)
|
|
266
|
+
symbolClauses.push(`organism_id:${organismId}`);
|
|
267
|
+
if (reviewedOnly)
|
|
268
|
+
symbolClauses.push("reviewed:true");
|
|
269
|
+
const symbolQuery = symbolClauses.join(" AND ");
|
|
270
|
+
if (!isUniProtAccession(query))
|
|
271
|
+
return symbolQuery;
|
|
272
|
+
return `((accession:${query} AND active:true) OR (${symbolQuery}))`;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Search UniProtKB for a protein by gene symbol or by accession.
|
|
276
|
+
*
|
|
277
|
+
* An unknown query is HTTP 200 with `{"results":[]}`, never a 404, thus the
|
|
278
|
+
* empty result is an ordinary answer and never an error. The request asks for
|
|
279
|
+
* one row beyond `limit`, because UniProt reports its match count in the
|
|
280
|
+
* `x-total-results` header and `apiFetch` exposes no header. The extra row is
|
|
281
|
+
* what separates a trimmed answer from a complete one.
|
|
282
|
+
*/
|
|
283
|
+
export async function searchProteins(query, opts = {}) {
|
|
284
|
+
const { organismId, reviewedOnly = true, limit = 10 } = opts;
|
|
285
|
+
const trimmed = query.trim();
|
|
286
|
+
const params = new URLSearchParams({
|
|
287
|
+
query: buildSearchQuery(trimmed, organismId, reviewedOnly),
|
|
288
|
+
fields: SEARCH_FIELDS,
|
|
289
|
+
format: "json",
|
|
290
|
+
size: String(limit + 1),
|
|
291
|
+
});
|
|
292
|
+
const res = await apiFetchValidated(`${UNIPROT_BASE}/uniprotkb/search?${params.toString()}`, UniProtSearchResponseSchema, { headers: UNIPROT_HEADERS });
|
|
293
|
+
if (res.isErr())
|
|
294
|
+
throw new Error(describeApiError(res.error));
|
|
295
|
+
const rows = res.value.results ?? [];
|
|
296
|
+
return { proteins: rows.slice(0, limit).map(toProtein), hasMore: rows.length > limit };
|
|
297
|
+
}
|
|
@@ -26,6 +26,7 @@ import { createChemDbTools, createNcbiTools } from "../bio/keys.js";
|
|
|
26
26
|
import { genePreclinicalProfileTool } from "../bio/gene-preclinical-profile.js";
|
|
27
27
|
import { lookupAnnotationTool } from "../bio/lookup-annotation.js";
|
|
28
28
|
import { searchGeneTool } from "../bio/search-gene.js";
|
|
29
|
+
import { searchProteinTool } from "../bio/search-protein.js";
|
|
29
30
|
import { searchInteractionsTool } from "../bio/search-interactions.js";
|
|
30
31
|
import { createResolveCitationTool } from "./resolve-citation.js";
|
|
31
32
|
/** Sub-agent identity — appended to `callPath`, set as `agentId`. */
|
|
@@ -38,6 +39,7 @@ export function createLiteratureReviewerTool(deps) {
|
|
|
38
39
|
const chemDb = createChemDbTools(deps.bioKeys, { ...(deps.logger ? { logger: deps.logger } : {}) });
|
|
39
40
|
const reviewerTools = [
|
|
40
41
|
searchGeneTool,
|
|
42
|
+
searchProteinTool,
|
|
41
43
|
lookupAnnotationTool,
|
|
42
44
|
searchInteractionsTool,
|
|
43
45
|
ncbi.pubmed,
|
|
@@ -116,6 +116,31 @@ export type ListAvailablePackagesDeps = Pick<EnvironmentStorePaths, "farmLockFil
|
|
|
116
116
|
*/
|
|
117
117
|
readonly linkToolPresent?: boolean;
|
|
118
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* What one inventory read produced: the sections of the bound source, or the
|
|
121
|
+
* fact that the source cannot answer, with the reason where the source gives
|
|
122
|
+
* one. There is no empty-on-error shape — an unreadable inventory must stay
|
|
123
|
+
* distinguishable from an inventory that holds nothing, and a caller that
|
|
124
|
+
* cannot tell them apart reports a present package as absent.
|
|
125
|
+
*/
|
|
126
|
+
export type InventoryRead = {
|
|
127
|
+
readonly kind: "sections";
|
|
128
|
+
readonly sections: readonly Section[];
|
|
129
|
+
} | {
|
|
130
|
+
readonly kind: "unavailable";
|
|
131
|
+
readonly reason?: string;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Read the inventory of the bound source: the pool-scope reader when the
|
|
135
|
+
* embedder binds one (a conversation or planning surface), the first readable
|
|
136
|
+
* farm `inflexa.lock` otherwise (a sandbox agent). The image record merges
|
|
137
|
+
* into either report.
|
|
138
|
+
*
|
|
139
|
+
* Every caller that resolves a name against the environment reads through
|
|
140
|
+
* here, thus the census a model sees and the check a launch runs never
|
|
141
|
+
* disagree about what the source holds.
|
|
142
|
+
*/
|
|
143
|
+
export declare function readInventorySections(deps: ListAvailablePackagesDeps): Promise<InventoryRead>;
|
|
119
144
|
/**
|
|
120
145
|
* Create the package inventory over the bound source: the pool-scope reader
|
|
121
146
|
* when the embedder binds one (a conversation or planning surface), the
|
|
@@ -390,6 +390,50 @@ export function queryPackages(rawSections, { names, query, language, limit }) {
|
|
|
390
390
|
const { content, returned } = renderListing(filtered, limit ?? DEFAULT_LIMIT, bothTrackNames(sections));
|
|
391
391
|
return { available: true, total, returned, hasMore: returned < total, content };
|
|
392
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Read the inventory of the bound source: the pool-scope reader when the
|
|
395
|
+
* embedder binds one (a conversation or planning surface), the first readable
|
|
396
|
+
* farm `inflexa.lock` otherwise (a sandbox agent). The image record merges
|
|
397
|
+
* into either report.
|
|
398
|
+
*
|
|
399
|
+
* Every caller that resolves a name against the environment reads through
|
|
400
|
+
* here, thus the census a model sees and the check a launch runs never
|
|
401
|
+
* disagree about what the source holds.
|
|
402
|
+
*/
|
|
403
|
+
export async function readInventorySections(deps) {
|
|
404
|
+
// The image record merges in when the host can read a valid one. An absent
|
|
405
|
+
// record and an invalid record both merge nothing, and neither is an
|
|
406
|
+
// error: the record is an enrichment, and the farm or pool inventory stays
|
|
407
|
+
// whole without it. A store packed before the record existed carries none.
|
|
408
|
+
// There is no logger here, thus an invalid record cannot be reported — the
|
|
409
|
+
// schema of the harness refuses it, and the report degrades to the tracks
|
|
410
|
+
// that it could read.
|
|
411
|
+
const recordSections = readImagePackagesFile(deps.imagePackagesFile ?? DEFAULT_IMAGE_PACKAGES_FILE)
|
|
412
|
+
.map(imageSections)
|
|
413
|
+
.unwrapOr([]);
|
|
414
|
+
if (deps.readPoolInventory) {
|
|
415
|
+
const pool = await deps.readPoolInventory().catch((cause) => ({
|
|
416
|
+
kind: "unavailable",
|
|
417
|
+
reason: cause instanceof Error ? cause.message : String(cause),
|
|
418
|
+
}));
|
|
419
|
+
if (pool.kind === "unavailable")
|
|
420
|
+
return { kind: "unavailable", reason: pool.reason };
|
|
421
|
+
return { kind: "sections", sections: [...pool.sections, ...recordSections] };
|
|
422
|
+
}
|
|
423
|
+
// Both farm container paths are tried when the host injects none, because
|
|
424
|
+
// the path keys on the declared toolchain and this read carries none.
|
|
425
|
+
let lock = null;
|
|
426
|
+
for (const candidate of deps.farmLockFile ? [deps.farmLockFile] : DEFAULT_FARM_LOCK_FILES) {
|
|
427
|
+
lock = readFarmLockFile(candidate).unwrapOr(null);
|
|
428
|
+
if (lock !== null)
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
// A lock that no candidate path holds is an absence with no reason to
|
|
432
|
+
// report: the read never got far enough to learn one.
|
|
433
|
+
if (lock === null)
|
|
434
|
+
return { kind: "unavailable" };
|
|
435
|
+
return { kind: "sections", sections: [...lockSections(lock), ...recordSections] };
|
|
436
|
+
}
|
|
393
437
|
/**
|
|
394
438
|
* Create the package inventory over the bound source: the pool-scope reader
|
|
395
439
|
* when the embedder binds one (a conversation or planning surface), the
|
|
@@ -397,8 +441,6 @@ export function queryPackages(rawSections, { names, query, language, limit }) {
|
|
|
397
441
|
* record merges into either report.
|
|
398
442
|
*/
|
|
399
443
|
export function createListAvailablePackagesTool(deps = {}) {
|
|
400
|
-
const lockCandidates = deps.farmLockFile ? [deps.farmLockFile] : DEFAULT_FARM_LOCK_FILES;
|
|
401
|
-
const imagePackagesFile = deps.imagePackagesFile ?? DEFAULT_IMAGE_PACKAGES_FILE;
|
|
402
444
|
const readPoolInventory = deps.readPoolInventory;
|
|
403
445
|
// The scope decides the framing sentence: a sandbox agent reads what it can
|
|
404
446
|
// import NOW, and a conversation surface reads what the store HOLDS — a
|
|
@@ -461,38 +503,18 @@ export function createListAvailablePackagesTool(deps = {}) {
|
|
|
461
503
|
return "full package list";
|
|
462
504
|
},
|
|
463
505
|
execute: async (input) => {
|
|
464
|
-
|
|
465
|
-
// An absent record and an invalid record both merge nothing, and
|
|
466
|
-
// neither is an error: the record is an enrichment, and the farm
|
|
467
|
-
// or pool inventory stays whole without it. A store packed before
|
|
468
|
-
// the record existed carries none. The tool holds no logger, thus
|
|
469
|
-
// an invalid record cannot be reported here — the schema of the
|
|
470
|
-
// harness refuses it, and the report degrades to the tracks it
|
|
471
|
-
// could read.
|
|
472
|
-
const recordSections = readImagePackagesFile(imagePackagesFile).map(imageSections).unwrapOr([]);
|
|
506
|
+
const read = await readInventorySections(deps);
|
|
473
507
|
// An unreadable inventory is an expected environment state — model it as an
|
|
474
508
|
// `available: false` data variant telling the caller the set is UNKNOWN,
|
|
475
509
|
// WITH the reason: without it, a structural fault (a damaged dependency
|
|
476
510
|
// graph) reads as a transient flake, and the caller retries for ever.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
})
|
|
482
|
-
if (pool.kind === "unavailable")
|
|
483
|
-
return ok({ available: false, content: `${POOL_UNAVAILABLE_NOTE} The reason: ${pool.reason}.` });
|
|
484
|
-
return ok(queryPackages([...pool.sections, ...recordSections], input));
|
|
485
|
-
}
|
|
486
|
-
let lock = null;
|
|
487
|
-
for (const candidate of lockCandidates) {
|
|
488
|
-
lock = readFarmLockFile(candidate).unwrapOr(null);
|
|
489
|
-
if (lock !== null)
|
|
490
|
-
break;
|
|
491
|
-
}
|
|
492
|
-
if (lock === null) {
|
|
493
|
-
return ok({ available: false, content: UNAVAILABLE_NOTE });
|
|
511
|
+
// The note follows the vantage of the bound source, because a
|
|
512
|
+
// conversation surface has no runtime to probe.
|
|
513
|
+
if (read.kind === "unavailable") {
|
|
514
|
+
const note = readPoolInventory ? POOL_UNAVAILABLE_NOTE : UNAVAILABLE_NOTE;
|
|
515
|
+
return ok({ available: false, content: read.reason === undefined ? note : `${note} The reason: ${read.reason}.` });
|
|
494
516
|
}
|
|
495
|
-
return ok(queryPackages(
|
|
517
|
+
return ok(queryPackages(read.sections, input));
|
|
496
518
|
},
|
|
497
519
|
});
|
|
498
520
|
}
|
|
@@ -34,6 +34,7 @@ import { unwrapOrThrow } from "../lib/result.js";
|
|
|
34
34
|
import { isBudgetExceeded } from "../loop/budget-exceeded.js";
|
|
35
35
|
import { runAgent } from "../loop/run-agent.js";
|
|
36
36
|
import { durableStep } from "../loop/run-step.js";
|
|
37
|
+
import { lastExecOutcome } from "../sandbox/exec-outcome.js";
|
|
37
38
|
import { activityForTool, applyTreeDelta, isChatDataPart, sandboxTreeDelta, stepPartId } from "../sandbox/sandbox-step-translate.js";
|
|
38
39
|
import { createDetailResolver } from "../tools/detail-resolver.js";
|
|
39
40
|
import { forSubAgent } from "../auth/types.js";
|
|
@@ -274,8 +275,15 @@ export async function runSandboxStepBody(input, deps) {
|
|
|
274
275
|
// The scrub below is deliberate and load-bearing, which makes this record
|
|
275
276
|
// the ONLY account of why the step died — the thrown error carries just the
|
|
276
277
|
// generic phrase, so nothing downstream can reconstruct the cause.
|
|
278
|
+
// `durationMs`, the iteration cap, and the last exec of the sandbox are
|
|
279
|
+
// what separate a step that died in the model from one that died in the
|
|
280
|
+
// machine. `lastExec` holds no stream text (see `exec-outcome.ts`).
|
|
281
|
+
const lastExec = lastExecOutcome(sandbox.sandboxId);
|
|
277
282
|
logger.error("step failure", {
|
|
278
283
|
errorClass,
|
|
284
|
+
durationMs,
|
|
285
|
+
hitMaxSteps,
|
|
286
|
+
...(lastExec ? { lastExec } : {}),
|
|
279
287
|
...logger.errorFields(err),
|
|
280
288
|
});
|
|
281
289
|
const safe = userFacingStepFailure(errorClass);
|
|
@@ -341,7 +349,8 @@ export async function runSandboxStepBody(input, deps) {
|
|
|
341
349
|
const neverAbort = new AbortController().signal;
|
|
342
350
|
let transcript;
|
|
343
351
|
let finishReason;
|
|
344
|
-
|
|
352
|
+
// `false` until the loop reports otherwise: a loop that throws never reached its cap.
|
|
353
|
+
let hitMaxSteps = false;
|
|
345
354
|
let stepUsage;
|
|
346
355
|
try {
|
|
347
356
|
const agentResult = await runAgent(agent, initial, session, {
|
package/package.json
CHANGED