@telnyx/agent-harness 0.1.0-beta.0 → 0.1.0-beta.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.
- package/README.md +51 -1
- package/dist/harness.d.ts +4 -1
- package/dist/harness.js +184 -33
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/speculative.d.ts +36 -0
- package/dist/speculative.js +272 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ execution. The Node-only test adapter is intentionally isolated to a subpath.
|
|
|
7
7
|
## Beta and release gate
|
|
8
8
|
|
|
9
9
|
This source change does not publish a package. The planned initial release is
|
|
10
|
-
`0.1.0-beta.
|
|
10
|
+
`0.1.0-beta.1` under the `beta` tag. The package remains `UNLICENSED`; an owner
|
|
11
11
|
must approve npm organization/package ownership, distribution terms, and trusted
|
|
12
12
|
publisher bootstrap before the main-only publication workflow is dispatched. A
|
|
13
13
|
registry-installed `@telnyx/edge-runtime@0.15.2` is required: the runtime
|
|
@@ -61,6 +61,56 @@ const harness = createHarness({
|
|
|
61
61
|
const result = await harness.turn("Summarize this input");
|
|
62
62
|
console.log(result.text);
|
|
63
63
|
```
|
|
64
|
+
## Speculative turns (begin / commit / discard)
|
|
65
|
+
|
|
66
|
+
`harness.begin(input, { signal })` starts the model once its activation is available and returns a
|
|
67
|
+
speculative turn handle. Nothing durable is written until `commit(confirmed)`
|
|
68
|
+
is called — no user row, no assistant row, no tool-result row, no SQL run row.
|
|
69
|
+
The handle exposes a `textStream` (async iterable of text deltas), a `result`
|
|
70
|
+
promise, `commit(confirmedText)`, and `discard()`.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
const turn = harness.begin("guess", { signal });
|
|
74
|
+
// confirmedTranscript is the recognizer's Promise<string | undefined>.
|
|
75
|
+
// Confirmation must run concurrently: stream completion can wait for commit.
|
|
76
|
+
const commitment = confirmedTranscript.then((text) =>
|
|
77
|
+
text === undefined ? turn.discard() : turn.commit(text));
|
|
78
|
+
void commitment.catch(() => {}); // observe even if the consumer exits early
|
|
79
|
+
try {
|
|
80
|
+
for await (const delta of turn.textStream) prepareProvisionalText(delta);
|
|
81
|
+
await commitment;
|
|
82
|
+
await turn.result;
|
|
83
|
+
} finally {
|
|
84
|
+
turn.discard(); // no-op if commitment already won
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`commit` publishes the confirmed text (not the guess) as the user row, then
|
|
89
|
+
allows completed assistant/tool steps to publish. In-flight model work continues
|
|
90
|
+
without restarting; all tools are deferred until commitment. Provisional text
|
|
91
|
+
can be synthesized early, but audio must be held until confirmation. `discard`
|
|
92
|
+
before commitment aborts the model, fences late callbacks, and leaves no durable trace; the next
|
|
93
|
+
turn's `seq` follows the previous confirmed turn's with no hole. All tools —
|
|
94
|
+
including read-only tools — wait until commit; there is no tool opt-in
|
|
95
|
+
framework. Durable step contexts, pre-model lifecycle hooks, turn/model-step
|
|
96
|
+
middleware, and structured output fail closed on `begin`; use `turn` for those.
|
|
97
|
+
A static output or any nonempty construction-captured `outputs` catalog makes
|
|
98
|
+
`begin` refuse synchronously, even with no runtime output currently selected.
|
|
99
|
+
The default voice hook waits for confirmation and falls back to `turn` on that
|
|
100
|
+
refusal; absent or empty captured catalogs still permit speculation.
|
|
101
|
+
Unread text is cleared when its single consumer returns. Total text is limited
|
|
102
|
+
to 1,048,576 UTF-8 bytes / 16,384 deltas, failing with `RangeError` on excess.
|
|
103
|
+
Always commit/discard/abort held work. Cancellation cannot undo issued host
|
|
104
|
+
effects; they retain activation ownership until they settle. Recursive turns
|
|
105
|
+
across that exclusive reservation are rejected rather than deadlocking.
|
|
106
|
+
An error after commit may leave partial history, and repeated commit does not retry.
|
|
107
|
+
Speculation reserves exclusive ownership in FIFO order: an already-active
|
|
108
|
+
ordinary turn on the same activation can delay the voice model start even after
|
|
109
|
+
confirmation. All earlier ordinary turns must finish first; later turns remain
|
|
110
|
+
blocked until exclusive ownership is released. Ordinary turns run concurrently
|
|
111
|
+
only when no exclusive reservation intervenes. Other activations are unaffected.
|
|
112
|
+
See the [transactional voice contract](https://github.com/team-telnyx/edge-compute-statefulactor/blob/compute-953/transactional-voice-turn/docs/agent-harness/transactional-voice-turn.md)
|
|
113
|
+
for the full design.
|
|
64
114
|
|
|
65
115
|
A harness captures its ports, model, instructions, and tool map at construction.
|
|
66
116
|
Each completed model step is committed before callbacks run. This beta does not
|
package/dist/harness.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { type HarnessCheckpoint, type HarnessRunLedger } from "./durable.js";
|
|
|
5
5
|
import { type HarnessMiddleware } from "./lifecycle.js";
|
|
6
6
|
import { type HarnessRuntimeConfig } from "./runtime-config.js";
|
|
7
7
|
import type { HarnessStepContext } from "./steps.js";
|
|
8
|
+
import { type HarnessSpeculativeTurn } from "./speculative.js";
|
|
9
|
+
export { HarnessSpeculationConflictError, HarnessSpeculationUnsupportedError, type HarnessSpeculativeTurn, } from "./speculative.js";
|
|
8
10
|
/** Structural subset of an AI SDK output definition used for parsed-result inference. */
|
|
9
11
|
interface HarnessOutputDefinition<Parsed = unknown> {
|
|
10
12
|
parseCompleteOutput(options: {
|
|
@@ -189,11 +191,12 @@ export interface HarnessTurnOptions<TOOLS extends ToolSet = ToolSet, OutputDefin
|
|
|
189
191
|
}
|
|
190
192
|
/** Portable harness constructed by {@link createHarness}. */
|
|
191
193
|
export interface Harness<TOOLS extends ToolSet = ToolSet, OutputDefinition extends HarnessOutputDefinition | undefined = undefined> extends HarnessRunLedger {
|
|
194
|
+
/** Start a provisional streaming direct turn, with no writes or tools until commitment. */
|
|
195
|
+
begin(input: string, options?: HarnessTurnOptions<TOOLS, OutputDefinition>): HarnessSpeculativeTurn<HarnessTurnResult<HarnessParsedOutput<OutputDefinition>>>;
|
|
192
196
|
turn(input: string, options?: HarnessTurnOptions<TOOLS, OutputDefinition>): Promise<HarnessTurnResult<HarnessParsedOutput<OutputDefinition>>>;
|
|
193
197
|
/** Durable, authorization-gated approval inspection and decisions. */
|
|
194
198
|
readonly approvals: HarnessApprovalLedger;
|
|
195
199
|
}
|
|
196
200
|
/** Construct a portable AI SDK v6 turn engine over an explicit ports object. */
|
|
197
201
|
export declare function createHarness<State extends Record<string, unknown>, TOOLS extends ToolSet, OutputDefinition extends HarnessOutputDefinition | undefined = undefined>(spec: HarnessSpec<State, TOOLS, OutputDefinition>): Harness<TOOLS, OutputDefinition>;
|
|
198
|
-
export {};
|
|
199
202
|
//# sourceMappingURL=harness.d.ts.map
|
package/dist/harness.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GatewayError } from "@ai-sdk/gateway";
|
|
2
|
-
import { AISDKError, APICallError, RetryError, asSchema, generateText, jsonSchema, stepCountIs, wrapLanguageModel, } from "ai";
|
|
2
|
+
import { AISDKError, APICallError, RetryError, asSchema, generateText, streamText, jsonSchema, stepCountIs, wrapLanguageModel, } from "ai";
|
|
3
3
|
import { z } from "zod/v4";
|
|
4
4
|
import { isToolOutputSerializationFailure } from "./steps.js";
|
|
5
5
|
import { HarnessApprovalError, createHarnessApprovalExecutionLedger, createHarnessApprovalMessageJournal, } from "./approvals.js";
|
|
@@ -7,9 +7,17 @@ import { createHarnessRunExecutionLedger, HarnessApprovalPause, } from "./durabl
|
|
|
7
7
|
import { runMiddleware } from "./lifecycle.js";
|
|
8
8
|
import { withHarnessToolContext } from "./tool-context.js";
|
|
9
9
|
import { resolveHarnessRuntimeConfig, } from "./runtime-config.js";
|
|
10
|
+
import { HarnessSpeculationUnsupportedError, serializeTurn, speculativeTurn, } from "./speculative.js";
|
|
11
|
+
export { HarnessSpeculationConflictError, HarnessSpeculationUnsupportedError, } from "./speculative.js";
|
|
10
12
|
const maxStepsSchema = z.number().int().positive();
|
|
11
13
|
const TOOL_OUTPUT_PREFIX = "\u001e@telnyx/agent-harness:tool-output:v1:";
|
|
12
14
|
const DURABLE_TOOL_STEP_VERSION = "tool-execution-v1";
|
|
15
|
+
function assertSpeculativeSdkHooks() {
|
|
16
|
+
if (globalThis.AI_SDK_TELEMETRY_INTEGRATIONS?.length
|
|
17
|
+
|| typeof globalThis.AI_SDK_LOG_WARNINGS === "function") {
|
|
18
|
+
throw new HarnessSpeculationUnsupportedError("global AI SDK lifecycle integrations or custom warning callbacks");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
13
21
|
/** Raised after the final allowed model step still requests executable tools. */
|
|
14
22
|
export class HarnessStepLimitError extends Error {
|
|
15
23
|
maxSteps;
|
|
@@ -66,6 +74,41 @@ export class HarnessObserverError extends Error {
|
|
|
66
74
|
this.name = "HarnessObserverError";
|
|
67
75
|
}
|
|
68
76
|
}
|
|
77
|
+
async function cancelTurn(onCancel, cancellation = new HarnessCancellationError()) {
|
|
78
|
+
try {
|
|
79
|
+
await onCancel?.(cancellation);
|
|
80
|
+
}
|
|
81
|
+
catch (observer) {
|
|
82
|
+
throw new HarnessObserverError(cancellation, observer, "cancel");
|
|
83
|
+
}
|
|
84
|
+
throw cancellation;
|
|
85
|
+
}
|
|
86
|
+
function captureSpeculativeOptions(options) {
|
|
87
|
+
// Inspect descriptors, not values: spreading invokes getters. A detached
|
|
88
|
+
// null-prototype snapshot cannot inherit hooks installed while queued.
|
|
89
|
+
const supported = new Set(["signal", "onStepCommitted", "onOutput"]);
|
|
90
|
+
const unsupported = new Set(["steps", "callbacks", "onCancel", "onError"]);
|
|
91
|
+
if (options === null || typeof options !== "object"
|
|
92
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(options))) {
|
|
93
|
+
throw new HarnessSpeculationUnsupportedError("non-plain turn options");
|
|
94
|
+
}
|
|
95
|
+
const captured = Object.create(null);
|
|
96
|
+
for (const key of Reflect.ownKeys(options)) {
|
|
97
|
+
const descriptor = Object.getOwnPropertyDescriptor(options, key);
|
|
98
|
+
if (typeof key !== "string" || !descriptor.enumerable || !("value" in descriptor)
|
|
99
|
+
|| (!supported.has(key) && !unsupported.has(key))
|
|
100
|
+
|| (unsupported.has(key) && descriptor.value !== undefined)) {
|
|
101
|
+
throw new HarnessSpeculationUnsupportedError("accessors, inherited options, or pre-model lifecycle hooks");
|
|
102
|
+
}
|
|
103
|
+
if (supported.has(key)) {
|
|
104
|
+
if (key !== "signal" && descriptor.value !== undefined && typeof descriptor.value !== "function") {
|
|
105
|
+
throw new HarnessSpeculationUnsupportedError("non-function observers");
|
|
106
|
+
}
|
|
107
|
+
Object.defineProperty(captured, key, descriptor);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze(captured);
|
|
111
|
+
}
|
|
69
112
|
function historyMessages(stored) {
|
|
70
113
|
return stored.map((message) => {
|
|
71
114
|
if (message.role === "assistant") {
|
|
@@ -652,9 +695,10 @@ function validateTools(tools, authorization) {
|
|
|
652
695
|
}
|
|
653
696
|
/** Construct a portable AI SDK v6 turn engine over an explicit ports object. */
|
|
654
697
|
export function createHarness(spec) {
|
|
698
|
+
const activation = spec.ports.activation;
|
|
655
699
|
const messagePort = spec.ports.messages;
|
|
656
|
-
const
|
|
657
|
-
const
|
|
700
|
+
const appendMessagePort = messagePort.append.bind(messagePort);
|
|
701
|
+
const appendMessagesPort = messagePort.appendMany.bind(messagePort);
|
|
658
702
|
const allMessages = messagePort.all.bind(messagePort);
|
|
659
703
|
const getState = spec.ports.state.get.bind(spec.ports.state);
|
|
660
704
|
const model = spec.model;
|
|
@@ -697,22 +741,24 @@ export function createHarness(spec) {
|
|
|
697
741
|
let lastValidRuntimeConfig = resolveHarnessRuntimeConfig(staticRuntimeConfig, undefined).config;
|
|
698
742
|
let runtimeConfigReadSequence = 0;
|
|
699
743
|
let lastPublishedRuntimeConfigReadSequence = 0;
|
|
700
|
-
const
|
|
744
|
+
const executeTurn = async (input, options = {}, speculation) => {
|
|
745
|
+
const committedEffect = (work) => speculation ? speculation.write(work) : work();
|
|
746
|
+
const appendMessage = (message) => speculation
|
|
747
|
+
? speculation.write(() => appendMessagePort(message)) : appendMessagePort(message);
|
|
748
|
+
const appendMessages = (batch) => speculation
|
|
749
|
+
? speculation.write(() => appendMessagesPort(batch)) : appendMessagesPort(batch);
|
|
701
750
|
if (options.signal?.aborted) {
|
|
702
|
-
|
|
703
|
-
try {
|
|
704
|
-
await options.onCancel?.(cancellation);
|
|
705
|
-
}
|
|
706
|
-
catch (observer) {
|
|
707
|
-
throw new HarnessObserverError(cancellation, observer, "cancel");
|
|
708
|
-
}
|
|
709
|
-
throw cancellation;
|
|
751
|
+
return cancelTurn(options.onCancel);
|
|
710
752
|
}
|
|
711
753
|
try {
|
|
712
754
|
const runtimeConfigReadSequenceForTurn = ++runtimeConfigReadSequence;
|
|
713
755
|
const state = await getState();
|
|
756
|
+
speculation?.assertLive();
|
|
714
757
|
const resolution = resolveHarnessRuntimeConfig(staticRuntimeConfig, state.__telnyx_agent_harness, lastValidRuntimeConfig);
|
|
715
758
|
const runtimeConfig = resolution.config;
|
|
759
|
+
if (speculation && runtimeConfig.output !== undefined) {
|
|
760
|
+
throw new HarnessSpeculationUnsupportedError("structured output");
|
|
761
|
+
}
|
|
716
762
|
if (typeof runtimeConfig.model === "string" && lifecycleMiddleware.modelStep.length > 0) {
|
|
717
763
|
throw new TypeError("Harness model-step middleware requires an executable LanguageModel");
|
|
718
764
|
}
|
|
@@ -731,7 +777,11 @@ export function createHarness(spec) {
|
|
|
731
777
|
});
|
|
732
778
|
return await runMiddleware(turnContext, lifecycleMiddleware.turn, async () => {
|
|
733
779
|
await toolSnapshot.preflight(Object.keys(runtimeConfig.tools ?? {}));
|
|
780
|
+
speculation?.assertLive();
|
|
734
781
|
const priorMessages = await allMessages();
|
|
782
|
+
speculation?.assertLive();
|
|
783
|
+
if (speculation)
|
|
784
|
+
assertSpeculativeSdkHooks();
|
|
735
785
|
const inputSteps = options.steps;
|
|
736
786
|
const inputJournal = inputSteps === undefined
|
|
737
787
|
? undefined
|
|
@@ -775,10 +825,29 @@ export function createHarness(spec) {
|
|
|
775
825
|
const replaysJournaledInput = journaledInput !== undefined;
|
|
776
826
|
let journalThrough = replaysJournaledInput
|
|
777
827
|
? priorMessages.at(-1).seq
|
|
778
|
-
: await appendMessage({ role: "user", content: input });
|
|
779
|
-
const storedMessages =
|
|
780
|
-
?
|
|
781
|
-
:
|
|
828
|
+
: speculation ? (priorMessages.at(-1)?.seq ?? 0) : await appendMessage({ role: "user", content: input });
|
|
829
|
+
const storedMessages = speculation
|
|
830
|
+
? [...priorMessages, { role: "user", content: input, seq: journalThrough + 1, at: new Date(spec.ports.clock.now()) }]
|
|
831
|
+
: !replaysJournaledInput
|
|
832
|
+
? await allMessages()
|
|
833
|
+
: priorMessages;
|
|
834
|
+
// The append-only port allocates the confirmed input's actual sequence.
|
|
835
|
+
// No speculative reservation can conflict with unrelated message writers.
|
|
836
|
+
let publication;
|
|
837
|
+
let confirmedForModel;
|
|
838
|
+
const confirmedMessages = (batch) => confirmedForModel === undefined ? batch : batch.map((message, index) => index === priorMessages.length ? { role: "user", content: confirmedForModel } : message);
|
|
839
|
+
const publishInput = () => {
|
|
840
|
+
if (!speculation)
|
|
841
|
+
return Promise.resolve();
|
|
842
|
+
publication ??= (async () => {
|
|
843
|
+
const confirmed = await speculation.confirmed();
|
|
844
|
+
speculation.assertLive();
|
|
845
|
+
confirmedForModel = confirmed;
|
|
846
|
+
journalThrough = await appendMessage({ role: "user", content: confirmed });
|
|
847
|
+
speculation.assertLive();
|
|
848
|
+
})();
|
|
849
|
+
return publication;
|
|
850
|
+
};
|
|
782
851
|
const restoredApprovals = options.steps === undefined
|
|
783
852
|
? []
|
|
784
853
|
: await approvalMessages.restoreRun(options.steps.runId);
|
|
@@ -825,9 +894,18 @@ export function createHarness(spec) {
|
|
|
825
894
|
let modelStepNumber = 0;
|
|
826
895
|
const limitReached = stepCountIs(runtimeConfig.limits.steps);
|
|
827
896
|
const assertBoundary = () => {
|
|
897
|
+
speculation?.assertLive();
|
|
828
898
|
if (boundaryFailed)
|
|
829
899
|
throw boundaryError;
|
|
830
900
|
};
|
|
901
|
+
if (speculation)
|
|
902
|
+
void publishInput().catch((error) => {
|
|
903
|
+
if (!boundaryFailed) {
|
|
904
|
+
boundaryFailed = true;
|
|
905
|
+
boundaryError = error;
|
|
906
|
+
}
|
|
907
|
+
speculation.fail(error);
|
|
908
|
+
});
|
|
831
909
|
const modelForTurn = lifecycleMiddleware.modelStep.length === 0 || typeof runtimeConfig.model === "string"
|
|
832
910
|
? runtimeConfig.model
|
|
833
911
|
: wrapLanguageModel({
|
|
@@ -847,7 +925,21 @@ export function createHarness(spec) {
|
|
|
847
925
|
toolName,
|
|
848
926
|
Object.freeze({
|
|
849
927
|
...candidate,
|
|
928
|
+
...(speculation ? {
|
|
929
|
+
inputSchema: jsonSchema(async () => await asSchema(candidate.inputSchema).jsonSchema, {
|
|
930
|
+
validate: async (value) => {
|
|
931
|
+
await publishInput();
|
|
932
|
+
assertBoundary();
|
|
933
|
+
const schema = asSchema(candidate.inputSchema);
|
|
934
|
+
return schema.validate
|
|
935
|
+
? await committedEffect(async () => await schema.validate(value))
|
|
936
|
+
: { success: true, value };
|
|
937
|
+
},
|
|
938
|
+
}),
|
|
939
|
+
} : {}),
|
|
850
940
|
async execute(...args) {
|
|
941
|
+
await publishInput();
|
|
942
|
+
assertBoundary();
|
|
851
943
|
const toolCallOptions = args[1];
|
|
852
944
|
const toolCallId = typeof toolCallOptions?.toolCallId === "string"
|
|
853
945
|
? toolCallOptions.toolCallId
|
|
@@ -862,13 +954,14 @@ export function createHarness(spec) {
|
|
|
862
954
|
let coreCompleted = false;
|
|
863
955
|
let replayingCompletedApproval = false;
|
|
864
956
|
try {
|
|
865
|
-
const output = await runMiddleware(Object.freeze({
|
|
957
|
+
const output = await committedEffect(() => runMiddleware(Object.freeze({
|
|
866
958
|
phase: "tool",
|
|
867
959
|
config: runtimeConfig,
|
|
868
960
|
toolName,
|
|
869
961
|
toolCallId,
|
|
870
962
|
signal: options.signal,
|
|
871
963
|
}), lifecycleMiddleware.tool, async () => {
|
|
964
|
+
assertBoundary();
|
|
872
965
|
const stepName = `tool:${toolName}:${durableToolCallOrdinal}`;
|
|
873
966
|
const stepVersion = spec.durability?.toolStepVersion ?? DURABLE_TOOL_STEP_VERSION;
|
|
874
967
|
const restoringCompletedApproval = options.steps !== undefined
|
|
@@ -881,12 +974,20 @@ export function createHarness(spec) {
|
|
|
881
974
|
? undefined
|
|
882
975
|
: await approvals.reauthorizeEffect(options.steps.runId, toolCallId, toolName, args[0]);
|
|
883
976
|
const approvalId = authorizedEffect?.id;
|
|
884
|
-
|
|
977
|
+
let effectArgs = authorizedEffect === undefined
|
|
885
978
|
? args
|
|
886
979
|
: [authorizedEffect.input, ...args.slice(1)];
|
|
980
|
+
if (speculation) {
|
|
981
|
+
const callOptions = objectRecord(effectArgs[1]);
|
|
982
|
+
if (Array.isArray(callOptions?.messages))
|
|
983
|
+
effectArgs = [effectArgs[0], {
|
|
984
|
+
...callOptions, messages: confirmedMessages(callOptions.messages),
|
|
985
|
+
}, ...effectArgs.slice(2)];
|
|
986
|
+
}
|
|
987
|
+
assertBoundary();
|
|
887
988
|
coreStarted = true;
|
|
888
989
|
const result = options.steps === undefined
|
|
889
|
-
? cloneToolOutput(toolName, await execute.apply(this,
|
|
990
|
+
? cloneToolOutput(toolName, await committedEffect(async () => await execute.apply(this, effectArgs)))
|
|
890
991
|
: await options.steps.step(stepName, async () => await withHarnessToolContext(Object.freeze({ runId: options.steps.runId, toolName, ordinal: durableToolCallOrdinal }), async () => await execute.apply(this, effectArgs)), {
|
|
891
992
|
external: true,
|
|
892
993
|
beforeEffect: () => approvals.consumeAuthorizedEffect(options.steps.runId, approvalId),
|
|
@@ -895,7 +996,7 @@ export function createHarness(spec) {
|
|
|
895
996
|
});
|
|
896
997
|
coreCompleted = true;
|
|
897
998
|
return result;
|
|
898
|
-
});
|
|
999
|
+
}));
|
|
899
1000
|
const restoredCall = toolCallId === undefined ? undefined : restoredApprovalCalls.get(toolCallId);
|
|
900
1001
|
if (restoredCall !== undefined) {
|
|
901
1002
|
const approvedOutput = typeof output === "string"
|
|
@@ -937,7 +1038,7 @@ export function createHarness(spec) {
|
|
|
937
1038
|
durableSteps.nextToolCallOrdinal(event.toolCall.toolName, event.toolCall.toolCallId);
|
|
938
1039
|
await options.callbacks?.onToolCallStart?.(event);
|
|
939
1040
|
};
|
|
940
|
-
const
|
|
1041
|
+
const generationOptions = {
|
|
941
1042
|
model: modelForTurn,
|
|
942
1043
|
tools: toolsForTurn,
|
|
943
1044
|
...(runtimeConfig.output === undefined ? {} : { output: runtimeConfig.output }),
|
|
@@ -948,9 +1049,9 @@ export function createHarness(spec) {
|
|
|
948
1049
|
experimental_onStepStart: options.callbacks?.onStepStart,
|
|
949
1050
|
experimental_onToolCallStart: onToolCallStart,
|
|
950
1051
|
experimental_onToolCallFinish: options.callbacks?.onToolCallFinish,
|
|
951
|
-
prepareStep() {
|
|
1052
|
+
prepareStep({ messages: stepMessages }) {
|
|
952
1053
|
assertBoundary();
|
|
953
|
-
return undefined;
|
|
1054
|
+
return speculation && confirmedForModel !== undefined ? { messages: confirmedMessages(stepMessages) } : undefined;
|
|
954
1055
|
},
|
|
955
1056
|
async stopWhen(context) {
|
|
956
1057
|
assertBoundary();
|
|
@@ -961,6 +1062,7 @@ export function createHarness(spec) {
|
|
|
961
1062
|
},
|
|
962
1063
|
async onStepFinish(step) {
|
|
963
1064
|
try {
|
|
1065
|
+
await publishInput();
|
|
964
1066
|
assertBoundary();
|
|
965
1067
|
const approvalRequests = step.content.filter((part) => part.type === "tool-approval-request");
|
|
966
1068
|
const pendingRequests = options.steps === undefined ? [] : await Promise.all(approvalRequests.map(async (request) => replayedCompletedApprovalToolCallIds.has(request.toolCall.toolCallId)
|
|
@@ -1042,7 +1144,12 @@ export function createHarness(spec) {
|
|
|
1042
1144
|
projectedToolCallIds.add(call.toolCallId);
|
|
1043
1145
|
for (const call of restoredCalls)
|
|
1044
1146
|
projectedRestoredToolCallIds.add(call.toolCallId);
|
|
1045
|
-
|
|
1147
|
+
assertBoundary();
|
|
1148
|
+
if (options.onStepCommitted) {
|
|
1149
|
+
const snapshot = observedStep(step, journalThrough);
|
|
1150
|
+
await committedEffect(async () => { await options.onStepCommitted(snapshot); });
|
|
1151
|
+
}
|
|
1152
|
+
assertBoundary();
|
|
1046
1153
|
await options.callbacks?.onStepFinish?.(step);
|
|
1047
1154
|
}
|
|
1048
1155
|
catch (error) {
|
|
@@ -1054,7 +1161,38 @@ export function createHarness(spec) {
|
|
|
1054
1161
|
}
|
|
1055
1162
|
},
|
|
1056
1163
|
onFinish: options.callbacks?.onFinish,
|
|
1057
|
-
}
|
|
1164
|
+
};
|
|
1165
|
+
const generation = speculation ? (async () => {
|
|
1166
|
+
let streamFailure;
|
|
1167
|
+
let streamFailed = false;
|
|
1168
|
+
const onStreamError = (error) => {
|
|
1169
|
+
streamFailed = true;
|
|
1170
|
+
streamFailure = boundaryFailed ? boundaryError : isProviderFailure(error)
|
|
1171
|
+
? new HarnessProviderError(providerDiagnostics(runtimeConfig.model, error)) : error;
|
|
1172
|
+
speculation.fail(streamFailure);
|
|
1173
|
+
};
|
|
1174
|
+
const streamed = streamText({
|
|
1175
|
+
...generationOptions,
|
|
1176
|
+
onError: ({ error }) => onStreamError(error),
|
|
1177
|
+
onChunk: ({ chunk }) => {
|
|
1178
|
+
assertBoundary();
|
|
1179
|
+
const part = objectRecord(chunk);
|
|
1180
|
+
if (part?.type === "text-delta" && typeof part.text === "string")
|
|
1181
|
+
speculation.text(part.text);
|
|
1182
|
+
},
|
|
1183
|
+
});
|
|
1184
|
+
await streamed.consumeStream({ onError: onStreamError });
|
|
1185
|
+
assertBoundary();
|
|
1186
|
+
if (streamFailed)
|
|
1187
|
+
throw streamFailure;
|
|
1188
|
+
return {
|
|
1189
|
+
text: await streamed.text,
|
|
1190
|
+
steps: await streamed.steps,
|
|
1191
|
+
finishReason: await streamed.finishReason,
|
|
1192
|
+
totalUsage: await streamed.totalUsage,
|
|
1193
|
+
output: undefined,
|
|
1194
|
+
};
|
|
1195
|
+
})() : generateText(generationOptions);
|
|
1058
1196
|
let result;
|
|
1059
1197
|
try {
|
|
1060
1198
|
result = await generation;
|
|
@@ -1089,7 +1227,9 @@ export function createHarness(spec) {
|
|
|
1089
1227
|
? {}
|
|
1090
1228
|
: { output: result.output }),
|
|
1091
1229
|
});
|
|
1092
|
-
|
|
1230
|
+
assertBoundary();
|
|
1231
|
+
if (options.onOutput)
|
|
1232
|
+
await committedEffect(async () => { await options.onOutput(output); });
|
|
1093
1233
|
return output;
|
|
1094
1234
|
});
|
|
1095
1235
|
}
|
|
@@ -1102,13 +1242,7 @@ export function createHarness(spec) {
|
|
|
1102
1242
|
const classified = error instanceof HarnessCancellationError
|
|
1103
1243
|
? error
|
|
1104
1244
|
: new HarnessCancellationError();
|
|
1105
|
-
|
|
1106
|
-
await options.onCancel?.(classified);
|
|
1107
|
-
}
|
|
1108
|
-
catch (observer) {
|
|
1109
|
-
throw new HarnessObserverError(classified, observer, "cancel");
|
|
1110
|
-
}
|
|
1111
|
-
throw classified;
|
|
1245
|
+
return cancelTurn(options.onCancel, classified);
|
|
1112
1246
|
}
|
|
1113
1247
|
try {
|
|
1114
1248
|
await options.onError?.(error);
|
|
@@ -1119,6 +1253,22 @@ export function createHarness(spec) {
|
|
|
1119
1253
|
throw error;
|
|
1120
1254
|
}
|
|
1121
1255
|
};
|
|
1256
|
+
const turn = (input, options = {}) => serializeTurn(activation, () => executeTurn(input, options), false, options.signal, () => cancelTurn(options.onCancel));
|
|
1257
|
+
const begin = (input, options = {}) => {
|
|
1258
|
+
const capturedOptions = captureSpeculativeOptions(options);
|
|
1259
|
+
assertSpeculativeSdkHooks();
|
|
1260
|
+
// A captured catalog can select output from async runtime state. Refuse
|
|
1261
|
+
// before returning a handle so voice can safely fall back after confirmation.
|
|
1262
|
+
if (lifecycleMiddleware.turn.length || lifecycleMiddleware.modelStep.length
|
|
1263
|
+
|| staticRuntimeConfig.output !== undefined || Object.keys(outputs).length > 0) {
|
|
1264
|
+
throw new HarnessSpeculationUnsupportedError("durable step contexts, pre-model lifecycle hooks, or structured output");
|
|
1265
|
+
}
|
|
1266
|
+
return speculativeTurn(activation, capturedOptions.signal, () => new HarnessCancellationError(), (control) => executeTurn(input, Object.freeze(Object.assign(Object.create(null), {
|
|
1267
|
+
signal: control.signal,
|
|
1268
|
+
onStepCommitted: capturedOptions.onStepCommitted,
|
|
1269
|
+
onOutput: capturedOptions.onOutput,
|
|
1270
|
+
})), control));
|
|
1271
|
+
};
|
|
1122
1272
|
const ledger = createHarnessRunExecutionLedger(spec.ports, async (input, signal, steps) => {
|
|
1123
1273
|
// Journal correlation enriches scheduling recovery, but it must not turn
|
|
1124
1274
|
// an otherwise executable durable run into a failed one when the journal
|
|
@@ -1209,6 +1359,7 @@ export function createHarness(spec) {
|
|
|
1209
1359
|
});
|
|
1210
1360
|
return Object.freeze({
|
|
1211
1361
|
turn,
|
|
1362
|
+
begin,
|
|
1212
1363
|
accept: ledger.accept,
|
|
1213
1364
|
get: ledger.get,
|
|
1214
1365
|
status: ledger.status,
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export * from "./ports.js";
|
|
3
3
|
export * from "./runtime-adapter.js";
|
|
4
4
|
export * from "./harness.js";
|
|
5
|
+
export { HarnessSpeculationConflictError, HarnessSpeculationUnsupportedError, type HarnessSpeculativeTurn, } from "./speculative.js";
|
|
5
6
|
export { HARNESS_RUN_STATUSES, HarnessApprovalPause, HarnessInjectedCrash, createHarnessRunLedger, ensureHarnessDurabilitySchema, type HarnessCheckpoint, type HarnessRun, type HarnessRunAcceptance, type HarnessRunLedger, type HarnessRunLedgerOptions, type HarnessRunListOptions, type HarnessRunStatus, } from "./durable.js";
|
|
6
7
|
export { HARNESS_APPROVAL_STATUSES, HarnessApprovalError, HarnessAuthorizationError, type HarnessApproval, type HarnessApprovalLedger, type HarnessApprovalStatus, } from "./approvals.js";
|
|
7
8
|
export * from "./steps.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./runtime-adapter.js";
|
|
|
9
9
|
// "@telnyx/agent-harness/node" subpath (internal code from
|
|
10
10
|
// "./node-adapter.js"). The clean-consumer CI check enforces both directions.
|
|
11
11
|
export * from "./harness.js";
|
|
12
|
+
export { HarnessSpeculationConflictError, HarnessSpeculationUnsupportedError, } from "./speculative.js";
|
|
12
13
|
export { HARNESS_RUN_STATUSES, HarnessApprovalPause, HarnessInjectedCrash, createHarnessRunLedger, ensureHarnessDurabilitySchema, } from "./durable.js";
|
|
13
14
|
export { HARNESS_APPROVAL_STATUSES, HarnessApprovalError, HarnessAuthorizationError, } from "./approvals.js";
|
|
14
15
|
export * from "./steps.js";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** In-memory ownership and commitment; never opens a storage transaction. */
|
|
2
|
+
export declare class HarnessSpeculationConflictError extends Error {
|
|
3
|
+
readonly code: "HARNESS_SPECULATION_CONFLICT";
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
export declare class HarnessSpeculationUnsupportedError extends Error {
|
|
7
|
+
readonly code: "HARNESS_SPECULATION_UNSUPPORTED";
|
|
8
|
+
constructor(detail: string);
|
|
9
|
+
}
|
|
10
|
+
/** One speculative direct turn. Text is provisional until commit succeeds. */
|
|
11
|
+
export interface HarnessSpeculativeTurn<Result = {
|
|
12
|
+
text: string;
|
|
13
|
+
}> {
|
|
14
|
+
/** Single-consumer, provisional text. Consuming it must not cause durable effects. */
|
|
15
|
+
readonly textStream: AsyncIterable<string>;
|
|
16
|
+
/** Observed internally even when a caller discards without awaiting it. */
|
|
17
|
+
readonly result: Promise<Result>;
|
|
18
|
+
/** First decision wins. Same text returns the same promise; different text rejects. */
|
|
19
|
+
commit(confirmed: string): Promise<Result>;
|
|
20
|
+
/** True for a discarded turn (including repeats), false after commit won. */
|
|
21
|
+
discard(): boolean;
|
|
22
|
+
}
|
|
23
|
+
/** FIFO exclusive reservations; ordinary callers still run concurrently. */
|
|
24
|
+
export declare function serializeTurn<T>(activation: object, work: () => Promise<T>, exclusive?: boolean, signal?: AbortSignal, onQueuedCancel?: () => Promise<never>): Promise<T>;
|
|
25
|
+
export interface SpeculativeControl {
|
|
26
|
+
readonly signal: AbortSignal;
|
|
27
|
+
assertLive(): void;
|
|
28
|
+
fail(error: unknown): void;
|
|
29
|
+
/** Retain ownership until an issued post-commit host effect settles. */
|
|
30
|
+
write<T>(work: () => Promise<T>): Promise<T>;
|
|
31
|
+
/** Resolves to the confirmed input; rejects on abandonment. */
|
|
32
|
+
confirmed(): Promise<string>;
|
|
33
|
+
text(delta: string): void;
|
|
34
|
+
}
|
|
35
|
+
export declare function speculativeTurn<Result>(activation: object, signal: AbortSignal | undefined, cancellation: () => Error, execute: (control: SpeculativeControl) => Promise<Result>): HarnessSpeculativeTurn<Result>;
|
|
36
|
+
//# sourceMappingURL=speculative.d.ts.map
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
/** In-memory ownership and commitment; never opens a storage transaction. */
|
|
3
|
+
export class HarnessSpeculationConflictError extends Error {
|
|
4
|
+
code = "HARNESS_SPECULATION_CONFLICT";
|
|
5
|
+
constructor() { super("Speculative turn already has a different terminal decision"); this.name = "HarnessSpeculationConflictError"; }
|
|
6
|
+
}
|
|
7
|
+
export class HarnessSpeculationUnsupportedError extends Error {
|
|
8
|
+
code = "HARNESS_SPECULATION_UNSUPPORTED";
|
|
9
|
+
constructor(detail) { super(`Speculation does not support ${detail}`); this.name = "HarnessSpeculationUnsupportedError"; }
|
|
10
|
+
}
|
|
11
|
+
function deferred() {
|
|
12
|
+
let resolve;
|
|
13
|
+
let reject;
|
|
14
|
+
const promise = new Promise((yes, no) => { resolve = yes; reject = no; });
|
|
15
|
+
void promise.catch(() => { });
|
|
16
|
+
return { promise, resolve, reject };
|
|
17
|
+
}
|
|
18
|
+
const contexts = new AsyncLocalStorage();
|
|
19
|
+
const owners = new WeakMap();
|
|
20
|
+
function drain(owner) {
|
|
21
|
+
if (owner.draining)
|
|
22
|
+
return;
|
|
23
|
+
owner.draining = true;
|
|
24
|
+
try {
|
|
25
|
+
while (!owner.exclusive) {
|
|
26
|
+
const next = owner.pending.values().next().value;
|
|
27
|
+
if (!next || (next.exclusive && owner.active > 0))
|
|
28
|
+
return;
|
|
29
|
+
owner.pending.delete(next);
|
|
30
|
+
next.start();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
owner.draining = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** FIFO exclusive reservations; ordinary callers still run concurrently. */
|
|
38
|
+
export function serializeTurn(activation, work, exclusive = true, signal, onQueuedCancel) {
|
|
39
|
+
let owner = owners.get(activation);
|
|
40
|
+
if (!owner) {
|
|
41
|
+
owner = { active: 0, exclusive: false, draining: false, pending: new Set() };
|
|
42
|
+
owners.set(activation, owner);
|
|
43
|
+
}
|
|
44
|
+
const completion = deferred();
|
|
45
|
+
const inherited = contexts.getStore() ?? [];
|
|
46
|
+
if (inherited.some((context) => context.activation === activation && context.active
|
|
47
|
+
&& (exclusive || context.exclusive || [...owner.pending].some((entry) => entry.exclusive)))) {
|
|
48
|
+
completion.reject(new HarnessSpeculationUnsupportedError("recursive turns across an exclusive reservation"));
|
|
49
|
+
return completion.promise;
|
|
50
|
+
}
|
|
51
|
+
const context = { activation, exclusive, active: false };
|
|
52
|
+
const reservation = {
|
|
53
|
+
exclusive,
|
|
54
|
+
start() {
|
|
55
|
+
signal?.removeEventListener("abort", cancel);
|
|
56
|
+
context.active = true;
|
|
57
|
+
owner.active++;
|
|
58
|
+
owner.exclusive = exclusive;
|
|
59
|
+
const release = () => {
|
|
60
|
+
context.active = false;
|
|
61
|
+
owner.active--;
|
|
62
|
+
if (exclusive)
|
|
63
|
+
owner.exclusive = false;
|
|
64
|
+
drain(owner);
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
const result = contexts.run([...inherited, context], work);
|
|
68
|
+
void Promise.resolve(result).then((value) => { release(); completion.resolve(value); }, (error) => { release(); completion.reject(error); });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
release();
|
|
72
|
+
completion.reject(error);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const cancel = () => {
|
|
77
|
+
if (!owner.pending.delete(reservation))
|
|
78
|
+
return;
|
|
79
|
+
signal?.removeEventListener("abort", cancel);
|
|
80
|
+
// Deletion is the pending/start winner. Never run execution or acquire
|
|
81
|
+
// ownership for cancellation, nor inherit the abort dispatcher's context.
|
|
82
|
+
try {
|
|
83
|
+
if (onQueuedCancel) {
|
|
84
|
+
void contexts.run(inherited, onQueuedCancel).then(completion.resolve, completion.reject);
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
completion.reject(signal?.reason);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
completion.reject(error);
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
drain(owner);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
owner.pending.add(reservation);
|
|
97
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
98
|
+
if (signal?.aborted)
|
|
99
|
+
cancel();
|
|
100
|
+
// Register before invoking work, including synchronous reentry.
|
|
101
|
+
else
|
|
102
|
+
drain(owner);
|
|
103
|
+
return completion.promise;
|
|
104
|
+
}
|
|
105
|
+
// The AI SDK also retains stream content, even when our consumer drains it.
|
|
106
|
+
// Bound total text, not just the currently unread queue. A separate chunk cap
|
|
107
|
+
// bounds object overhead for small provider deltas.
|
|
108
|
+
const MAX_TEXT_BYTES = 1_048_576;
|
|
109
|
+
const MAX_TEXT_CHUNKS = 16_384;
|
|
110
|
+
export function speculativeTurn(activation, signal, cancellation, execute) {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const confirmation = deferred();
|
|
113
|
+
const stopped = deferred();
|
|
114
|
+
let decision = "held";
|
|
115
|
+
let confirmedInput;
|
|
116
|
+
let failure;
|
|
117
|
+
let failed = false;
|
|
118
|
+
let ended = false;
|
|
119
|
+
let executionClosed = false;
|
|
120
|
+
let taken = false;
|
|
121
|
+
let textBytes = 0;
|
|
122
|
+
let textChunks = 0;
|
|
123
|
+
let streamClosed = false;
|
|
124
|
+
const chunks = [];
|
|
125
|
+
const writes = new Set();
|
|
126
|
+
let changed = deferred();
|
|
127
|
+
const wake = () => { changed.resolve(); changed = deferred(); };
|
|
128
|
+
const stopWith = (error) => {
|
|
129
|
+
if (ended || controller.signal.aborted)
|
|
130
|
+
return;
|
|
131
|
+
if (decision === "held")
|
|
132
|
+
decision = "discarded";
|
|
133
|
+
failed = true;
|
|
134
|
+
failure = error;
|
|
135
|
+
chunks.length = 0;
|
|
136
|
+
confirmation.reject(error);
|
|
137
|
+
stopped.reject(error);
|
|
138
|
+
controller.abort(error);
|
|
139
|
+
wake();
|
|
140
|
+
};
|
|
141
|
+
const stop = () => stopWith(cancellation());
|
|
142
|
+
if (signal?.aborted)
|
|
143
|
+
stop();
|
|
144
|
+
else
|
|
145
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
146
|
+
const assertLive = () => {
|
|
147
|
+
if (controller.signal.aborted)
|
|
148
|
+
throw failure;
|
|
149
|
+
if (executionClosed)
|
|
150
|
+
throw new Error("Speculative execution already finished");
|
|
151
|
+
};
|
|
152
|
+
const control = {
|
|
153
|
+
signal: controller.signal,
|
|
154
|
+
assertLive,
|
|
155
|
+
fail: stopWith,
|
|
156
|
+
write(work) {
|
|
157
|
+
assertLive();
|
|
158
|
+
if (decision !== "committed")
|
|
159
|
+
throw new Error("Speculative writes require commitment");
|
|
160
|
+
const completion = deferred();
|
|
161
|
+
writes.add(completion.promise);
|
|
162
|
+
void completion.promise.then(() => { writes.delete(completion.promise); }, () => { writes.delete(completion.promise); });
|
|
163
|
+
try {
|
|
164
|
+
void work().then(completion.resolve, completion.reject);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
completion.reject(error);
|
|
168
|
+
}
|
|
169
|
+
return completion.promise;
|
|
170
|
+
},
|
|
171
|
+
confirmed: async () => { const input = await confirmation.promise; assertLive(); return input; },
|
|
172
|
+
text(delta) {
|
|
173
|
+
assertLive();
|
|
174
|
+
textBytes += Buffer.byteLength(delta, "utf8");
|
|
175
|
+
textChunks++;
|
|
176
|
+
if (textBytes > MAX_TEXT_BYTES || textChunks > MAX_TEXT_CHUNKS) {
|
|
177
|
+
stopWith(new RangeError("Speculative text limit exceeded"));
|
|
178
|
+
throw failure;
|
|
179
|
+
}
|
|
180
|
+
if (!delta)
|
|
181
|
+
return;
|
|
182
|
+
if (!streamClosed) {
|
|
183
|
+
chunks.push(delta);
|
|
184
|
+
wake();
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const owned = serializeTurn(activation, async () => {
|
|
189
|
+
assertLive();
|
|
190
|
+
try {
|
|
191
|
+
return await Promise.race([execute(control).catch((error) => {
|
|
192
|
+
stopWith(error);
|
|
193
|
+
throw error;
|
|
194
|
+
}), stopped.promise]);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
executionClosed = true;
|
|
198
|
+
// Issued host effects cannot be undone. Cancellation settles the public
|
|
199
|
+
// result promptly but cannot let a later turn overtake those effects.
|
|
200
|
+
// Recheck: an admitted callback can issue another tracked effect while
|
|
201
|
+
// an earlier batch is settling. The abort fence rejects new admissions.
|
|
202
|
+
while (writes.size)
|
|
203
|
+
await Promise.allSettled([...writes]);
|
|
204
|
+
}
|
|
205
|
+
}, true, controller.signal);
|
|
206
|
+
// Cancellation removes queued reservations and settles active callers
|
|
207
|
+
// promptly, independently of the ownership drain for issued effects.
|
|
208
|
+
const result = Promise.race([owned, stopped.promise]);
|
|
209
|
+
void result.then(() => { ended = true; signal?.removeEventListener("abort", stop); wake(); }, (error) => {
|
|
210
|
+
// A failed speculative model can never later publish its user input.
|
|
211
|
+
stopWith(error);
|
|
212
|
+
ended = true;
|
|
213
|
+
signal?.removeEventListener("abort", stop);
|
|
214
|
+
wake();
|
|
215
|
+
});
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
result,
|
|
218
|
+
textStream: {
|
|
219
|
+
[Symbol.asyncIterator]() {
|
|
220
|
+
if (taken)
|
|
221
|
+
throw new Error("Speculative text stream already has a consumer");
|
|
222
|
+
taken = true;
|
|
223
|
+
const iterator = (async function* () {
|
|
224
|
+
while (true) {
|
|
225
|
+
if (failed)
|
|
226
|
+
throw failure;
|
|
227
|
+
if (streamClosed)
|
|
228
|
+
return;
|
|
229
|
+
const chunk = chunks.shift();
|
|
230
|
+
if (chunk !== undefined)
|
|
231
|
+
yield chunk;
|
|
232
|
+
else if (ended)
|
|
233
|
+
return;
|
|
234
|
+
else
|
|
235
|
+
await changed.promise;
|
|
236
|
+
}
|
|
237
|
+
})();
|
|
238
|
+
return {
|
|
239
|
+
next: () => iterator.next(),
|
|
240
|
+
return: async () => {
|
|
241
|
+
streamClosed = true;
|
|
242
|
+
chunks.length = 0;
|
|
243
|
+
if (decision === "held")
|
|
244
|
+
stop();
|
|
245
|
+
wake();
|
|
246
|
+
return iterator.return();
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
commit(input) {
|
|
252
|
+
if (decision === "discarded" || (decision === "committed" && input !== confirmedInput)) {
|
|
253
|
+
const conflict = Promise.reject(new HarnessSpeculationConflictError());
|
|
254
|
+
void conflict.catch(() => { });
|
|
255
|
+
return conflict;
|
|
256
|
+
}
|
|
257
|
+
if (decision === "held") {
|
|
258
|
+
decision = "committed";
|
|
259
|
+
confirmedInput = input;
|
|
260
|
+
confirmation.resolve(input);
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
},
|
|
264
|
+
discard() {
|
|
265
|
+
if (decision === "committed")
|
|
266
|
+
return false;
|
|
267
|
+
stop();
|
|
268
|
+
return true;
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=speculative.js.map
|
package/package.json
CHANGED