@inflexa-ai/harness 0.38.0 → 0.39.0

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.
@@ -3,21 +3,25 @@
3
3
  *
4
4
  * Resolves an input to a value via `fetch`, caching success only behind a
5
5
  * bounded LRU with per-entry TTL. Concurrent callers for the same key coalesce
6
- * onto one in-flight fetch; a fetch that throws is not cached and clears its
7
- * in-flight slot. Callers supply their own key formula, fetch+validate step,
8
- * and (optionally) cache hit/miss metrics; this module owns eviction,
9
- * coalescing, and expiry — the deep, drift-prone plumbing.
6
+ * onto one in-flight fetch and share its outcome. An `err` is never cached:
7
+ * a host that gates spend per caller must see every refusal again, and a
8
+ * cached refusal or a cached transient fault would outlive its cause. A fetch
9
+ * that rejects is a defect: it is not cached, it clears its in-flight slot,
10
+ * and the rejection passes through. Callers supply their own key formula,
11
+ * fetch+validate step, and (optionally) cache hit/miss metrics; this module
12
+ * owns eviction, coalescing, and expiry — the deep, drift-prone plumbing.
10
13
  */
14
+ import { ResultAsync } from "neverthrow";
11
15
  export interface CachedResolverMetrics {
12
16
  onHit(): void;
13
17
  /** Recorded once per upstream fetch — coalesced callers do not count. */
14
18
  onMiss(): void;
15
19
  }
