@agentium/harness 4.5.0 → 4.6.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.
@@ -0,0 +1,173 @@
1
+ # Conversational runs (4.6)
2
+
3
+ Questions, steering, public messages and compaction stay inside one live run.
4
+ The runtime retains the session lease, Agent instance, transcript, provider
5
+ continuations and remaining budgets. Waiting does not poll the model.
6
+
7
+ ## Ask and resume
8
+
9
+ ```ts
10
+ import { openai } from "@agentium/core";
11
+ import { agentDriver, HarnessRuntime, requestInputTool } from "@agentium/harness";
12
+
13
+ const runtime = new HarnessRuntime({
14
+ driver: agentDriver({
15
+ name: "analyst",
16
+ model: openai("gpt-6-sol"),
17
+ instructions: "Analyze the available data. Ask the user when a necessary detail is missing.",
18
+ }, { stream: true }),
19
+ tools: [requestInputTool()],
20
+ grants: { toolIds: ["request_input"], modelRoles: ["main"] },
21
+ budgets: { maxModelCalls: 20, maxToolCalls: 30, maxTokens: 100_000 },
22
+ activeTimeoutMs: 120_000,
23
+ inputTimeoutMs: 15 * 60_000,
24
+ });
25
+
26
+ const handle = runtime.start("Prepare a report", {
27
+ identity: { tenantId: verifiedTenantId, userId: verifiedUserId },
28
+ sessionId: conversationId,
29
+ });
30
+
31
+ // In the application's event consumer:
32
+ for await (const event of handle.events()) {
33
+ if (event.payload.type === "input.requested") {
34
+ showQuestion(event.payload.request); // Store its id with the form.
35
+ } else {
36
+ renderActivity(event);
37
+ }
38
+ }
39
+
40
+ // In a separate form submission handler:
41
+ await handle.reply(requestIdFromForm, userAnswer);
42
+ const result = await handle.result(); // Resolves once, after the live wait.
43
+ ```
44
+
45
+ The model chooses when to call the granted question tool. A custom tool or driver
46
+ can call `await services.requestInput({ question, choices?, timeoutMs? })`
47
+ directly. The returned `InputReply` contains `requestId` and `input`.
48
+ `choices` are presentation suggestions; the application may validate a richer
49
+ answer before submitting it. This API does not invent a form schema or force a
50
+ question on every run.
51
+
52
+ `handle.state` is `running`, `awaiting_input`, `cancelling`, or `finished`.
53
+ `handle.pendingInput` returns a cloned current question, useful when reconnecting
54
+ after an event-history gap. Only one question can be pending per run.
55
+
56
+ Replies claim the pending request synchronously. Duplicate, stale, wrong-run,
57
+ malformed and post-completion replies reject with `HarnessInputError.code`.
58
+ Messages are limited to 64KB; questions to 32KB. The application remains responsible
59
+ for authenticating the actor before exposing a handle or submitting a reply.
60
+ Events `input.requested`, `input.resolved`, and `run.resumed` share the request ID.
61
+
62
+ | Timeout | Includes waiting? | Expiry reason code |
63
+ | --- | --- | --- |
64
+ | start option `deadline` (absolute timestamp) | Yes | `deadline_exceeded` |
65
+ | runtime `activeTimeoutMs` | No | `active_timeout` |
66
+ | runtime `inputTimeoutMs`, overridden per question | Only the current wait | `input_timeout` |
67
+
68
+ Timeouts cancel the run cooperatively. `cancel()` also works during a question.
69
+ Cleanup waits for already-started work to settle; external effects are not rolled
70
+ back. Without an input timeout or deadline, a question can wait indefinitely.
71
+ No new model calls may start while a question is pending; concurrent work already
72
+ started before the question still has its usual cancellation/lifetime contract.
73
+
74
+ For minor-version compatibility, a custom driver that **returns** legacy
75
+ `status: "awaiting_input"`, or a completion policy returning `await_input`,
76
+ still produces the old terminal hand-back. That path cannot resume its stack.
77
+ Migrate live conversations to **awaiting `requestInput()`** inside the driver/tool;
78
+ the new `awaiting_input` handle state is nonterminal.
79
+
80
+ ## Steer an active Agent
81
+
82
+ ```ts
83
+ await handle.send("Only include last month", { mode: "steer" });
84
+ ```
85
+
86
+ The built-in Agent driver now supports steering in both streaming modes. It
87
+ appends input before the next model call, after the entire current tool-call/result
88
+ group. Steering during a final model call is incorporated at the next complete
89
+ boundary if budgets allow. Effects already completed are not repeated by the
90
+ runtime. The model still decides its subsequent actions.
91
+
92
+ `input.received` means queued. `input.applied` means inserted into the
93
+ conversation at a safe boundary; both carry `inputId` and `mode`. Inputs within
94
+ each mode preserve FIFO order. `follow_up` remains queued until the current
95
+ driver turn completes; `steer` and `follow_up` have different scheduling semantics.
96
+ The queue holds at most 32 inputs. Steering rejects once the Agent loop closes,
97
+ including while a completion policy is still evaluating. Child agents cannot
98
+ consume their parent's steering inbox.
99
+
100
+ ## Public communication
101
+
102
+ Render these typed events by their stable item IDs:
103
+
104
+ | Event | Meaning |
105
+ | --- | --- |
106
+ | `message.started` | Start an item; `phase: "pending"` is explicitly unclassified. |
107
+ | `message.delta` | Append text to that item. |
108
+ | `message.completed` | The item has phase `commentary`, `final`, or `reasoning_summary`. |
109
+ | `message.failed` | A partial item was interrupted or cancelled. |
110
+
111
+ A `final` message is a model answer, not a terminal runtime result: policies or
112
+ queued input may still keep the run open. Only `run.terminal` closes the run.
113
+ Standalone native commentary continues the same Agent loop under its existing
114
+ budgets. No update schedule, artificial thinking, or progress tool is introduced.
115
+
116
+ Large completed items use `artifactId` with an empty inline text field; the deltas
117
+ remain complete. Retrieve the full `PublicMessage` with
118
+ `runtime.getArtifact(identity, sessionId, artifactId)`. Event cursors and the
119
+ existing bounded event store retain their normal semantics.
120
+
121
+ Core consumers can use `RunOutput.publicMessages`, the `run.message` EventBus event,
122
+ or opt into `Agent.stream(input, { publicMessageEvents: true })`. The default
123
+ stream retains its existing text/finish sequence. Terminal usage and cost data
124
+ remain on the final finish chunk, including with public lifecycle chunks enabled.
125
+ Legacy `text.delta`, `thinking`, and `RunOutput.text` remain available; prefer
126
+ public items when distinguishing progress from answers.
127
+
128
+ `getCommunicationCapabilities(provider)` reports adapter support:
129
+
130
+ | Adapter | Message phases | Public summaries |
131
+ | --- | --- | --- |
132
+ | OpenAI Responses and compatible endpoints implementing these fields | Native when present; otherwise inferred | Documented reasoning summary text |
133
+ | Anthropic / AWS Claude with reasoning enabled | Inferred | Text requested with `display: "summarized"` |
134
+ | Google / Vertex Gemini | Inferred | Documented thought summary text |
135
+ | Other adapters without an explicit capability declaration | Inferred | Unsupported |
136
+
137
+ `conditional` means support depends on the selected model, endpoint and request
138
+ options. It is not a promise that the model emits a summary on every call.
139
+ For providers without native phases, a complete response with tools is commentary;
140
+ a complete response without tools is final. Unclassified streaming text remains
141
+ pending until the response is complete.
142
+
143
+ Raw reasoning, signatures, encrypted/redacted blocks and `providerExtras` are
144
+ never used as public summaries. Opaque data remains in canonical conversation
145
+ history for provider replay. Custom adapters should populate `publicMessages`,
146
+ `message.phase`, and the corresponding streaming metadata explicitly.
147
+
148
+ ## Compact completed tool rounds
149
+
150
+ ```ts
151
+ const contextPolicy = summaryContextPolicy({
152
+ modelRole: "summary",
153
+ maxContextTokens: 12_000,
154
+ grouping: "tool_roundtrip",
155
+ keepRecentTurns: 2, // Here: keep the two latest complete groups.
156
+ });
157
+ ```
158
+
159
+ Bind and grant the summary model role through `HarnessRuntime.models` and
160
+ `grants.modelRoles` as usual. The default grouping remains whole user turns.
161
+ The new option recognizes completed tool rounds within a long task, keeps the
162
+ latest user request and complete retained tool/replay groups, and summarizes
163
+ older groups through the existing summary policy. Applications do not insert
164
+ synthetic user turns. Canonical session history is unchanged.
165
+
166
+ `compaction.started`, `compaction.completed` and `compaction.failed` correlate by
167
+ `compactionId` and `policyId`. The summary call shares the run's model/token
168
+ budgets and cancellation signal. If an intact live group cannot fit, compaction
169
+ fails rather than dropping required continuation data.
170
+
171
+ Live continuation does not provide process-restart recovery. Events, pending
172
+ promises and run handles remain in memory. A future recovery adapter must persist
173
+ questions/checkpoints, establish ownership, and reconcile uncertain tool effects.
package/README.md CHANGED
@@ -271,11 +271,19 @@ events; reconnect with `events({ after: cursor })`. Older cursors receive
271
271
  `HarnessEventGapError`. Large terminal outputs become scoped artifact references;
