@inflexa-ai/harness 0.40.0 → 0.41.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.
@@ -1,14 +1,15 @@
1
1
  /**
2
- * The chat turn of a host: a turn is `runChatTurn`, and `prepareChatTurn` is its first step. The
3
- * host gives only its transport values, and the harness stores the opening, each round, and the outcome.
2
+ * The chat turn of a host: a turn is `runChatTurn`, which is `openChatTurn` and then the `run` of the
3
+ * open turn. `prepareChatTurn` is the first step of the open. The host gives only its transport
4
+ * values, and the harness stores the opening, each round, and the outcome.
4
5
  */
5
6
  import type { Pool } from "pg";
6
- import type { AgentSession } from "../auth/types.js";
7
+ import type { RequestSession } from "../auth/types.js";
7
8
  import type { UsageRecorder } from "../billing/usage-recorder.js";
8
9
  import type { TokenUsageRollup } from "../contracts/usage.js";
9
10
  import type { DbError } from "../lib/db-result.js";
10
11
  import { type AgentFinish } from "../loop/run-agent.js";
11
- import type { EmitFn } from "../loop/types.js";
12
+ import type { AgentDefinition, EmitFn } from "../loop/types.js";
12
13
  import { type ThreadType } from "../memory/thread-store.js";
13
14
  import type { AgentChat, PromptCachePolicy } from "../providers/types.js";
14
15
  import type { ThreadAgentResolver } from "../runtime/assemble.js";
@@ -54,12 +55,20 @@ export interface RunChatTurnDeps extends PrepareChatTurnDeps {
54
55
  /** The agent of a turn comes from the thread type, which only the preparation knows. */
55
56
  readonly agents: ThreadAgentResolver;
56
57
  }
