@inflexa-ai/harness 0.46.0 → 0.46.2

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.
@@ -29,7 +29,7 @@ import { askPartOf } from "../tools/approval/gateway.js";
29
29
  import { stepUuid } from "../tools/workspace/edit-file.js";
30
30
  import { openThread, readTurnContext, runOpenTurn, userTextOf, } from "./chat-turn.js";
31
31
  import { CHAT_STREAM_KEY, createTurnStreamWriter, readChatTurn } from "../loop/chat-stream.js";
32
- import { credentialGuard, turnCredentials, withRootTurn } from "../loop/turn-credentials.js";
32
+ import { credentialGuard, liveAuth, turnCredentials, withRootTurn } from "../loop/turn-credentials.js";
33
33
  import { assembleMessages, loadHistoryView, sanitizedUserMessage } from "./message-assembly.js";
34
34
  /** The name of the turn workflow. */
35
35
  export const CHAT_TURN_WORKFLOW = "chat-turn";
@@ -270,11 +270,10 @@ async function runTurnWorkflow(deps, input, signal) {
270
270
  const session = {
271
271
  identity: { user: input.userId },
272
272
  scope: { kind: "analysis", analysisId: input.analysisId, threadId: input.threadId },
273
- // The credential comes at each read, thus a call after a resume uses the credential that the resume put. A
274
- // turn with no credential makes no model call, thus no adapter reads the empty value.
275
- get auth() {
276
- return turnCredentials.of(input.turnId) ?? {};
277
- },
273
+ // The credential comes at each read, also through a copy of the session, thus a call after a resume uses the
274
+ // credential that the resume put. A turn with no credential makes no model call, thus no adapter reads the
275
+ // empty value.
276
+ auth: liveAuth(() => turnCredentials.of(input.turnId)),
278
277
  };
279
278
  const ran = await runOpenTurn(deps, { analysisId: input.analysisId, threadId: input.threadId, userInput: input.userText }, { kind: "ok", threadType: input.threadType, history: view, userMessage: opening.userMessage, contextRecords: opening.contextRecords }, agent.value, { session, author: input.author, startedAtMs: input.startedAtMs, promptCache: input.promptCache }, transport);
280
279
  const { outcome } = ran;
@@ -24,6 +24,11 @@ export type GateFailure = {
24
24
  readonly reason: string;
25
25
  /** True when the host asks the harness to suspend the work in place of a failure. */
26
26
  readonly suspend: boolean;
27
+ /**
28
+ * True when the host refuses the credential of the caller: it is expired, revoked, or absent. A person repairs
29
+ * it, thus a work that can pause waits for a resume. The suspend flag wins over it.
30
+ */
31
+ readonly credential?: boolean;
27
32
  };
28
33
  /**
29
34
  * The settlement a gate's `ResultAsync` resolves to, read structurally. An embedder whose neverthrow is
@@ -54,6 +59,10 @@ export type GateRefusal = {
54
59
  readonly kind: "suspended";
55
60
  readonly gate: GateName;
56
61
  readonly reason: string;
62
+ } | {
63
+ readonly kind: "credential";
64
+ readonly gate: GateName;
65
+ readonly reason: string;
57
66
  };
58
67
  /**
59
68
  * The host can build its `Result` with its own copy of neverthrow. The helper
package/dist/lib/hooks.js CHANGED
@@ -30,6 +30,11 @@ async function settlementOf(result) {
30
30
  }
31
31
  return await result;
32
32
  }
33
+ function refusalKindOf(failure) {
34
+ if (failure.suspend)
35
+ return "suspended";
36
+ return failure.credential === true ? "credential" : "failed";
37
+ }
33
38
  /**
34
39
  * The host can build its `Result` with its own copy of neverthrow. The helper
35
40
  * gives the outcome again as a `Result` of the copy of the harness. Thus the
@@ -41,7 +46,7 @@ export function passGate(gate, result) {
41
46
  const settled = await settlementOf(result);
42
47
  if (settlementOk(settled))
43
48
  return ok(settled.value);
44
- return err({ kind: settled.error.suspend ? "suspended" : "failed", gate, reason: settled.error.reason });
49
+ return err({ kind: refusalKindOf(settled.error), gate, reason: settled.error.reason });
45
50
  })());
46
51
  }
47
52
  export async function deliverNotice(log, notice, result) {
@@ -23,7 +23,7 @@ import { DATA_PROFILE_RUN_LITERAL } from "../contracts/data-profile.js";
23
23
  import { createSubAgentActivityTracker } from "../contracts/subagent-activity.js";
24
24
  import { CHAT_STREAM_KEY, createTurnStreamWriter, SUB_AGENT_STREAM_PART } from "./chat-stream.js";
25
25
  import { addChatUsage } from "./metrics.js";
26
- import { credentialGuard, currentRootTurn, turnCredentials, withRootTurn } from "./turn-credentials.js";
26
+ import { credentialGuard, currentRootTurn, liveAuth, turnCredentials, withRootTurn } from "./turn-credentials.js";
27
27
  /** The work of a child, from its input. A data profile runs under one constant frame and starts no child, thus it gives none. */
28
28
  function workOf(input) {
29
29
  const { analysisId, threadId } = input.scope;
@@ -59,11 +59,10 @@ export function registerSubAgent(name, body, pauses) {
59
59
  scope: input.scope,
60
60
  provenance: input.provenance,
61
61
  runFrame: input.runFrame,
62
- // The credential comes at each read, thus a call after a resume of the turn uses the credential that the
63
- // resume put. A child with no credential makes no model call, thus no adapter reads the empty value.
64
- get auth() {
65
- return authOf() ?? {};
66
- },
62
+ // The credential comes at each read, also through a copy of the session, thus a call after a resume of the
63
+ // turn uses the credential that the resume put. A child with no credential makes no model call, thus no
64
+ // adapter reads the empty value.
65
+ auth: liveAuth(authOf),
67
66
  };
68
67
  // The input holds the provenance of the caller. The body of the sub-agent extends it.
69
68
  const caller = { agentId: input.provenance.agentId, callPath: [...input.provenance.callPath] };
@@ -17,6 +17,11 @@ export declare const turnCredentials: {
17
17
  /** The function of the host for a turn that this process did not open. */
18
18
  setHostCredential(credentialFor: ((turnId: string) => AuthContext | undefined) | undefined): void;
19
19
  };
20
+ /**
21
+ * The `auth` of a session whose credential can come later: each read goes to `current`, and no credential reads as an
22
+ * empty object. A copy of the session keeps this object, thus each copy sees the credential that a resume puts.
23
+ */
24
+ export declare function liveAuth(current: () => AuthContext | undefined): AuthContext;
20
25
  /** Run the body of a turn, or of one of its sub-agents, with the id of its root turn in its context. */
21
26
  export declare function withRootTurn<T>(turnId: string, body: () => Promise<T>): Promise<T>;
22
27
  /** The id of the root chat turn of the body that runs, or `undefined` outside a chat turn. */
@@ -29,6 +29,23 @@ export const turnCredentials = {
29
29
  hostCredential = credentialFor;
30
30
  },
31
31
  };
32
+ /**
33
+ * The `auth` of a session whose credential can come later: each read goes to `current`, and no credential reads as an
34
+ * empty object. A copy of the session keeps this object, thus each copy sees the credential that a resume puts.
35
+ */
36
+ export function liveAuth(current) {
37
+ const credential = () => current() ?? {};
38
+ return new Proxy({}, {
39
+ get: (_shell, key) => Reflect.get(credential(), key),
40
+ has: (_shell, key) => Reflect.has(credential(), key),
41
+ ownKeys: () => Reflect.ownKeys(credential()),
42
+ getOwnPropertyDescriptor: (_shell, key) => {
43
+ const descriptor = Reflect.getOwnPropertyDescriptor(credential(), key);
44
+ // A proxy can report a key that its shell does not hold only as configurable.
45
+ return descriptor === undefined ? undefined : { ...descriptor, configurable: true };
46
+ },
47
+ });
48
+ }
32
49
  /** Run the body of a turn, or of one of its sub-agents, with the id of its root turn in its context. */
33
50
  export function withRootTurn(turnId, body) {
34
51
  return rootTurn.run(turnId, body);
@@ -16,7 +16,16 @@ export function requestHeadersFor(hook, session) {
16
16
  }
17
17
  export function headersRefusalError(refusal, workload) {
18
18
  const message = `The request headers hook refused the model call for ${workload}: ${refusal.reason}`;
19
- return refusal.kind === "suspended"
20
- ? { type: "suspend", retryable: false, reason: refusal.reason, message }
21
- : { type: "provider", retryable: false, message };
19
+ switch (refusal.kind) {
20
+ case "suspended":
21
+ return { type: "suspend", retryable: false, reason: refusal.reason, message };
22
+ case "credential":
23
+ return { type: "auth", retryable: false, message };
24
+ case "failed":
25
+ return { type: "provider", retryable: false, message };
26
+ default: {
27
+ const exhaustive = refusal;
28
+ return exhaustive;
29
+ }
30
+ }
22
31
  }
package/dist/testing.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * embedder imports it from `@inflexa-ai/harness/testing`.
8
8
  */
9
9
  import type { LanguageModelV4CallOptions, SharedV4ProviderMetadata } from "@ai-sdk/provider";
10
+ import type { ResolveRequestHeaders } from "./providers/request-headers.js";
10
11
  export { inProcessChatTurns } from "./app/in-process-turn.js";
11
12
  import type { ChatProvider, ChatUsage, ProviderCapabilities } from "./providers/types.js";
12
13
  export type ScriptedPart = {
@@ -57,4 +58,5 @@ export declare function callTools(...calls: readonly ScriptedPart[]): ScriptedRe
57
58
  export declare function scriptedModel(script: readonly ScriptStep[], options?: {
58
59
  readonly capabilities?: Partial<ProviderCapabilities>;
59
60
  readonly maxRetries?: number;
61
+ readonly resolveRequestHeaders?: ResolveRequestHeaders;
60
62
  }): ScriptedModel;
package/dist/testing.js CHANGED
@@ -91,6 +91,11 @@ export function scriptedModel(script, options = {}) {
91
91
  return { stream: convertArrayToReadableStream(streamOf(entry, index)) };
92
92
  },
93
93
  });
94
- const provider = createAiSdkProvider({ model, capabilities: options.capabilities, maxRetries: options.maxRetries ?? 0 });
94
+ const provider = createAiSdkProvider({
95
+ model,
96
+ capabilities: options.capabilities,
97
+ maxRetries: options.maxRetries ?? 0,
98
+ resolveRequestHeaders: options.resolveRequestHeaders,
99
+ });
95
100
  return { provider, calls };
96
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.46.0",
3
+ "version": "0.46.2",
4
4
  "description": "Host-agnostic bioinformatics agent harness — an agent loop on the AI SDK, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",