272
272
  retrieve them with `runtime.getArtifact(identity, sessionId, artifactId)`.
273
273
 
274
- Built-in drivers support queued `follow_up` input. Custom drivers may declare
275
- `steer` and consume it at boundaries with `services.takeInput()`. Unsupported
276
- controls throw `HarnessUnsupportedError`. Interrupt-and-replace, pause/resume,
277
- remote policy coverage, and durable recovery are rejected by this local runtime.
278
- In-memory events, artifacts, and sessions are explicitly non-durable.
274
+ Built-in drivers support queued `follow_up` input. The built-in Agent driver also
275
+ supports `steer` at complete tool-roundtrip boundaries. Custom drivers may declare
276
+ `steer` and consume it with `services.takeInput()`. For live questions, use
277
+ `requestInputTool()` or `await services.requestInput()`, then answer with
278
+ `handle.reply(requestId, input)`. `handle.state === "awaiting_input"` is nonterminal;
279
+ `result()` stays pending and budgets are preserved. Returning the old terminal
280
+ `awaiting_input` status is a legacy hand-back, not live suspension.
281
+
282
+ See [conversational runs](./CONVERSATIONAL-RUNS.md) for clocks, input events,
283
+ public messages, provider capabilities, streaming compatibility and compaction.
284
+ Unsupported controls throw `HarnessUnsupportedError`. Interrupt-and-replace,
285
+ remote policy coverage, and durable recovery remain unsupported. In-memory
286
+ events, artifacts, sessions and pending questions are explicitly non-durable.
279
287
 