57
- /** The transport values of one turn. */
58
- export interface RunChatTurnParams {
58
+ /**
59
+ * The session of a turn without its provenance. The harness sets the provenance from the agent that
60
+ * it resolves, because only the preparation knows the thread type.
61
+ */
62
+ export type ChatTurnSession = Omit<RequestSession, "provenance">;
63
+ /** What opens a turn: the thread and the user input. */
64
+ export interface OpenChatTurnParams {
59
65
  readonly analysisId: string;
60
66
  readonly threadId: string;
61
67
  readonly userInput: string;
62
- readonly session: AgentSession;
68
+ }
69
+ /** The transport values of a turn that is open. */
70
+ export interface RunOpenChatTurnParams {
71
+ readonly session: ChatTurnSession;
63
72
  /** Makes the provider of the turn over the emit sink of the display recorder. */
64
73
  readonly chat: (emit: EmitFn) => AgentChat;
65
74
  readonly emit: EmitFn;
@@ -69,13 +78,15 @@ export interface RunChatTurnParams {
69
78
  readonly ask?: (request: AskRequest, emit: EmitFn) => Promise<AskApproval>;
70
79
  /** Who sent the user message. */
71
80
  readonly author?: string;
72
- /** When the host opened the turn. Absent counts from the call. */
81
+ /** When the host opened the turn. Absent counts from the call to {@link openChatTurn}. */
73
82
  readonly startedAtMs?: number;
74
83
  /** The cache policy of the root loop. Absent gives {@link CONVERSATION_PROMPT_CACHE}. */
75
84
  readonly promptCache?: PromptCachePolicy;
76
85
  /** The budget of the view of the root loop. Absent gives {@link DEFAULT_CONVERSATION_BUDGET}. */
77
86
  readonly conversationBudget?: number;
78
87
  }
88
+ /** The values of one whole turn. */
89
+ export type RunChatTurnParams = OpenChatTurnParams & RunOpenChatTurnParams;
79
90
  export type ChatTurnOutcome = {
80
91
  readonly status: "done";
81
92
  readonly finish: AgentFinish;
@@ -87,7 +98,8 @@ export type ChatTurnOutcome = {
87
98
  readonly reason: string;
88
99
  readonly cause: unknown;
89
100
  };
90
- export type ChatTurnResult = {
101
+ /** A turn that ended before its run. It wrote no row and no turn record. */
102
+ export type ChatTurnRefusal = {
91
103
  readonly kind: "prepare_failed";
92
104
  readonly cause: unknown;
93
105
  } | {
@@ -95,7 +107,8 @@ export type ChatTurnResult = {
95
107
  } | {
96
108
  readonly kind: "agent_unresolved";
97
109
  readonly threadType: ThreadType;
98
- } | {
110
+ };
111
+ export interface ChatTurnRan {
99
112
  readonly kind: "ran";
100
113
  readonly outcome: ChatTurnOutcome;
101
114
  /** True when the opening of the turn landed. */
@@ -106,9 +119,25 @@ export type ChatTurnResult = {
106
119
  readonly turnUsage?: TokenUsageRollup;
107
120
  /** The final text of a run that returned. */
108
121
  readonly fallbackText?: string;
109
- };
122
+ }
123
+ export type ChatTurnResult = ChatTurnRefusal | ChatTurnRan;
124
+ /**
125
+ * A turn that is prepared, with its agent resolved. Call `run` one time: each call stores a new
126
+ * opening of the same user message.
127
+ */
128
+ export interface OpenChatTurn {
129
+ readonly kind: "ready";
130
+ /** The agent that runs the turn. The harness stamps its provenance on the session of the run. */
131
+ readonly agent: AgentDefinition;
132
+ readonly run: (params: RunOpenChatTurnParams) => Promise<ChatTurnRan>;
133
+ }
134
+ /**
135
+ * Open one chat turn: prepare it and resolve its agent. A refusal comes back before any row of the
136
+ * turn exists, thus a host can answer it before it starts a stream.
137
+ */
138
+ export declare function openChatTurn(deps: RunChatTurnDeps, params: OpenChatTurnParams): Promise<OpenChatTurn | ChatTurnRefusal>;
110
139
  /**
111
- * Run one chat turn: prepare it, resolve its agent, store its opening, run the root loop with a
112
- * round sink that stores each round, and close the turn with its outcome.
140
+ * Run one chat turn: open it, store its opening, run the root loop with a round sink that stores
141
+ * each round, and close the turn with its outcome.
113
142
  */
114
143
  export declare function runChatTurn(deps: RunChatTurnDeps, params: RunChatTurnParams): Promise<ChatTurnResult>;
@@ -1,6 +1,7 @@
1
1
  /**
2
- * The chat turn of a host: a turn is `runChatTurn`, and `prepareChatTurn` is its first step. The
3
- * host gives only its transport values, and the harness stores the opening, each round, and the outcome.
2
+ * The chat turn of a host: a turn is `runChatTurn`, which is `openChatTurn` and then the `run` of the
3
+ * open turn. `prepareChatTurn` is the first step of the open. The host gives only its transport
4
+ * values, and the harness stores the opening, each round, and the outcome.
4
5
  */
5
6
  import { unwrapOrThrow } from "../lib/result.js";
6
7
  import { finalText, runAgent } from "../loop/run-agent.js";
@@ -100,15 +101,14 @@ async function readTurnContext(pool, analysisId) {
100
101
  /** The root loop of a chat turn compacts its view past this estimate, in tokens. */
101
102
  export const DEFAULT_CONVERSATION_BUDGET = 150_000;
102
103
  /**
103
- * Run one chat turn: prepare it, resolve its agent, store its opening, run the root loop with a
104
- * round sink that stores each round, and close the turn with its outcome.
104
+ * Open one chat turn: prepare it and resolve its agent. A refusal comes back before any row of the
105
+ * turn exists, thus a host can answer it before it starts a stream.
105
106
  */
106
- export async function runChatTurn(deps, params) {
107
- const logger = (deps.logger ?? createNoopLogger()).named("harness.chat");
108
- const startedAtMs = params.startedAtMs ?? Date.now();
107
+ export async function openChatTurn(deps, params) {
108
+ const openedAtMs = Date.now();
109
109
  let prepared;
110
110
  try {
111
- prepared = await prepareChatTurn(deps, { analysisId: params.analysisId, threadId: params.threadId, userInput: params.userInput });
111
+ prepared = await prepareChatTurn(deps, params);
112
112
  }
113
113
  catch (cause) {
114
114
  return { kind: "prepare_failed", cause };
@@ -118,8 +118,33 @@ export async function runChatTurn(deps, params) {
118
118
  const agent = deps.agents.forThread(prepared.threadType);
119
119
  if (agent.isErr())
120
120
  return { kind: "agent_unresolved", threadType: prepared.threadType };
121
+ const ready = prepared;
122
+ return {
123
+ kind: "ready",
124
+ agent: agent.value,
125
+ run: (run) => runOpenTurn(deps, params, ready, agent.value, { ...run, startedAtMs: run.startedAtMs ?? openedAtMs }),
126
+ };
127
+ }
128
+ /**
129
+ * Run one chat turn: open it, store its opening, run the root loop with a round sink that stores
130
+ * each round, and close the turn with its outcome.
131
+ */
132
+ export async function runChatTurn(deps, params) {
133
+ const { analysisId, threadId, userInput, ...run } = params;
134
+ const opened = await openChatTurn(deps, { analysisId, threadId, userInput });
135
+ if (opened.kind !== "ready")
136
+ return opened;
137
+ return opened.run(run);
138
+ }
139
+ async function runOpenTurn(deps, turn, prepared, agent, params) {
140
+ const logger = (deps.logger ?? createNoopLogger()).named("harness.chat");
141
+ const session = { ...params.session, provenance: { agentId: agent.id, callPath: [agent.id] } };
121
142
  const history = createThreadHistory(deps.pool, deps.logger);
122
- const recorder = createConversationDisplayRecorder({ userText: params.userInput, topLevelCallPath: params.session.provenance.callPath, sink: params.emit });
143
+ const recorder = createConversationDisplayRecorder({
144
+ userText: userTextOf(prepared.userMessage),
145
+ topLevelCallPath: session.provenance.callPath,
146
+ sink: params.emit,
147
+ });
123
148
  // The groups that did not land yet, in the order of the turn. A failed write keeps them for the next write.
124
149
  let pending = [
125
150
  {
@@ -136,13 +161,13 @@ export async function runChatTurn(deps, params) {
136
161
  return;
137
162
  const closing = close === undefined ? {} : { close };
138
163
  const write = startSeq === undefined ? { opening: pending[0], rounds: pending.slice(1), ...closing } : { startSeq, rounds: pending, ...closing };
139
- (await history.writeTurn(params.threadId, write)).match((written) => {
164
+ (await history.writeTurn(turn.threadId, write)).match((written) => {
140
165
  pending = [];
141
166
  startSeq = written.startSeq;
142
167
  storeError = undefined;
143
168
  }, (error) => {
144
169
  storeError = error;
145
- logger.warn("chat turn write failed", { threadId: params.threadId, op: error.op, ...logger.errorFields(error.cause) });
170
+ logger.warn("chat turn write failed", { threadId: turn.threadId, op: error.op, ...logger.errorFields(error.cause) });
146
171
  });
147
172
  };
148
173
  await flush();
@@ -150,7 +175,7 @@ export async function runChatTurn(deps, params) {
150
175
  let outcome;
151
176
  let fallbackText;
152
177
  try {
153
- const run = await runAgent(agent.value, prepared.messages, params.session, {
178
+ const run = await runAgent(agent, prepared.messages, session, {
154
179
  provider: params.chat(recorder.emit),
155
180
  signal: params.signal,
156
181
  emit: recorder.emit,
@@ -158,7 +183,7 @@ export async function runChatTurn(deps, params) {
158
183
  usageRecorder: params.usageRecorder,
159
184
  toolOutputStore: createToolOutputStore(deps.pool),
160
185
  promptCache: params.promptCache ?? CONVERSATION_PROMPT_CACHE,
161
- compaction: compactionPolicy(deps, params, prepared.threadType, agent.value, recorder.emit),
186
+ compaction: compactionPolicy(deps, turn.analysisId, params, prepared.threadType, agent, recorder.emit),
162
187
  ...(deps.logger === undefined ? {} : { logger: deps.logger }),
163
188
  ...(ask === undefined ? {} : { ask: (request) => ask(request, recorder.emit) }),
164
189
  onRound: async (round) => {
@@ -175,7 +200,7 @@ export async function runChatTurn(deps, params) {
175
200
  const aborted = params.signal.aborted && cause instanceof Error && cause.name === "AbortError";
176
201
  outcome = aborted ? { status: "aborted" } : { status: "failed", reason: failureReasonOf(cause), cause };
177
202
  }
178
- const durationMs = Date.now() - startedAtMs;
203
+ const durationMs = Date.now() - params.startedAtMs;
179
204
  const turnUsage = outcome.status === "failed" ? undefined : outcome.finish?.turnUsage;
180
205
  await flush({
181
206
  status: outcome.status,
@@ -193,8 +218,14 @@ export async function runChatTurn(deps, params) {
193
218
  ...(fallbackText === undefined ? {} : { fallbackText }),
194
219
  };
195
220
  }
221
+ /** The sanitized text of the user message, thus the display never holds a secret that the model does not see. */
222
+ function userTextOf(message) {
223
+ if (typeof message.content === "string")
224
+ return message.content;
225
+ return message.content.map((part) => (part.type === "text" ? part.text : "")).join("");
226
+ }
196
227
  const WORKING_MEMORY_TOOL_ID = "update_working_memory";
197
- function compactionPolicy(deps, params, threadType, agent, emit) {
228
+ function compactionPolicy(deps, analysisId, params, threadType, agent, emit) {
198
229
  // A report thread reads a frozen copy of the working memory, thus its agent declares no memory tool.
199
230
  const remembers = agent.tools.some((tool) => tool.id === WORKING_MEMORY_TOOL_ID);
200
231
  const workingMemory = createWorkingMemory(deps.pool);
@@ -205,7 +236,7 @@ function compactionPolicy(deps, params, threadType, agent, emit) {
205
236
  request: remembers ? MEMORY_COMPACTION_REQUEST : SUMMARY_COMPACTION_REQUEST,
206
237
  mask: remembers ? { allow: [WORKING_MEMORY_TOOL_ID] } : "none",
207
238
  keepFirstTurn: threadType === "report",
208
- recordsAfter: async (view) => contextRecordsFor({ threadType, analysisId: params.analysisId, workingMemory, ...(await readTurnContext(deps.pool, params.analysisId)) }, view),
239
+ recordsAfter: async (view) => contextRecordsFor({ threadType, analysisId, workingMemory, ...(await readTurnContext(deps.pool, analysisId)) }, view),
209
240
  };
210
241
  }
211
242
  /** The reason of a failed turn. It never holds the error message, because each later turn sends the note to the vendor. */
package/dist/index.d.ts CHANGED
@@ -82,8 +82,8 @@ export type { SuspendOn } from "./providers/errors.js";
82
82
  export { suspensionOfFailure } from "./workflows/suspension.js";
83
83
  export type { Suspension } from "./workflows/suspension.js";
84
84
  export { createStreamingChat } from "./providers/streaming-chat.js";
85
- export { DEFAULT_CONVERSATION_BUDGET, prepareChatTurn, runChatTurn } from "./app/chat-turn.js";
86
- export type { ChatTurnOutcome, ChatTurnResult, PrepareChatTurnDeps, PrepareChatTurnParams, PrepareChatTurnResult, RunChatTurnDeps, RunChatTurnParams, } from "./app/chat-turn.js";
85
+ export { DEFAULT_CONVERSATION_BUDGET, openChatTurn, prepareChatTurn, runChatTurn } from "./app/chat-turn.js";
86
+ export type { ChatTurnOutcome, ChatTurnRan, ChatTurnRefusal, ChatTurnResult, ChatTurnSession, OpenChatTurn, OpenChatTurnParams, PrepareChatTurnDeps, PrepareChatTurnParams, PrepareChatTurnResult, RunChatTurnDeps, RunChatTurnParams, RunOpenChatTurnParams, } from "./app/chat-turn.js";
87
87
  export { createThreadStore } from "./memory/thread-store.js";
88
88
  export type { ThreadStore, Thread, ThreadType, ThreadInputError, CreateThreadInput, ListThreadsInput, ThreadPage } from "./memory/thread-store.js";
89
89
  export { createThreadHistory, conversationRecordTurn } from "./memory/thread-history.js";
package/dist/index.js CHANGED
@@ -130,7 +130,7 @@ export { createStreamingChat } from "./providers/streaming-chat.js";
130
130
  // and gives only its transport values: the harness stores the opening, each round,
131
131
  // and the outcome. `appendTurn` stays for a record of a host. `createThreadStore`
132
132
  // owns thread metadata (create/list/title).
133
- export { DEFAULT_CONVERSATION_BUDGET, prepareChatTurn, runChatTurn } from "./app/chat-turn.js";
133
+ export { DEFAULT_CONVERSATION_BUDGET, openChatTurn, prepareChatTurn, runChatTurn } from "./app/chat-turn.js";
134
134
  export { createThreadStore } from "./memory/thread-store.js";
135
135
  export { createThreadHistory, conversationRecordTurn } from "./memory/thread-history.js";
136
136
  // The synthetic-message primitives. A host appends a record of out-of-band work (an analysis run's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.40.0",
3
+ "version": "0.41.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",