@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.
- package/CONVERSATIONAL-RUNS.md +173 -0
- package/README.md +13 -5
- package/dist/{driver-Cr2BGG4h.cjs → driver-Bh_wLWCP.cjs} +285 -15
- package/dist/{driver-BvzvqvpB.js → driver-reTLBJ_o.js} +280 -16
- package/dist/drivers.d.ts.map +1 -1
- package/dist/index.cjs +103 -46
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +103 -48
- package/dist/input-tool.d.cts +7 -0
- package/dist/input-tool.d.ts +7 -0
- package/dist/input-tool.d.ts.map +1 -0
- package/dist/policies.d.cts +2 -0
- package/dist/policies.d.ts +2 -0
- package/dist/policies.d.ts.map +1 -1
- package/dist/runtime/context-policy.d.cts +1 -0
- package/dist/runtime/context-policy.d.ts +1 -0
- package/dist/runtime/context-policy.d.ts.map +1 -1
- package/dist/runtime/driver.d.cts +14 -3
- package/dist/runtime/driver.d.ts +14 -3
- package/dist/runtime/driver.d.ts.map +1 -1
- package/dist/runtime/events.d.cts +35 -2
- package/dist/runtime/events.d.ts +35 -2
- package/dist/runtime/events.d.ts.map +1 -1
- package/dist/runtime/input.d.cts +46 -0
- package/dist/runtime/input.d.ts +46 -0
- package/dist/runtime/input.d.ts.map +1 -0
- package/dist/testing.cjs +1 -1
- package/dist/testing.js +1 -1
- package/package.json +4 -3
|
@@ -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.
|
|
275
|
-
`steer`
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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)
|
|
1390
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
2037
|
-
|
|
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() {
|