16
- export interface CachedResolverConfig<In, Out> {
20
+ export interface CachedResolverConfig<In, Out, E> {
17
21
  readonly ttlMs: number;
18
22
  readonly maxEntries: number;
19
23
  readonly keyFn: (input: In) => string;
20
- readonly fetch: (input: In) => Promise<Out>;
24
+ readonly fetch: (input: In) => ResultAsync<Out, E>;
21
25
  readonly metrics?: CachedResolverMetrics;
22
26
  }
23
- export declare function createCachedResolver<In, Out>(cfg: CachedResolverConfig<In, Out>): (input: In) => Promise<Out>;
27
+ export declare function createCachedResolver<In, Out, E>(cfg: CachedResolverConfig<In, Out, E>): (input: In) => ResultAsync<Out, E>;
@@ -3,11 +3,15 @@
3
3
  *
4
4
  * Resolves an input to a value via `fetch`, caching success only behind a
5
5
  * bounded LRU with per-entry TTL. Concurrent callers for the same key coalesce
6
- * onto one in-flight fetch; a fetch that throws is not cached and clears its
7
- * in-flight slot. Callers supply their own key formula, fetch+validate step,
8
- * and (optionally) cache hit/miss metrics; this module owns eviction,
9
- * coalescing, and expiry — the deep, drift-prone plumbing.
6
+ * onto one in-flight fetch and share its outcome. An `err` is never cached:
7
+ * a host that gates spend per caller must see every refusal again, and a
8
+ * cached refusal or a cached transient fault would outlive its cause. A fetch
9
+ * that rejects is a defect: it is not cached, it clears its in-flight slot,
10
+ * and the rejection passes through. Callers supply their own key formula,
11
+ * fetch+validate step, and (optionally) cache hit/miss metrics; this module
12
+ * owns eviction, coalescing, and expiry — the deep, drift-prone plumbing.
10
13
  */
14
+ import { ResultAsync, ok } from "neverthrow";
11
15
  export function createCachedResolver(cfg) {
12
16
  // Map insertion order doubles as LRU recency: a hit re-inserts the key at
13
17
  // the tail; eviction drops the head (oldest).
@@ -23,7 +27,7 @@ export function createCachedResolver(cfg) {
23
27
  }
24
28
  cache.delete(key);
25
29
  cache.set(key, entry);
26
- return entry.value;
30
+ return entry;
27
31
  }
28
32
  function store(key, value) {
29
33
  cache.set(key, { value, expiresAt: Date.now() + cfg.ttlMs });
@@ -34,21 +38,22 @@ export function createCachedResolver(cfg) {
34
38
  cache.delete(oldest);
35
39
  }
36
40
  }
37
- return async function resolve(input) {
41
+ async function resolve(input) {
38
42
  const key = cfg.keyFn(input);
39
43
  const fresh = readFresh(key);
40
44
  if (fresh !== undefined) {
41
45
  cfg.metrics?.onHit();
42
- return fresh;
46
+ return ok(fresh.value);
43
47
  }
44
48
  const pending = inflight.get(key);
45
49
  if (pending)
46
50
  return pending;
47
51
  cfg.metrics?.onMiss();
48
52
  const promise = (async () => {
49
- const value = await cfg.fetch(input);
50
- store(key, value);
51
- return value;
53
+ const fetched = await cfg.fetch(input);
54
+ if (fetched.isOk())
55
+ store(key, fetched.value);
56
+ return fetched;
52
57
  })();
53
58
  inflight.set(key, promise);
54
59
  try {
@@ -57,5 +62,6 @@ export function createCachedResolver(cfg) {
57
62
  finally {
58
63
  inflight.delete(key);
59
64
  }
60
- };
65
+ }
66
+ return (input) => new ResultAsync(resolve(input));
61
67
  }
package/dist/index.d.ts CHANGED
@@ -35,7 +35,7 @@ export type { ArtifactRegistry, ArtifactRegistrationInput, ArtifactSyncInput, Ex
35
35
  export type { ArtifactManifestEntry } from "./schemas/artifact-manifest.js";
36
36
  export type { ProvenanceCollector } from "./provenance/collector.js";
37
37
  export { UnavailableSessionPagePublisher } from "./tools/report-session/session-page-publisher.js";
38
- export type { MakeSessionPagePublisher, SessionPagePublisher, SessionPageMintResult } from "./tools/report-session/session-page-publisher.js";
38
+ export type { MakeSessionPagePublisher, SessionPageGrant, SessionPageMintFailure, SessionPagePublisher, } from "./tools/report-session/session-page-publisher.js";
39
39
  export type { ResolvePageUrl, SessionPageAccess } from "./tools/report-session/index.js";
40
40
  export { bindReadExport, bindSessionEmit } from "./provenance/seam.js";
41
41
  export type { ProvenanceExport, ProvenanceSeam, RunProvenanceEvent, SessionProvenanceEvent } from "./provenance/seam.js";
@@ -144,8 +144,12 @@ export type PackageRequestOutcome = {
144
144
  *
145
145
  * The seam speaks the query of the `package-identity` capability, thus the
146
146
  * host resolves the identity and the harness never folds a name.
147
+ *
148
+ * A failure of one package is an outcome. An `err` means that the whole
149
+ * call gave no answer, and the harness reads it as `unavailable` for each
150
+ * query.
147
151
  */
148
- export type ExtendAnalysisFarm = (analysisId: string, queries: readonly PackageQuery[]) => Promise<readonly PackageRequestOutcome[]>;
152
+ export type ExtendAnalysisFarm = (analysisId: string, queries: readonly PackageQuery[]) => ResultAsync<readonly PackageRequestOutcome[], GateFailure>;
149
153
  /**
150
154
  * Per-sandbox-machine liveness verdict. `oomKilled` is meaningful only when
151
155
  * `alive` is false: true when the backend reports the machine was killed for
@@ -19,6 +19,7 @@ import { routeAdHocRequest } from "./ad-hoc-router.js";
19
19
  import { adHocPlanId, adHocRunId } from "./analysis-invocation.js";
20
20
  import { defineTool } from "./define-tool.js";
21
21
  import { queryPackages, readInventorySections } from "./sandbox/list-available-packages.js";
22
+ import { callExtendAnalysisFarm } from "./sandbox/link-packages.js";
22
23
  const planIdSchema = z.string().regex(/^pln-[a-f0-9]{8}$/, "planId must be a pln-<8hex> value");
23
24
  const inputSchema = z
24
25
  .object({
@@ -120,16 +121,13 @@ async function linkPlanPackages(extendAnalysisFarm, analysisId, plan) {
120
121
  const queries = [...union.values()];
121
122
  if (queries.length === 0)
122
123
  return 0;
123
- let outcomes;
124
- try {
125
- outcomes = await extendAnalysisFarm(analysisId, queries);
126
- }
127
- catch (cause) {
128
- // A realization throw is an `unavailable` answer, not a driver error.
129
- // The sibling resolver seam maps a throw the same way (farm.ts), and
130
- // the planner can act on "the store cannot answer" — not on a stack.
131
- throw new PlanPackagesMissingError([], [], cause instanceof Error ? cause.message : String(cause));
124
+ const linked = await callExtendAnalysisFarm(extendAnalysisFarm, analysisId, queries);
125
+ if (linked.isErr()) {
126
+ // A call that gave no answer is an `unavailable` answer, not a driver
127
+ // error: the planner can act on "the store cannot answer".
128
+ throw new PlanPackagesMissingError([], [], linked.error);
132
129
  }
130
+ const outcomes = linked.value;
133
131
  // An `unavailable` outcome preempts everything: the link pass could not
134
132
  // answer at all (an unreadable graph, a locked farm), so a per-package
135
133
  // absence list would be a fabrication. One reason covers the batch.
@@ -37,16 +37,19 @@
37
37
  * one and the look cannot count. The tool then gives a missed-stamp outcome that directs a new preview,
38
38
  * because a repeated look never stamps a marker that no preview wrote.
39
39
  *
40
- * The eyes seam, the chrome navigation, and the workspace-root seam each speak the throw protocol. The tool
41
- * guards each of them, thus a fault of a look becomes a typed outcome and the loop never sees a throw.
40
+ * The eyes seam, the chrome navigation, and the workspace-root seam each speak the throw protocol. The URL
41
+ * seam gives a `Result`. The tool guards each of them, thus a fault of a look becomes a typed outcome and
42
+ * the loop never sees a throw.
42
43
  *
43
44
  * A hang is not a throw, and a guard alone never ends one. Thus the acquire and the release each carry a
44
45
  * deadline. A realization that hangs while it starts a browser would otherwise hold the whole agent turn,
45
46
  * and no outcome would ever arrive.
46
47
  */
48
+ import { type ResultAsync } from "neverthrow";
47
49
  import { z } from "zod";
48
50
  import type { AuthContext } from "../../auth/types.js";
49
51
  import { type ChromeConfig } from "../../lib/chrome.js";
52
+ import type { GateFailure } from "../../lib/hooks.js";
50
53
  import { type AcquireEyes } from "../../lib/eyes.js";
51
54
  import { type Logger } from "../../lib/logger.js";
52
55
  import { type CaptureCoverage, type CapturePage, type FailedRequest, type PageCapture } from "../../lib/page-capture.js";
@@ -65,7 +68,7 @@ export type ResolvePageUrl = (args: {
65
68
  analysisId: string;
66
69
  threadId: string;
67
70
  auth: AuthContext;
68
- }) => Promise<string>;
71
+ }) => ResultAsync<string, GateFailure>;
69
72
  /** The empty input. The tool examines the current page of the thread, thus it needs no field. */
70
73
  declare const examinePageInput: z.ZodObject<{}, z.core.$strip>;
71
74
  export type ExaminePageInput = z.infer<typeof examinePageInput>;
@@ -37,8 +37,9 @@
37
37
  * one and the look cannot count. The tool then gives a missed-stamp outcome that directs a new preview,
38
38
  * because a repeated look never stamps a marker that no preview wrote.
39
39
  *
40
- * The eyes seam, the chrome navigation, and the workspace-root seam each speak the throw protocol. The tool
41
- * guards each of them, thus a fault of a look becomes a typed outcome and the loop never sees a throw.
40
+ * The eyes seam, the chrome navigation, and the workspace-root seam each speak the throw protocol. The URL
41
+ * seam gives a `Result`. The tool guards each of them, thus a fault of a look becomes a typed outcome and
42
+ * the loop never sees a throw.
42
43
  *
43
44
  * A hang is not a throw, and a guard alone never ends one. Thus the acquire and the release each carry a
44
45
  * deadline. A realization that hangs while it starts a browser would otherwise hold the whole agent turn,
@@ -273,14 +274,21 @@ export function createExaminePageTool(deps) {
273
274
  logger.warn("the session page could not be read", { threadId, analysisId, ...defaultErrorFields(cause) });
274
275
  return ok({ outcome: "capture-failed", detail: "the session page could not be read" });
275
276
  }
276
- // The URL seam speaks the throw protocol like the other seams, thus a fault of the formation
277
- // becomes a typed outcome and the look never starts.
277
+ // An `err` of the URL seam and a rejected realization each become a typed outcome, and the look
278
+ // never starts.
278
279
  let url;
279
280
  try {
280
- url =
281
- deps.resolvePageUrl !== undefined
282
- ? await deps.resolvePageUrl({ pagePath, analysisId, threadId, auth: ctx.session.auth })
283
- : pathToFileURL(pagePath).href;
281
+ if (deps.resolvePageUrl === undefined) {
282
+ url = pathToFileURL(pagePath).href;
283
+ }
284
+ else {
285
+ const resolved = await deps.resolvePageUrl({ pagePath, analysisId, threadId, auth: ctx.session.auth });
286
+ if (resolved.isErr()) {
287
+ logger.warn("the page URL did not resolve", { threadId, analysisId, reason: resolved.error.reason });
288
+ return ok({ outcome: "capture-failed", detail: "the page URL did not resolve" });
289
+ }
290
+ url = resolved.value;
291
+ }
284
292
  }
285
293
  catch (cause) {
286
294
  logger.warn("the page URL did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
@@ -7,7 +7,7 @@
7
7
  */
8
8
  export { createPreviewReportTool, type PreviewReportInput, type PreviewReportResult, type PreviewReportToolDeps, type ResolvePageAsset, type SessionPageAccess, } from "./preview-report.js";
9
9
  export { createExaminePageTool, type CaptureCoverage, type CapturePage, type ExaminedTile, type ExaminePageInput, type ExaminePageResult, type ExaminePageToolDeps, type FailedRequest, type PageCapture, type ResolvePageUrl, } from "./examine-page.js";
10
- export { describeSessionPageMintFailure, UnavailableSessionPagePublisher, type MakeSessionPagePublisher, type SessionPageMintFailure, type SessionPageMintResult, type SessionPagePublisher, } from "./session-page-publisher.js";
10
+ export { describeSessionPageMintFailure, UnavailableSessionPagePublisher, type MakeSessionPagePublisher, type SessionPageGrant, type SessionPageMintFailure, type SessionPagePublisher, } from "./session-page-publisher.js";
11
11
  export { createRecordVersionTool, type RecordVersionInput, type RecordVersionResult, type RecordVersionToolDeps } from "./record-version.js";
12
12
  export { createDeriveTableTool, type DeriveTableInput, type DeriveTableResult, type DeriveTableToolDeps } from "./derive-table.js";
13
13
  export { createListPinnedArtifactsTool, type ListPinnedArtifactsInput, type ListPinnedArtifactsResult, type ListPinnedArtifactsToolDeps, type PinnedArtifact, type PinnedCitation, } from "./list-artifacts.js";
@@ -350,7 +350,7 @@ async function sweepAssets(assetsDir, staged, logger) {
350
350
  *
351
351
  * An absent factory gives no grant, and the arm carries no access field — the page path stays the whole
352
352
  * local contract. The tool builds the publisher over the scope of the call, thus the mint runs under the
353
- * auth of the caller. A refusal, a thrown construction, and a thrown realization each become the
353
+ * auth of the caller. An `err`, a thrown construction, and a rejected realization each become the
354
354
  * not-granted arm, thus a broken grant surface never costs the render. The URL spells through
355
355
  * `buildReportSessionUrl`, thus the formula lives in the contract and the seam gives the content-server
356
356
  * base alone.
@@ -359,22 +359,24 @@ async function mintAccess(makeSessionPages, analysisId, threadId, auth, logger)
359
359
  if (makeSessionPages === undefined) {
360
360
  return undefined;
361
361
  }
362
- let minted;
362
+ let grant;
363
363
  try {
364
- minted = await makeSessionPages({ analysisId, auth }).mintSessionPageAccess(threadId);
364
+ const minted = await makeSessionPages({ analysisId, auth }).mintSessionPageAccess(threadId);
365
+ if (minted.isErr()) {
366
+ const detail = describeSessionPageMintFailure(minted.error);
367
+ logger.warn("the session-page mint refused", { threadId, analysisId, detail });
368
+ return { granted: false, detail };
369
+ }
370
+ grant = minted.value;
365
371
  }
366
372
  catch (cause) {
367
373
  logger.warn("the session-page mint threw", { threadId, analysisId, ...defaultErrorFields(cause) });
368
374
  return { granted: false, detail: "session-page-access mint failed" };
369
375
  }
370
- if (!minted.ok) {
371
- logger.warn("the session-page mint refused", { threadId, analysisId, detail: describeSessionPageMintFailure(minted) });
372
- return { granted: false, detail: describeSessionPageMintFailure(minted) };
373
- }
374
376
  return {
375
377
  granted: true,
376
- url: buildReportSessionUrl(minted.data.baseUrl, analysisId, threadId, "index.html", minted.data.token),
377
- expiresAt: minted.data.expiresAt,
378
+ url: buildReportSessionUrl(grant.baseUrl, analysisId, threadId, "index.html", grant.token),
379
+ expiresAt: grant.expiresAt,
378
380
  };
379
381
  }
380
382
  /**
@@ -11,28 +11,24 @@
11
11
  * tool call, thus the composition binds a factory and the preview tool builds the publisher over the
12
12
  * scope of the call — exactly as it builds the reference resolver. A boot-time singleton could not
13
13
  * carry that credential, and a host that bans ambient state could not bind one at all. The local
14
- * default (`UnavailableSessionPagePublisher`) returns not-ok, thus the preview tool attaches no URL
14
+ * default (`UnavailableSessionPagePublisher`) gives an `err`, thus the preview tool attaches no URL
15
15
  * and the render still lands — the page path stays the whole local contract.
16
16
  */
17
+ import { type ResultAsync } from "neverthrow";
17
18
  import type { AuthContext } from "../../auth/types.js";
18
- export type SessionPageMintResult = {
19
- ok: true;
20
- data: {
21
- baseUrl: string;
22
- token: string;
23
- expiresAt: string;
24
- };
25
- } | {
26
- ok: false;
27
- status?: number;
28
- error: {
29
- message?: string;
19
+ /** The grant of one session page: the content-server base, the token, and its expiry. */
20
+ export interface SessionPageGrant {
21
+ readonly baseUrl: string;
22
+ readonly token: string;
23
+ readonly expiresAt: string;
24
+ }
25
+ /** What a failed mint is allowed to carry. */
26
+ export interface SessionPageMintFailure {
27
+ readonly status?: number;
28
+ readonly error: {
29
+ readonly message?: string;
30
30
  };
31
- };
32
- /** The not-ok arm of the seam's result — what a failed mint is allowed to carry. */
33
- export type SessionPageMintFailure = Extract<SessionPageMintResult, {
34
- ok: false;
35
- }>;
31
+ }
36
32
  /**
37
33
  * Renders a failed mint as the line an agent reads back from the preview tool.
38
34
  *
@@ -49,7 +45,7 @@ export interface SessionPagePublisher {
49
45
  * caller spells the whole URL through `buildReportSessionUrl`, thus the formula lives in the
50
46
  * contract and never in a realization.
51
47
  */
52
- mintSessionPageAccess(threadId: string): Promise<SessionPageMintResult>;
48
+ mintSessionPageAccess(threadId: string): ResultAsync<SessionPageGrant, SessionPageMintFailure>;
53
49
  }
54
50
  /**
55
51
  * The per-call construction of the publisher. The factory binds one analysis and the auth of the tool
@@ -60,9 +56,9 @@ export type MakeSessionPagePublisher = (scope: {
60
56
  auth: AuthContext;
61
57
  }) => SessionPagePublisher;
62
58
  /**
63
- * Local default — no hosted view of a session page. Returns not-ok, thus the preview tool carries the
59
+ * Local default — no hosted view of a session page. Gives an `err`, thus the preview tool carries the
64
60
  * refusal as data and the page path stays the whole result.
65
61
  */
66
62
  export declare class UnavailableSessionPagePublisher implements SessionPagePublisher {
67
- mintSessionPageAccess(_threadId: string): Promise<SessionPageMintResult>;
63
+ mintSessionPageAccess(_threadId: string): ResultAsync<SessionPageGrant, SessionPageMintFailure>;
68
64
  }
@@ -11,9 +11,10 @@
11
11
  * tool call, thus the composition binds a factory and the preview tool builds the publisher over the
12
12
  * scope of the call — exactly as it builds the reference resolver. A boot-time singleton could not
13
13
  * carry that credential, and a host that bans ambient state could not bind one at all. The local
14
- * default (`UnavailableSessionPagePublisher`) returns not-ok, thus the preview tool attaches no URL
14
+ * default (`UnavailableSessionPagePublisher`) gives an `err`, thus the preview tool attaches no URL
15
15
  * and the render still lands — the page path stays the whole local contract.
16
16
  */
17
+ import { errAsync } from "neverthrow";
17
18
  /**
18
19
  * Renders a failed mint as the line an agent reads back from the preview tool.
19
20
  *
@@ -32,14 +33,11 @@ export function describeSessionPageMintFailure(failure) {
32
33
  return detail.length > 0 ? `session-page-access mint failed: ${detail.join(" ")}` : "session-page-access mint failed";
33
34
  }
34
35
  /**
35
- * Local default — no hosted view of a session page. Returns not-ok, thus the preview tool carries the
36
+ * Local default — no hosted view of a session page. Gives an `err`, thus the preview tool carries the
36
37
  * refusal as data and the page path stays the whole result.
37
38
  */
38
39
  export class UnavailableSessionPagePublisher {
39
- async mintSessionPageAccess(_threadId) {
40
- return {
41
- ok: false,
42
- error: { message: "the hosted view of a session page is unavailable in this environment" },
43
- };
40
+ mintSessionPageAccess(_threadId) {
41
+ return errAsync({ error: { message: "the hosted view of a session page is unavailable in this environment" } });
44
42
  }
45
43
  }
@@ -13,6 +13,8 @@
13
13
  * capability, thus the agent learns one grammar for the plan, the tool, and
14
14
  * the prompt layer.
15
15
  */
16
+ import { type Result } from "neverthrow";
17
+ import { type PackageQuery } from "../../sandbox/package-identity.js";
16
18
  import type { ExtendAnalysisFarm, PackageRequestOutcome } from "../../sandbox/types.js";
17
19
  export interface LinkPackagesDeps {
18
20
  readonly extendAnalysisFarm: ExtendAnalysisFarm;
@@ -22,6 +24,12 @@ export interface LinkPackagesDeps {
22
24
  export interface LinkPackagesResult {
23
25
  readonly outcomes: readonly PackageRequestOutcome[];
24
26
  }
27
+ /**
28
+ * Call the seam, and give the outcomes or the one reason that the call gave
29
+ * no answer. An `err` and a rejected realization each give that reason, thus
30
+ * no call site sees a throw.
31
+ */
32
+ export declare function callExtendAnalysisFarm(extendAnalysisFarm: ExtendAnalysisFarm, analysisId: string, queries: readonly PackageQuery[]): Promise<Result<readonly PackageRequestOutcome[], string>>;
25
33
  export declare function createLinkPackagesTool(deps: LinkPackagesDeps): import("../define-tool.js").Tool<{
26
34
  packages: string[];
27
35
  }, LinkPackagesResult>;
@@ -34,6 +34,19 @@ function describeParseError(entry, error) {
34
34
  return `"${entry}" pins with the specifier "${error.specifier}", which this tool cannot honor — pass a bare name, or name==version with one exact version`;
35
35
  }
36
36
  }
37
+ /**
38
+ * Call the seam, and give the outcomes or the one reason that the call gave
39
+ * no answer. An `err` and a rejected realization each give that reason, thus
40
+ * no call site sees a throw.
41
+ */
42
+ export async function callExtendAnalysisFarm(extendAnalysisFarm, analysisId, queries) {
43
+ try {
44
+ return (await extendAnalysisFarm(analysisId, queries)).mapErr((failure) => failure.reason);
45
+ }
46
+ catch (cause) {
47
+ return err(cause instanceof Error ? cause.message : String(cause));
48
+ }
49
+ }
37
50
  export function createLinkPackagesTool(deps) {
38
51
  return defineTool({
39
52
  id: "link_packages",
@@ -67,17 +80,14 @@ export function createLinkPackagesTool(deps) {
67
80
  }
68
81
  queries.push(parsed.value);
69
82
  }
70
- // A realization throw reads as `unavailable` per query. The loop
71
- // would render the throw as a raw tool error, and a raw driver
72
- // message teaches the agent nothing that `reason` does not.
73
- try {
74
- const outcomes = await deps.extendAnalysisFarm(deps.analysisId, queries);
75
- return ok({ outcomes });
76
- }
77
- catch (cause) {
78
- const reason = cause instanceof Error ? cause.message : String(cause);
83
+ // A call that gave no answer reads as `unavailable` per query. A
84
+ // raw tool error teaches the agent nothing that `reason` does not.
85
+ const linked = await callExtendAnalysisFarm(deps.extendAnalysisFarm, deps.analysisId, queries);
86
+ if (linked.isErr()) {
87
+ const reason = linked.error;
79
88
  return ok({ outcomes: queries.map((query) => ({ kind: "unavailable", spelling: query.spelling, reason })) });
80
89
  }
90
+ return ok({ outcomes: linked.value });
81
91
  },
82
92
  });
83
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.38.0",
3
+ "version": "0.39.0",
4
4
  "description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",