280
288
  Host grants are upper bounds. Controllers select only approved tools/model roles;
281
289
  omitting required tools or selecting unsupported model options fails closed.
@@ -71,7 +71,7 @@ async function projectHarnessContext(policy, history, ctx, entries = []) {
71
71
  history: structuredClone(original),
72
72
  entries: structuredClone(entries)
73
73
  }, ctx);
74
- (0, _agentium_core.validateConversationTransform)(original, result.messages);
74
+ (0, _agentium_core.validateConversationTransform)(original, result.messages, policy.grouping);
75
75
  if (!Array.isArray(result.provenance) || result.provenance.some((item) => !item || typeof item.sourceId !== "string" || typeof item.included !== "boolean" || item.reason !== void 0 && typeof item.reason !== "string")) throw new Error("Context policy must return provenance");
76
76
  return {
77
77
  messages: structuredClone(result.messages),
@@ -397,6 +397,183 @@ var InMemoryHarnessEventStore = class {
397
397
  }
398
398
  };
399
399
  //#endregion
400
+ //#region src/runtime/input.ts
401
+ var HarnessInputError = class extends Error {
402
+ code;
403
+ constructor(code, message) {
404
+ super(message);
405
+ this.code = code;
406
+ this.name = "HarnessInputError";
407
+ }
408
+ };
409
+ const text = zod_v3.z.object({
410
+ type: zod_v3.z.literal("text"),
411
+ text: zod_v3.z.string()
412
+ }).strict();
413
+ const media = zod_v3.z.discriminatedUnion("type", [
414
+ text,
415
+ zod_v3.z.object({
416
+ type: zod_v3.z.literal("image"),
417
+ data: zod_v3.z.string(),
418
+ mimeType: zod_v3.z.enum([
419
+ "image/png",
420
+ "image/jpeg",
421
+ "image/gif",
422
+ "image/webp"
423
+ ]).optional()
424
+ }).strict(),
425
+ zod_v3.z.object({
426
+ type: zod_v3.z.literal("audio"),
427
+ data: zod_v3.z.string(),
428
+ mimeType: zod_v3.z.enum([
429
+ "audio/mp3",
430
+ "audio/wav",
431
+ "audio/ogg",
432
+ "audio/webm"
433
+ ]).optional()
434
+ }).strict(),
435
+ zod_v3.z.object({
436
+ type: zod_v3.z.literal("file"),
437
+ data: zod_v3.z.string(),
438
+ mimeType: zod_v3.z.string(),
439
+ filename: zod_v3.z.string().optional()
440
+ }).strict()
441
+ ]);
442
+ const message = zod_v3.z.union([zod_v3.z.string(), zod_v3.z.array(media).min(1)]);
443
+ const question = zod_v3.z.object({
444
+ question: zod_v3.z.string().trim().min(1),
445
+ choices: zod_v3.z.array(zod_v3.z.string().min(1)).min(1).max(100).optional(),
446
+ timeoutMs: zod_v3.z.number().int().positive().safe().optional()
447
+ }).strict();
448
+ function validateRunInput(input) {
449
+ const parsed = message.safeParse(input);
450
+ if (!parsed.success || Buffer.byteLength(JSON.stringify(parsed.data)) > 65536) throw new HarnessInputError("invalid_input", "Input must be valid message content no larger than 64KB");
451
+ return parsed.data;
452
+ }
453
+ function validateTimeout(value) {
454
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) throw new HarnessInputError("invalid_input", "Timeout must be a positive safe integer");
455
+ }
456
+ /** Owns live promises only; this is not a restart checkpoint. */
457
+ var LiveInput = class {
458
+ runId;
459
+ signal;
460
+ timeouts;
461
+ emit;
462
+ expire;
463
+ pending;
464
+ resolved = /* @__PURE__ */ new Set();
465
+ timer;
466
+ activeTimer;
467
+ activeSince = Date.now();
468
+ activeRemaining;
469
+ closed = false;
470
+ onAbort = () => {
471
+ const pending = this.pending;
472
+ this.pending = void 0;
473
+ this.clearTimers();
474
+ pending?.reject(this.signal.reason);
475
+ };
476
+ constructor(runId, signal, timeouts, emit, expire) {
477
+ this.runId = runId;
478
+ this.signal = signal;
479
+ this.timeouts = timeouts;
480
+ this.emit = emit;
481
+ this.expire = expire;
482
+ this.activeRemaining = timeouts.activeTimeoutMs ?? Infinity;
483
+ signal.addEventListener("abort", this.onAbort, { once: true });
484
+ if (!signal.aborted) this.resumeClock();
485
+ }
486
+ get request() {
487
+ return this.pending && structuredClone(this.pending.request);
488
+ }
489
+ assertActive() {
490
+ if (this.closed || this.signal.aborted) throw new HarnessInputError("run_finished", "Run is no longer active");
491
+ }
492
+ assertNotWaiting() {
493
+ this.assertActive();
494
+ if (this.pending) throw new HarnessInputError("input_pending", "No model calls are allowed while waiting for input");
495
+ }
496
+ requestInput(options) {
497
+ this.assertNotWaiting();
498
+ const parsed = question.safeParse(options);
499
+ if (!parsed.success || Buffer.byteLength(JSON.stringify(parsed.data)) > 32768) throw new HarnessInputError("invalid_input", "Question must be valid and no larger than 32KB");
500
+ const request = {
501
+ ...parsed.data,
502
+ id: `${this.runId}:${(0, node_crypto.randomUUID)()}`,
503
+ runId: this.runId
504
+ };
505
+ const promise = new Promise((resolve, reject) => {
506
+ this.pending = {
507
+ request,
508
+ resolve,
509
+ reject
510
+ };
511
+ });
512
+ if (this.activeTimer) clearTimeout(this.activeTimer);
513
+ this.activeRemaining -= Date.now() - this.activeSince;
514
+ const timeoutMs = request.timeoutMs ?? this.timeouts.inputTimeoutMs;
515
+ if (timeoutMs !== void 0) {
516
+ const deadline = Date.now() + timeoutMs;
517
+ const tick = () => {
518
+ if (Date.now() >= deadline) this.expire("input_timeout");
519
+ else this.timer = setTimeout(tick, Math.min(deadline - Date.now(), 2147483647));
520
+ };
521
+ tick();
522
+ }
523
+ this.emit({
524
+ type: "input.requested",
525
+ request
526
+ });
527
+ return promise;
528
+ }
529
+ reply(requestId, input) {
530
+ this.assertActive();
531
+ if (this.resolved.has(requestId)) throw new HarnessInputError("input_already_resolved", "Input request already resolved");
532
+ const pending = this.pending;
533
+ if (!pending) throw new HarnessInputError("input_not_pending", "There is no pending input request");
534
+ if (pending.request.id !== requestId) throw new HarnessInputError("input_mismatch", "Reply does not match the pending input request");
535
+ const value = validateRunInput(input);
536
+ this.resolved.add(requestId);
537
+ this.pending = void 0;
538
+ if (this.timer) clearTimeout(this.timer);
539
+ this.emit({
540
+ type: "input.resolved",
541
+ requestId
542
+ });
543
+ this.resumeClock();
544
+ this.emit({
545
+ type: "run.resumed",
546
+ requestId
547
+ });
548
+ pending.resolve({
549
+ requestId,
550
+ input: value
551
+ });
552
+ }
553
+ close() {
554
+ this.closed = true;
555
+ this.clearTimers();
556
+ this.signal.removeEventListener("abort", this.onAbort);
557
+ const pending = this.pending;
558
+ this.pending = void 0;
559
+ pending?.reject(new HarnessInputError("run_finished", "Run finished with unresolved input"));
560
+ }
561
+ resumeClock() {
562
+ this.activeSince = Date.now();
563
+ if (!Number.isFinite(this.activeRemaining)) return;
564
+ const tick = () => {
565
+ const remaining = this.activeRemaining - (Date.now() - this.activeSince);
566
+ if (remaining <= 0) this.expire("active_timeout");
567
+ else this.activeTimer = setTimeout(tick, Math.min(remaining, 2147483647));
568
+ };
569
+ tick();
570
+ }
571
+ clearTimers() {
572
+ if (this.timer) clearTimeout(this.timer);
573
+ if (this.activeTimer) clearTimeout(this.activeTimer);
574
+ }
575
+ };
576
+ //#endregion
400
577
  //#region src/runtime/runtime-registry.ts
