@inflexa-ai/harness 0.42.0 → 0.42.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.
@@ -24,7 +24,8 @@ export type ChatPartCheck = {
24
24
  export declare function toChatFrame(event: EmitEvent | ChatStreamEvent | ChatDataPart, fallbackSource: EmitEvent["source"]): ChatFrame | null;
25
25
  /**
26
26
  * Check a part frame against the schema of its type, one time, where it arrives. The schema drops a field that it
27
- * does not know. A type that the registry does not know passes unchanged, because a newer emitter can send it.
27
+ * does not know. A type that the registry does not know passes unchanged, because a newer emitter can send it. The
28
+ * source is checked for each type, because {@link isRootFrame} reads its call path.
28
29
  */
29
30
  export declare function checkChatPart(frame: ChatPartFrame): ChatPartCheck;
30
31
  /** A frame of the root agent of a chat turn, whose call path is `[agent.id]`. A longer path is a sub-agent. */
@@ -1,5 +1,6 @@
1
1
  // The one translation path of a chat stream. Browser-safe: each import from outside `contracts/` is a type import.
2
2
  import { isReconciling, PART_REGISTRY } from "./part-registry.js";
3
+ import { EventSourceSchema } from "./schemas/chat-events.js";
3
4
  import { ChatPartSchema } from "./schemas/chat-parts.js";
4
5
  function toSource(source) {
5
6
  return { agentId: source.agentId, callPath: [...source.callPath] };
@@ -44,12 +45,18 @@ export function toChatFrame(event, fallbackSource) {
44
45
  }
45
46
  /**
46
47
  * Check a part frame against the schema of its type, one time, where it arrives. The schema drops a field that it
47
- * does not know. A type that the registry does not know passes unchanged, because a newer emitter can send it.
48
+ * does not know. A type that the registry does not know passes unchanged, because a newer emitter can send it. The
49
+ * source is checked for each type, because {@link isRootFrame} reads its call path.
48
50
  */
49
51
  export function checkChatPart(frame) {
52
+ const { source, ...part } = frame;
53
+ if (source !== undefined) {
54
+ const checkedSource = EventSourceSchema.safeParse(source);
55
+ if (!checkedSource.success)
56
+ return { ok: false, error: checkedSource.error.message };
57
+ }
50
58
  if (!Object.hasOwn(PART_REGISTRY, frame.type))
51
59
  return { ok: true, frame };
52
- const { source, ...part } = frame;
53
60
  const parsed = ChatPartSchema.safeParse(part);
54
61
  if (!parsed.success)
55
62
  return { ok: false, error: parsed.error.message };
@@ -1,12 +1,11 @@
1
1
  /**
2
- * The two input-only rules of the run-event read seam: narrowing a raw durable
3
- * stream value to the chat data-part contract, and the latest-wins reconciling
4
- * fold applied before delivery.
2
+ * The rules of the run-event read side that need no database and no durability
3
+ * engine: narrowing a raw durable stream value to the chat data-part contract,
4
+ * and the latest-wins reconciling fold, as a batch for `createRunEventStream`
5
+ * and live for an embedder that reads the run streams itself.
5
6
  *
6
- * They live apart from the reader because they are the only part of the seam
7
- * whose correctness is a function of its argument alone — no database, no
8
- * durability engine, no clock — and keeping them here is what lets that be
9
- * proven directly instead of inferred from a driven subscription.
7
+ * They live apart from the reader, thus a test drives them directly instead of
8
+ * through a subscription.
10
9
  */
11
10
  import type { ChatPart } from "../contracts/chat-parts.js";
12
11
  /**
@@ -42,3 +41,12 @@ export declare function parseRunEventPart(value: unknown): ChatPart | null;
42
41
  * the parts that are delivered is the order they were written.
43
42
  */
44
43
  export declare function foldRunEventParts(parts: readonly ChatPart[]): ChatPart[];
44
+ /**
45
+ * The live form of {@link foldRunEventParts}, for a stream that stays open. An
46
+ * entry with a reconcile key waits `flushMs`, and a later entry with the same
47
+ * key replaces it. Thus a catch-up burst delivers each key once, and a live
48
+ * update lands one window later. Every other entry, also one of a type that the
49
+ * registry does not know, goes to `emit` at once, thus it can overtake a held
50
+ * entry. The held entries go to `emit` when the stream ends.
51
+ */
52
+ export declare function pipeFoldedRunEvents<T>(entries: AsyncIterable<T>, emit: (entry: T) => Promise<void>, flushMs: number): Promise<void>;
@@ -1,12 +1,11 @@
1
1
  /**
2
- * The two input-only rules of the run-event read seam: narrowing a raw durable
3
- * stream value to the chat data-part contract, and the latest-wins reconciling
4
- * fold applied before delivery.
2
+ * The rules of the run-event read side that need no database and no durability
3
+ * engine: narrowing a raw durable stream value to the chat data-part contract,
4
+ * and the latest-wins reconciling fold, as a batch for `createRunEventStream`
5
+ * and live for an embedder that reads the run streams itself.
5
6
  *
6
- * They live apart from the reader because they are the only part of the seam
7
- * whose correctness is a function of its argument alone — no database, no
8
- * durability engine, no clock — and keeping them here is what lets that be
9
- * proven directly instead of inferred from a driven subscription.
7
+ * They live apart from the reader, thus a test drives them directly instead of
8
+ * through a subscription.
10
9
  */
11
10
  import { reconcileKey } from "../contracts/chat-frame.js";
12
11
  import { PART_REGISTRY } from "../contracts/part-registry.js";
@@ -67,3 +66,59 @@ export function foldRunEventParts(parts) {
67
66
  }
68
67
  return folded;
69
68
  }
69
+ /**
70
+ * The live form of {@link foldRunEventParts}, for a stream that stays open. An
71
+ * entry with a reconcile key waits `flushMs`, and a later entry with the same
72
+ * key replaces it. Thus a catch-up burst delivers each key once, and a live
73
+ * update lands one window later. Every other entry, also one of a type that the
74
+ * registry does not know, goes to `emit` at once, thus it can overtake a held
75
+ * entry. The held entries go to `emit` when the stream ends.
76
+ */
77
+ export async function pipeFoldedRunEvents(entries, emit, flushMs) {
78
+ const latestByKey = new Map();
79
+ const pendingKeys = new Set();
80
+ let flushTimer = null;
81
+ let flushChain = Promise.resolve();
82
+ const flushPending = async () => {
83
+ flushTimer = null;
84
+ const keys = [...pendingKeys];
85
+ pendingKeys.clear();
86
+ for (const key of keys) {
87
+ const entry = latestByKey.get(key);
88
+ if (entry !== undefined)
89
+ await emit(entry);
90
+ }
91
+ };
92
+ const scheduleFlush = () => {
93
+ if (flushTimer !== null)
94
+ return;
95
+ flushTimer = setTimeout(() => {
96
+ // A timed flush has no caller to report to. A lasting emit failure
97
+ // still reaches the caller, through the next direct emit or the
98
+ // final flush.
99
+ flushChain = flushChain.then(flushPending).catch(() => undefined);
100
+ }, flushMs);
101
+ };
102
+ try {
103
+ for await (const entry of entries) {
104
+ const part = parseRunEventPart(entry);
105
+ const key = part === null ? undefined : reconcileKey(part);
106
+ if (key === undefined) {
107
+ await emit(entry);
108
+ continue;
109
+ }
110
+ latestByKey.set(key, entry);
111
+ pendingKeys.add(key);
112
+ scheduleFlush();
113
+ }
114
+ }
115
+ finally {
116
+ if (flushTimer !== null) {
117
+ clearTimeout(flushTimer);
118
+ flushTimer = null;
119
+ }
120
+ await flushChain;
121
+ if (pendingKeys.size > 0)
122
+ await flushPending();
123
+ }
124
+ }
package/dist/index.d.ts CHANGED
@@ -170,6 +170,7 @@ export { registerSandboxStep } from "./workflows/sandbox-step.js";
170
170
  export type { SandboxStepDeps, SandboxStepInput, SandboxStepResult, SandboxAgentBuildContext } from "./workflows/sandbox-step.js";
171
171
  export { createRunEventStream } from "./execution/run-event-stream.js";
172
172
  export type { RunEventStream, RunEventStreamDeps, RunEventSubscribeOptions, RunEventPartHandler } from "./execution/run-event-stream.js";
173
+ export { pipeFoldedRunEvents } from "./execution/run-event-parts.js";
173
174
  export { createSandboxAgents, SANDBOX_AGENT_META } from "./agents/sandbox/index.js";
174
175
  export type { SandboxAgentDeps, SandboxStepCoords, AgentMeta } from "./agents/sandbox/index.js";
175
176
  export { AnalysisPlanSchema } from "./schemas/workflow-state.js";
package/dist/index.js CHANGED
@@ -271,6 +271,7 @@ export { registerSandboxStep } from "./workflows/sandbox-step.js";
271
271
  // Nothing in the signature belongs to the durability engine, so an embedder
272
272
  // consumes run events without depending on it.
273
273
  export { createRunEventStream } from "./execution/run-event-stream.js";
274
+ export { pipeFoldedRunEvents } from "./execution/run-event-parts.js";
274
275
  // Sandbox-agent catalog. `buildAgent` maps a `SandboxAgentBuildContext` onto
275
276
  // `SandboxAgentDeps`, then selects the per-step agent by id from this record;
276
277
  // `SANDBOX_AGENT_META` is the planner-facing meta the same ids key.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.42.0",
3
+ "version": "0.42.1",
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",