401
578
  /** Registry resolution is static; it never initializes providers or executes drivers. */
402
579
  function resolveHarnessRuntime(definition) {
@@ -1222,6 +1399,7 @@ var HarnessRuntime = class {
1222
1399
  })])),
1223
1400
  contextPolicy: config.contextPolicy && Object.freeze({
1224
1401
  id: config.contextPolicy.id,
1402
+ grouping: config.contextPolicy.grouping,
1225
1403
  project: config.contextPolicy.project.bind(config.contextPolicy)
1226
1404
  }),
1227
1405
  completionPolicy: config.completionPolicy && Object.freeze({
@@ -1269,6 +1447,8 @@ var HarnessRuntime = class {
1269
1447
  } : config.grants;
1270
1448
  if (grants.requiredToolIds?.some((id) => !grants.toolIds.includes(id))) throw new Error("Required tool omitted by grants");
1271
1449
  if (options.deadline !== void 0 && !Number.isFinite(options.deadline)) throw new Error("Deadline must be finite");
1450
+ validateTimeout(config.activeTimeoutMs);
1451
+ validateTimeout(config.inputTimeoutMs);
1272
1452
  const controller = new AbortController();
1273
1453
  const runId = (0, node_crypto.randomUUID)();
1274
1454
  const request = Object.freeze({
@@ -1358,6 +1538,7 @@ var HarnessRuntime = class {
1358
1538
  const generateObserved = async (provider, messages, supplied) => {
1359
1539
  const end = observeModel(provider);
1360
1540
  try {
1541
+ liveInput.assertNotWaiting();
1361
1542
  const response = await provider.generate(messages, supplied);
1362
1543
  end(!controller.signal.aborted, response.usage);
1363
1544
  return response;
@@ -1367,6 +1548,9 @@ var HarnessRuntime = class {
1367
1548
  }
1368
1549
  };
1369
1550
  let terminal = false;
1551
+ let acceptingInput = true;
1552
+ let acceptingSteering = true;
1553
+ let cancellationCode = "cancelled";
1370
1554
  const inbox = [];
1371
1555
  const cancel = (reason = "Run cancelled") => {
1372
1556
  if (!terminal && !controller.signal.aborted) {
@@ -1378,6 +1562,10 @@ var HarnessRuntime = class {
1378
1562
  controller.abort(new Error(reason));
1379
1563
  }
1380
1564
  };
1565
+ const liveInput = new LiveInput(runId, controller.signal, config, (event) => events.append(event), (code) => {
1566
+ cancellationCode = code;
1567
+ cancel(code === "input_timeout" ? "User input timeout exceeded" : "Active execution timeout exceeded");
1568
+ });
1381
1569
  const onAbort = () => cancel("Caller cancelled the run");
1382
1570
  const callerSignal = options.signal;
1383
1571
  callerSignal?.addEventListener("abort", onAbort, { once: true });
@@ -1386,8 +1574,10 @@ var HarnessRuntime = class {
1386
1574
  const scheduleDeadline = () => {
1387
1575
  if (request.deadline === void 0 || controller.signal.aborted) return;
1388
1576
  const remaining = request.deadline - Date.now();
1389
- if (remaining <= 0) cancel("Run deadline exceeded");
1390
- else timer = setTimeout(scheduleDeadline, Math.min(remaining, 2147483647));
1577
+ if (remaining <= 0) {
1578
+ cancellationCode = "deadline_exceeded";
1579
+ cancel("Run deadline exceeded");
1580
+ } else timer = setTimeout(scheduleDeadline, Math.min(remaining, 2147483647));
1391
1581
  };
1392
1582
  scheduleDeadline();
1393
1583
  const result = Promise.resolve().then(async () => {
@@ -1420,7 +1610,10 @@ var HarnessRuntime = class {
1420
1610
  return operation;
1421
1611
  };
1422
1612
  const ensureActive = () => {
1423
- if (request.deadline !== void 0 && Date.now() >= request.deadline) cancel("Run deadline exceeded");
1613
+ if (request.deadline !== void 0 && Date.now() >= request.deadline) {
1614
+ cancellationCode = "deadline_exceeded";
1615
+ cancel("Run deadline exceeded");
1616
+ }
1424
1617
  controller.signal.throwIfAborted();
1425
1618
  if (!acceptingOperations) throw new Error("Run has finished accepting operations");
1426
1619
  };
@@ -1508,6 +1701,7 @@ var HarnessRuntime = class {
1508
1701
  }));
1509
1702
  const prepare = async (provider, messages, supplied = {}, callCtx = ctx) => {
1510
1703
  ensureActive();
1704
+ liveInput.assertNotWaiting();
1511
1705
  if (modelCalls >= (config.budgets?.maxModelCalls ?? Infinity) || usage.totalTokens >= (config.budgets?.maxTokens ?? Infinity)) throw new HarnessBudgetError("Aggregate model/token budget exhausted");
1512
1706
  const index = modelCalls++;
1513
1707
  const next = config.controller?.prepareStep ? await observeControl("prepareStep", () => config.controller.prepareStep({
@@ -1604,6 +1798,7 @@ var HarnessRuntime = class {
1604
1798
  },
1605
1799
  controlModel: (role, messages, options = {}) => track((async () => {
1606
1800
  ensureActive();
1801
+ liveInput.assertNotWaiting();
1607
1802
  if (!request.grants.modelRoles.includes(role)) throw new Error("Ungranted control model role");
1608
1803
  const binding = config.models?.[role];
1609
1804
  if (!binding) throw new Error("Control model requires an explicit host binding");
@@ -1662,9 +1857,12 @@ var HarnessRuntime = class {
1662
1857
  let amount = zero();
1663
1858
  let reason = "stop";
1664
1859
  let extras;
1860
+ let publicMessages;
1861
+ let phase;
1665
1862
  const calls = [];
1666
1863
  try {
1667
1864
  ensureActive();
1865
+ liveInput.assertNotWaiting();
1668
1866
  stream = call.provider.stream(call.messages, call.options);
1669
1867
  while (true) {
1670
1868
  const next = await stream.next();
@@ -1673,6 +1871,8 @@ var HarnessRuntime = class {
1673
1871
  finished = true;
1674
1872
  reason = next.value.finishReason;
1675
1873
  extras = next.value.providerExtras;
1874
+ publicMessages = next.value.publicMessages;
1875
+ phase = next.value.phase;
1676
1876
  amount = next.value.usage ?? zero();
1677
1877
  account(call.provider, amount);
1678
1878
  }
@@ -1695,6 +1895,7 @@ var HarnessRuntime = class {
1695
1895
  message: {
1696
1896
  role: "assistant",
1697
1897
  content: text || null,
1898
+ ...phase ? { phase } : {},
1698
1899
  ...calls.length ? { toolCalls: calls.map((item) => ({
1699
1900
  id: item.id,
1700
1901
  name: item.name,
@@ -1704,7 +1905,8 @@ var HarnessRuntime = class {
1704
1905
  },
1705
1906
  usage: amount,
1706
1907
  finishReason: reason === "tool_calls" || reason === "length" || reason === "content_filter" ? reason : "stop",
1707
- raw: { streamed: true }
1908
+ raw: { streamed: true },
1909
+ ...publicMessages ? { publicMessages } : {}
1708
1910
  };
1709
1911
  await runAfterModel(middleware, response, callCtx ?? ctx);
1710
1912
  succeeded = true;
@@ -1808,14 +2010,47 @@ var HarnessRuntime = class {
1808
2010
  canonical.push(...structuredClone(messages));
1809
2011
  },
1810
2012
  emit: (payload) => {
1811
- ensureActive();
2013
+ if (payload.type !== "compaction.failed" || terminal) ensureActive();
1812
2014
  if (Buffer.byteLength(JSON.stringify(payload)) > 65536) throw new Error("Generic event exceeds 64KB; store an artifact reference instead");
1813
2015
  events.append(payload);
1814
2016
  },
1815
- takeInput: () => {
2017
+ finishInput: (context) => {
2018
+ if (context.runId === request.runId) acceptingSteering = false;
2019
+ },
2020
+ publishMessage: (event) => {
2021
+ if (terminal) return;
2022
+ if (event.type === "message.completed" && Buffer.byteLength(JSON.stringify(event)) > 6e4) {
2023
+ const artifactId = (0, node_crypto.randomUUID)();
2024
+ this.artifacts.set(artifactId, {
2025
+ owner: services.sessionKey,
2026
+ value: structuredClone(event.message)
2027
+ });
2028
+ events.append({
2029
+ ...event,
2030
+ message: {
2031
+ ...event.message,
2032
+ text: ""
2033
+ },
2034
+ artifactId
2035
+ });
2036
+ } else events.append(event);
2037
+ },
2038
+ requestInput: (question) => {
2039
+ ensureActive();
2040
+ return track(liveInput.requestInput(question));
2041
+ },
2042
+ takeInput: (context) => {
1816
2043
  ensureActive();
2044
+ if (context && context.runId !== request.runId) return void 0;
1817
2045
  const index = inbox.findIndex((item) => item.mode === "steer");
1818
- return index < 0 ? void 0 : inbox.splice(index, 1)[0];
2046
+ if (index < 0) return void 0;
2047
+ const item = inbox.splice(index, 1)[0];
2048
+ events.append({
2049
+ type: "input.applied",
2050
+ inputId: item.id,
2051
+ mode: "steer"
2052
+ });
2053
+ return item;
1819
2054
  },
1820
2055
  resource: (id, scope, initialize) => track((async () => {
1821
2056
  ensureActive();
@@ -1847,7 +2082,9 @@ var HarnessRuntime = class {
1847
2082
  let current = request;
1848
2083
  while (true) {
1849
2084
  ensureActive();
2085
+ acceptingSteering = true;
1850
2086
  final = await config.driver.start(current, services);
2087
+ acceptingSteering = false;
1851
2088
  if (streams.size) throw new Error("Driver returned with an unfinished model stream");
1852
2089
  ensureActive();
1853
2090
  if (!final || typeof final.text !== "string" || final.status !== void 0 && ![
@@ -1910,11 +2147,18 @@ var HarnessRuntime = class {
1910
2147
  }
1911
2148
  const followUp = inbox.findIndex((item) => item.mode === "follow_up");
1912
2149
  if (followUp < 0) break;
2150
+ const item = inbox.splice(followUp, 1)[0];
2151
+ events.append({
2152
+ type: "input.applied",
2153
+ inputId: item.id,
2154
+ mode: "follow_up"
2155
+ });
1913
2156
  current = {
1914
2157
  ...request,
1915
- input: inbox.splice(followUp, 1)[0].input
2158
+ input: item.input
1916
2159
  };
1917
2160
  }
2161
+ acceptingInput = false;
1918
2162
  const rejected = (await Promise.allSettled([...owned])).find((entry) => entry.status === "rejected");
1919
2163
  if (rejected?.status === "rejected") throw rejected.reason;
1920
2164
  ensureActive();
@@ -1935,6 +2179,8 @@ var HarnessRuntime = class {
1935
2179
  }
1936
2180
  };
1937
2181
  } finally {
2182
+ acceptingInput = false;
2183
+ liveInput.close();
1938
2184
  acceptingOperations = false;
1939
2185
  await Promise.allSettled([...streams].map((stream) => stream.return(void 0)));
1940
2186
  await Promise.allSettled([...owned]);
@@ -1971,8 +2217,8 @@ var HarnessRuntime = class {
1971
2217
  ...final,
1972
2218
  status: "cancelled",
1973
2219
  reason: {
1974
- code: "cancelled",
1975
- message: "Cancellation acknowledged"
2220
+ code: cancellationCode,
2221
+ message: controller.signal.reason instanceof Error ? controller.signal.reason.message : "Cancellation acknowledged"
1976
2222
  }
1977
2223
  };
1978
2224
  const terminalResult = {
@@ -2026,21 +2272,39 @@ var HarnessRuntime = class {
2026
2272
  });
2027
2273
  return {
2028
2274
  runId,
2275
+ get state() {
2276
+ return terminal ? "finished" : controller.signal.aborted ? "cancelling" : liveInput.request ? "awaiting_input" : "running";
2277
+ },
2278
+ get pendingInput() {
2279
+ return liveInput.request;
2280
+ },
2281
+ reply: async (requestId, input) => {
2282
+ if (!acceptingInput) throw new HarnessInputError("run_finished", "Run is no longer active");
2283
+ liveInput.reply(requestId, input);
2284
+ },
2029
2285
  events: (options) => events.events(options),
2030
2286
  result: () => result.then((value) => structuredClone(value)),
2031
2287
  cancel,
2032
2288
  send: async (input, options) => {
2033
- if (terminal || controller.signal.aborted) throw new Error("Run is no longer active");
2289
+ if (!acceptingInput || options.mode === "steer" && !acceptingSteering || terminal || controller.signal.aborted) throw new HarnessInputError("run_finished", "Run is no longer active");
2034
2290
  if (!config.driver.capabilities.controls.includes(options.mode)) throw new HarnessUnsupportedError(options.mode);
2035
2291
  if (inbox.length >= 32) throw new Error("Run input queue is full");
2036
- inbox.push({
2037
- input: structuredClone(input),
2292
+ if (options.mode === "replace") throw new HarnessUnsupportedError(options.mode);
2293
+ const item = {
2294
+ id: (0, node_crypto.randomUUID)(),
2295
+ input: validateRunInput(input),
2038
2296
  mode: options.mode
2039
- });
2297
+ };
2298
+ inbox.push(item);
2040
2299
  events.append({
2041
2300
  type: "control",
2042
2301
  operation: options.mode
2043
2302
  });
2303
+ events.append({
2304
+ type: "input.received",
2305
+ inputId: item.id,
2306
+ mode: options.mode
2307
+ });
2044
2308
  }
2045
2309
  };
2046
2310
  }
@@ -2058,6 +2322,12 @@ Object.defineProperty(exports, "HarnessEventGapError", {
2058
2322
  return HarnessEventGapError;
2059
2323
  }
2060
2324
  });
2325
+ Object.defineProperty(exports, "HarnessInputError", {
2326
+ enumerable: true,
2327
+ get: function() {
2328
+ return HarnessInputError;
2329
+ }
2330
+ });
2061
2331
  Object.defineProperty(exports, "HarnessResourcePool", {
2062
2332
  enumerable: true,
2063
2333
  get: function() {