@telnyx/agent-harness 0.1.0-beta.0 → 0.1.0-beta.2
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 +105 -3
- package/dist/approvals.d.ts +21 -0
- package/dist/approvals.js +93 -1
- package/dist/diagnostics.d.ts +33 -0
- package/dist/diagnostics.js +107 -0
- package/dist/durable.d.ts +1 -0
- package/dist/durable.js +2 -1
- package/dist/harness.d.ts +11 -1
- package/dist/harness.js +208 -35
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/ports.d.ts +14 -0
- package/dist/speculative.d.ts +36 -0
- package/dist/speculative.js +272 -0
- package/dist/voice-trace.d.ts +6 -0
- package/dist/voice-trace.js +160 -0
- package/dist/workspace.d.ts +13 -0
- package/dist/workspace.js +25 -5
- package/package.json +2 -2
package/dist/harness.js
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
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
|
+
import { traceActive, traceBind, voiceTraceContext, traceAttempt, traceStage } from "@telnyx/edge-runtime/internal";
|
|
5
|
+
import { traceModel, traceToolExecution } from "./voice-trace.js";
|
|
4
6
|
import { isToolOutputSerializationFailure } from "./steps.js";
|
|
7
|
+
import { inspectHarnessRun } from "./diagnostics.js";
|
|
5
8
|
import { HarnessApprovalError, createHarnessApprovalExecutionLedger, createHarnessApprovalMessageJournal, } from "./approvals.js";
|
|
6
9
|
import { createHarnessRunExecutionLedger, HarnessApprovalPause, } from "./durable.js";
|
|
7
10
|
import { runMiddleware } from "./lifecycle.js";
|
|
8
11
|
import { withHarnessToolContext } from "./tool-context.js";
|
|
9
12
|
import { resolveHarnessRuntimeConfig, } from "./runtime-config.js";
|
|
13
|
+
import { HarnessSpeculationUnsupportedError, serializeTurn, speculativeTurn, } from "./speculative.js";
|
|
14
|
+
export { HarnessSpeculationConflictError, HarnessSpeculationUnsupportedError, } from "./speculative.js";
|
|
10
15
|
const maxStepsSchema = z.number().int().positive();
|
|
11
16
|
const TOOL_OUTPUT_PREFIX = "\u001e@telnyx/agent-harness:tool-output:v1:";
|
|
12
17
|
const DURABLE_TOOL_STEP_VERSION = "tool-execution-v1";
|
|
18
|
+
function assertSpeculativeSdkHooks() {
|
|
19
|
+
if (globalThis.AI_SDK_TELEMETRY_INTEGRATIONS?.length
|
|
20
|
+
|| typeof globalThis.AI_SDK_LOG_WARNINGS === "function") {
|
|
21
|
+
throw new HarnessSpeculationUnsupportedError("global AI SDK lifecycle integrations or custom warning callbacks");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
13
24
|
/** Raised after the final allowed model step still requests executable tools. */
|
|
14
25
|
export class HarnessStepLimitError extends Error {
|
|
15
26
|
maxSteps;
|
|
@@ -66,6 +77,45 @@ export class HarnessObserverError extends Error {
|
|
|
66
77
|
this.name = "HarnessObserverError";
|
|
67
78
|
}
|
|
68
79
|
}
|
|
80
|
+
async function cancelTurn(onCancel, cancellation = new HarnessCancellationError()) {
|
|
81
|
+
try {
|
|
82
|
+
await onCancel?.(cancellation);
|
|
83
|
+
}
|
|
84
|
+
catch (observer) {
|
|
85
|
+
throw new HarnessObserverError(cancellation, observer, "cancel");
|
|
86
|
+
}
|
|
87
|
+
throw cancellation;
|
|
88
|
+
}
|
|
89
|
+
function captureSpeculativeOptions(options) {
|
|
90
|
+
// Inspect descriptors, not values: spreading invokes getters. A detached
|
|
91
|
+
// null-prototype snapshot cannot inherit hooks installed while queued.
|
|
92
|
+
const supported = new Set(["signal", "onStepCommitted", "onOutput"]);
|
|
93
|
+
const unsupported = new Set(["steps", "callbacks", "onCancel", "onError"]);
|
|
94
|
+
if (options === null || typeof options !== "object"
|
|
95
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(options))) {
|
|
96
|
+
throw new HarnessSpeculationUnsupportedError("non-plain turn options");
|
|
97
|
+
}
|
|
98
|
+
const captured = Object.create(null);
|
|
99
|
+
for (const key of Reflect.ownKeys(options)) {
|
|
100
|
+
const descriptor = Object.getOwnPropertyDescriptor(options, key);
|
|
101
|
+
if (key === voiceTraceContext && descriptor.enumerable && "value" in descriptor) {
|
|
102
|
+
Object.defineProperty(captured, key, descriptor);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (typeof key !== "string" || !descriptor.enumerable || !("value" in descriptor)
|
|
106
|
+
|| (!supported.has(key) && !unsupported.has(key))
|
|
107
|
+
|| (unsupported.has(key) && descriptor.value !== undefined)) {
|
|
108
|
+
throw new HarnessSpeculationUnsupportedError("accessors, inherited options, or pre-model lifecycle hooks");
|
|
109
|
+
}
|
|
110
|
+
if (supported.has(key)) {
|
|
111
|
+
if (key !== "signal" && descriptor.value !== undefined && typeof descriptor.value !== "function") {
|
|
112
|
+
throw new HarnessSpeculationUnsupportedError("non-function observers");
|
|
113
|
+
}
|
|
114
|
+
Object.defineProperty(captured, key, descriptor);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return Object.freeze(captured);
|
|
118
|
+
}
|
|
69
119
|
function historyMessages(stored) {
|
|
70
120
|
return stored.map((message) => {
|
|
71
121
|
if (message.role === "assistant") {
|
|
@@ -652,9 +702,11 @@ function validateTools(tools, authorization) {
|
|
|
652
702
|
}
|
|
653
703
|
/** Construct a portable AI SDK v6 turn engine over an explicit ports object. */
|
|
654
704
|
export function createHarness(spec) {
|
|
705
|
+
const activation = spec.ports.activation;
|
|
706
|
+
const diagnostics = traceBind(spec.diagnostics, activation);
|
|
655
707
|
const messagePort = spec.ports.messages;
|
|
656
|
-
const
|
|
657
|
-
const
|
|
708
|
+
const appendMessagePort = messagePort.append.bind(messagePort);
|
|
709
|
+
const appendMessagesPort = messagePort.appendMany.bind(messagePort);
|
|
658
710
|
const allMessages = messagePort.all.bind(messagePort);
|
|
659
711
|
const getState = spec.ports.state.get.bind(spec.ports.state);
|
|
660
712
|
const model = spec.model;
|
|
@@ -697,22 +749,25 @@ export function createHarness(spec) {
|
|
|
697
749
|
let lastValidRuntimeConfig = resolveHarnessRuntimeConfig(staticRuntimeConfig, undefined).config;
|
|
698
750
|
let runtimeConfigReadSequence = 0;
|
|
699
751
|
let lastPublishedRuntimeConfigReadSequence = 0;
|
|
700
|
-
const
|
|
752
|
+
const executeTurn = async (input, options = {}, speculation) => {
|
|
753
|
+
const trace = options[voiceTraceContext]?.scope === diagnostics ? options[voiceTraceContext] : traceAttempt(diagnostics, 0, "confirmed");
|
|
754
|
+
const committedEffect = (work) => speculation ? speculation.write(work) : work();
|
|
755
|
+
const appendMessage = (message) => speculation
|
|
756
|
+
? speculation.write(() => appendMessagePort(message)) : appendMessagePort(message);
|
|
757
|
+
const appendMessages = (batch) => speculation
|
|
758
|
+
? speculation.write(() => appendMessagesPort(batch)) : appendMessagesPort(batch);
|
|
701
759
|
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;
|
|
760
|
+
return cancelTurn(options.onCancel);
|
|
710
761
|
}
|
|
711
762
|
try {
|
|
712
763
|
const runtimeConfigReadSequenceForTurn = ++runtimeConfigReadSequence;
|
|
713
764
|
const state = await getState();
|
|
765
|
+
speculation?.assertLive();
|
|
714
766
|
const resolution = resolveHarnessRuntimeConfig(staticRuntimeConfig, state.__telnyx_agent_harness, lastValidRuntimeConfig);
|
|
715
767
|
const runtimeConfig = resolution.config;
|
|
768
|
+
if (speculation && runtimeConfig.output !== undefined) {
|
|
769
|
+
throw new HarnessSpeculationUnsupportedError("structured output");
|
|
770
|
+
}
|
|
716
771
|
if (typeof runtimeConfig.model === "string" && lifecycleMiddleware.modelStep.length > 0) {
|
|
717
772
|
throw new TypeError("Harness model-step middleware requires an executable LanguageModel");
|
|
718
773
|
}
|
|
@@ -731,7 +786,11 @@ export function createHarness(spec) {
|
|
|
731
786
|
});
|
|
732
787
|
return await runMiddleware(turnContext, lifecycleMiddleware.turn, async () => {
|
|
733
788
|
await toolSnapshot.preflight(Object.keys(runtimeConfig.tools ?? {}));
|
|
789
|
+
speculation?.assertLive();
|
|
734
790
|
const priorMessages = await allMessages();
|
|
791
|
+
speculation?.assertLive();
|
|
792
|
+
if (speculation)
|
|
793
|
+
assertSpeculativeSdkHooks();
|
|
735
794
|
const inputSteps = options.steps;
|
|
736
795
|
const inputJournal = inputSteps === undefined
|
|
737
796
|
? undefined
|
|
@@ -775,10 +834,29 @@ export function createHarness(spec) {
|
|
|
775
834
|
const replaysJournaledInput = journaledInput !== undefined;
|
|
776
835
|
let journalThrough = replaysJournaledInput
|
|
777
836
|
? priorMessages.at(-1).seq
|
|
778
|
-
: await appendMessage({ role: "user", content: input });
|
|
779
|
-
const storedMessages =
|
|
780
|
-
?
|
|
781
|
-
:
|
|
837
|
+
: speculation ? (priorMessages.at(-1)?.seq ?? 0) : await appendMessage({ role: "user", content: input });
|
|
838
|
+
const storedMessages = speculation
|
|
839
|
+
? [...priorMessages, { role: "user", content: input, seq: journalThrough + 1, at: new Date(spec.ports.clock.now()) }]
|
|
840
|
+
: !replaysJournaledInput
|
|
841
|
+
? await allMessages()
|
|
842
|
+
: priorMessages;
|
|
843
|
+
// The append-only port allocates the confirmed input's actual sequence.
|
|
844
|
+
// No speculative reservation can conflict with unrelated message writers.
|
|
845
|
+
let publication;
|
|
846
|
+
let confirmedForModel;
|
|
847
|
+
const confirmedMessages = (batch) => confirmedForModel === undefined ? batch : batch.map((message, index) => index === priorMessages.length ? { role: "user", content: confirmedForModel } : message);
|
|
848
|
+
const publishInput = () => {
|
|
849
|
+
if (!speculation)
|
|
850
|
+
return Promise.resolve();
|
|
851
|
+
publication ??= (async () => {
|
|
852
|
+
const confirmed = await speculation.confirmed();
|
|
853
|
+
speculation.assertLive();
|
|
854
|
+
confirmedForModel = confirmed;
|
|
855
|
+
journalThrough = await appendMessage({ role: "user", content: confirmed });
|
|
856
|
+
speculation.assertLive();
|
|
857
|
+
})();
|
|
858
|
+
return publication;
|
|
859
|
+
};
|
|
782
860
|
const restoredApprovals = options.steps === undefined
|
|
783
861
|
? []
|
|
784
862
|
: await approvalMessages.restoreRun(options.steps.runId);
|
|
@@ -825,9 +903,18 @@ export function createHarness(spec) {
|
|
|
825
903
|
let modelStepNumber = 0;
|
|
826
904
|
const limitReached = stepCountIs(runtimeConfig.limits.steps);
|
|
827
905
|
const assertBoundary = () => {
|
|
906
|
+
speculation?.assertLive();
|
|
828
907
|
if (boundaryFailed)
|
|
829
908
|
throw boundaryError;
|
|
830
909
|
};
|
|
910
|
+
if (speculation)
|
|
911
|
+
void publishInput().catch((error) => {
|
|
912
|
+
if (!boundaryFailed) {
|
|
913
|
+
boundaryFailed = true;
|
|
914
|
+
boundaryError = error;
|
|
915
|
+
}
|
|
916
|
+
speculation.fail(error);
|
|
917
|
+
});
|
|
831
918
|
const modelForTurn = lifecycleMiddleware.modelStep.length === 0 || typeof runtimeConfig.model === "string"
|
|
832
919
|
? runtimeConfig.model
|
|
833
920
|
: wrapLanguageModel({
|
|
@@ -847,7 +934,21 @@ export function createHarness(spec) {
|
|
|
847
934
|
toolName,
|
|
848
935
|
Object.freeze({
|
|
849
936
|
...candidate,
|
|
937
|
+
...(speculation ? {
|
|
938
|
+
inputSchema: jsonSchema(async () => await asSchema(candidate.inputSchema).jsonSchema, {
|
|
939
|
+
validate: async (value) => {
|
|
940
|
+
await publishInput();
|
|
941
|
+
assertBoundary();
|
|
942
|
+
const schema = asSchema(candidate.inputSchema);
|
|
943
|
+
return schema.validate
|
|
944
|
+
? await committedEffect(async () => await schema.validate(value))
|
|
945
|
+
: { success: true, value };
|
|
946
|
+
},
|
|
947
|
+
}),
|
|
948
|
+
} : {}),
|
|
850
949
|
async execute(...args) {
|
|
950
|
+
await publishInput();
|
|
951
|
+
assertBoundary();
|
|
851
952
|
const toolCallOptions = args[1];
|
|
852
953
|
const toolCallId = typeof toolCallOptions?.toolCallId === "string"
|
|
853
954
|
? toolCallOptions.toolCallId
|
|
@@ -862,13 +963,14 @@ export function createHarness(spec) {
|
|
|
862
963
|
let coreCompleted = false;
|
|
863
964
|
let replayingCompletedApproval = false;
|
|
864
965
|
try {
|
|
865
|
-
const output = await runMiddleware(Object.freeze({
|
|
966
|
+
const output = await committedEffect(() => runMiddleware(Object.freeze({
|
|
866
967
|
phase: "tool",
|
|
867
968
|
config: runtimeConfig,
|
|
868
969
|
toolName,
|
|
869
970
|
toolCallId,
|
|
870
971
|
signal: options.signal,
|
|
871
972
|
}), lifecycleMiddleware.tool, async () => {
|
|
973
|
+
assertBoundary();
|
|
872
974
|
const stepName = `tool:${toolName}:${durableToolCallOrdinal}`;
|
|
873
975
|
const stepVersion = spec.durability?.toolStepVersion ?? DURABLE_TOOL_STEP_VERSION;
|
|
874
976
|
const restoringCompletedApproval = options.steps !== undefined
|
|
@@ -881,13 +983,21 @@ export function createHarness(spec) {
|
|
|
881
983
|
? undefined
|
|
882
984
|
: await approvals.reauthorizeEffect(options.steps.runId, toolCallId, toolName, args[0]);
|
|
883
985
|
const approvalId = authorizedEffect?.id;
|
|
884
|
-
|
|
986
|
+
let effectArgs = authorizedEffect === undefined
|
|
885
987
|
? args
|
|
886
988
|
: [authorizedEffect.input, ...args.slice(1)];
|
|
989
|
+
if (speculation) {
|
|
990
|
+
const callOptions = objectRecord(effectArgs[1]);
|
|
991
|
+
if (Array.isArray(callOptions?.messages))
|
|
992
|
+
effectArgs = [effectArgs[0], {
|
|
993
|
+
...callOptions, messages: confirmedMessages(callOptions.messages),
|
|
994
|
+
}, ...effectArgs.slice(2)];
|
|
995
|
+
}
|
|
996
|
+
assertBoundary();
|
|
887
997
|
coreStarted = true;
|
|
888
998
|
const result = options.steps === undefined
|
|
889
|
-
? cloneToolOutput(toolName, await execute.apply(this,
|
|
890
|
-
: await options.steps.step(stepName, async () => await withHarnessToolContext(Object.freeze({ runId: options.steps.runId, toolName, ordinal: durableToolCallOrdinal }), async () => await execute.apply(this, effectArgs)), {
|
|
999
|
+
? cloneToolOutput(toolName, await committedEffect(async () => { traceToolExecution(trace, effectArgs[1]?.experimental_context); return await execute.apply(this, effectArgs); }))
|
|
1000
|
+
: await options.steps.step(stepName, async () => await withHarnessToolContext(Object.freeze({ runId: options.steps.runId, toolName, ordinal: durableToolCallOrdinal }), async () => { traceToolExecution(trace, effectArgs[1]?.experimental_context); return await execute.apply(this, effectArgs); }), {
|
|
891
1001
|
external: true,
|
|
892
1002
|
beforeEffect: () => approvals.consumeAuthorizedEffect(options.steps.runId, approvalId),
|
|
893
1003
|
input: args[0],
|
|
@@ -895,7 +1005,7 @@ export function createHarness(spec) {
|
|
|
895
1005
|
});
|
|
896
1006
|
coreCompleted = true;
|
|
897
1007
|
return result;
|
|
898
|
-
});
|
|
1008
|
+
}));
|
|
899
1009
|
const restoredCall = toolCallId === undefined ? undefined : restoredApprovalCalls.get(toolCallId);
|
|
900
1010
|
if (restoredCall !== undefined) {
|
|
901
1011
|
const approvedOutput = typeof output === "string"
|
|
@@ -937,8 +1047,8 @@ export function createHarness(spec) {
|
|
|
937
1047
|
durableSteps.nextToolCallOrdinal(event.toolCall.toolName, event.toolCall.toolCallId);
|
|
938
1048
|
await options.callbacks?.onToolCallStart?.(event);
|
|
939
1049
|
};
|
|
940
|
-
const
|
|
941
|
-
model: modelForTurn,
|
|
1050
|
+
const generationOptions = {
|
|
1051
|
+
model: traceModel(modelForTurn, trace),
|
|
942
1052
|
tools: toolsForTurn,
|
|
943
1053
|
...(runtimeConfig.output === undefined ? {} : { output: runtimeConfig.output }),
|
|
944
1054
|
abortSignal: options.signal,
|
|
@@ -948,9 +1058,15 @@ export function createHarness(spec) {
|
|
|
948
1058
|
experimental_onStepStart: options.callbacks?.onStepStart,
|
|
949
1059
|
experimental_onToolCallStart: onToolCallStart,
|
|
950
1060
|
experimental_onToolCallFinish: options.callbacks?.onToolCallFinish,
|
|
951
|
-
prepareStep() {
|
|
1061
|
+
prepareStep({ messages: stepMessages }) {
|
|
952
1062
|
assertBoundary();
|
|
953
|
-
|
|
1063
|
+
const confirmed = speculation && confirmedForModel !== undefined ? { messages: confirmedMessages(stepMessages) } : undefined;
|
|
1064
|
+
if (!traceActive(trace))
|
|
1065
|
+
return confirmed;
|
|
1066
|
+
// An empty per-step handoff carries no diagnostic metadata into results.
|
|
1067
|
+
// The source model and executor privately share its object identity.
|
|
1068
|
+
const handoff = Object.freeze({});
|
|
1069
|
+
return { ...confirmed, model: traceModel(modelForTurn, trace, handoff), experimental_context: handoff };
|
|
954
1070
|
},
|
|
955
1071
|
async stopWhen(context) {
|
|
956
1072
|
assertBoundary();
|
|
@@ -961,6 +1077,7 @@ export function createHarness(spec) {
|
|
|
961
1077
|
},
|
|
962
1078
|
async onStepFinish(step) {
|
|
963
1079
|
try {
|
|
1080
|
+
await publishInput();
|
|
964
1081
|
assertBoundary();
|
|
965
1082
|
const approvalRequests = step.content.filter((part) => part.type === "tool-approval-request");
|
|
966
1083
|
const pendingRequests = options.steps === undefined ? [] : await Promise.all(approvalRequests.map(async (request) => replayedCompletedApprovalToolCallIds.has(request.toolCall.toolCallId)
|
|
@@ -1042,7 +1159,12 @@ export function createHarness(spec) {
|
|
|
1042
1159
|
projectedToolCallIds.add(call.toolCallId);
|
|
1043
1160
|
for (const call of restoredCalls)
|
|
1044
1161
|
projectedRestoredToolCallIds.add(call.toolCallId);
|
|
1045
|
-
|
|
1162
|
+
assertBoundary();
|
|
1163
|
+
if (options.onStepCommitted) {
|
|
1164
|
+
const snapshot = observedStep(step, journalThrough);
|
|
1165
|
+
await committedEffect(async () => { await options.onStepCommitted(snapshot); });
|
|
1166
|
+
}
|
|
1167
|
+
assertBoundary();
|
|
1046
1168
|
await options.callbacks?.onStepFinish?.(step);
|
|
1047
1169
|
}
|
|
1048
1170
|
catch (error) {
|
|
@@ -1054,7 +1176,40 @@ export function createHarness(spec) {
|
|
|
1054
1176
|
}
|
|
1055
1177
|
},
|
|
1056
1178
|
onFinish: options.callbacks?.onFinish,
|
|
1057
|
-
}
|
|
1179
|
+
};
|
|
1180
|
+
const generation = speculation ? (async () => {
|
|
1181
|
+
let streamFailure;
|
|
1182
|
+
let streamFailed = false;
|
|
1183
|
+
const onStreamError = (error) => {
|
|
1184
|
+
streamFailed = true;
|
|
1185
|
+
streamFailure = boundaryFailed ? boundaryError : isProviderFailure(error)
|
|
1186
|
+
? new HarnessProviderError(providerDiagnostics(runtimeConfig.model, error)) : error;
|
|
1187
|
+
speculation.fail(streamFailure);
|
|
1188
|
+
};
|
|
1189
|
+
const streamed = streamText({
|
|
1190
|
+
...generationOptions,
|
|
1191
|
+
onError: ({ error }) => onStreamError(error),
|
|
1192
|
+
onChunk: ({ chunk }) => {
|
|
1193
|
+
assertBoundary();
|
|
1194
|
+
const part = objectRecord(chunk);
|
|
1195
|
+
if (part?.type === "text-delta" && typeof part.text === "string") {
|
|
1196
|
+
traceStage(trace, "text_forwarded");
|
|
1197
|
+
speculation.text(part.text);
|
|
1198
|
+
}
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
await streamed.consumeStream({ onError: onStreamError });
|
|
1202
|
+
assertBoundary();
|
|
1203
|
+
if (streamFailed)
|
|
1204
|
+
throw streamFailure;
|
|
1205
|
+
return {
|
|
1206
|
+
text: await streamed.text,
|
|
1207
|
+
steps: await streamed.steps,
|
|
1208
|
+
finishReason: await streamed.finishReason,
|
|
1209
|
+
totalUsage: await streamed.totalUsage,
|
|
1210
|
+
output: undefined,
|
|
1211
|
+
};
|
|
1212
|
+
})() : generateText(generationOptions);
|
|
1058
1213
|
let result;
|
|
1059
1214
|
try {
|
|
1060
1215
|
result = await generation;
|
|
@@ -1075,6 +1230,8 @@ export function createHarness(spec) {
|
|
|
1075
1230
|
name: call.toolName,
|
|
1076
1231
|
})));
|
|
1077
1232
|
}
|
|
1233
|
+
if (!speculation)
|
|
1234
|
+
traceStage(trace, "text_forwarded");
|
|
1078
1235
|
const output = Object.freeze({
|
|
1079
1236
|
text: result.text,
|
|
1080
1237
|
stepCount: result.steps.length,
|
|
@@ -1089,7 +1246,9 @@ export function createHarness(spec) {
|
|
|
1089
1246
|
? {}
|
|
1090
1247
|
: { output: result.output }),
|
|
1091
1248
|
});
|
|
1092
|
-
|
|
1249
|
+
assertBoundary();
|
|
1250
|
+
if (options.onOutput)
|
|
1251
|
+
await committedEffect(async () => { await options.onOutput(output); });
|
|
1093
1252
|
return output;
|
|
1094
1253
|
});
|
|
1095
1254
|
}
|
|
@@ -1102,13 +1261,7 @@ export function createHarness(spec) {
|
|
|
1102
1261
|
const classified = error instanceof HarnessCancellationError
|
|
1103
1262
|
? error
|
|
1104
1263
|
: new HarnessCancellationError();
|
|
1105
|
-
|
|
1106
|
-
await options.onCancel?.(classified);
|
|
1107
|
-
}
|
|
1108
|
-
catch (observer) {
|
|
1109
|
-
throw new HarnessObserverError(classified, observer, "cancel");
|
|
1110
|
-
}
|
|
1111
|
-
throw classified;
|
|
1264
|
+
return cancelTurn(options.onCancel, classified);
|
|
1112
1265
|
}
|
|
1113
1266
|
try {
|
|
1114
1267
|
await options.onError?.(error);
|
|
@@ -1119,6 +1272,23 @@ export function createHarness(spec) {
|
|
|
1119
1272
|
throw error;
|
|
1120
1273
|
}
|
|
1121
1274
|
};
|
|
1275
|
+
const turn = (input, options = {}) => serializeTurn(activation, () => executeTurn(input, options), false, options.signal, () => cancelTurn(options.onCancel));
|
|
1276
|
+
const begin = (input, options = {}) => {
|
|
1277
|
+
const capturedOptions = captureSpeculativeOptions(options);
|
|
1278
|
+
assertSpeculativeSdkHooks();
|
|
1279
|
+
// A captured catalog can select output from async runtime state. Refuse
|
|
1280
|
+
// before returning a handle so voice can safely fall back after confirmation.
|
|
1281
|
+
if (lifecycleMiddleware.turn.length || lifecycleMiddleware.modelStep.length
|
|
1282
|
+
|| staticRuntimeConfig.output !== undefined || Object.keys(outputs).length > 0) {
|
|
1283
|
+
throw new HarnessSpeculationUnsupportedError("durable step contexts, pre-model lifecycle hooks, or structured output");
|
|
1284
|
+
}
|
|
1285
|
+
return speculativeTurn(activation, capturedOptions.signal, () => new HarnessCancellationError(), (control) => executeTurn(input, Object.freeze(Object.assign(Object.create(null), {
|
|
1286
|
+
signal: control.signal,
|
|
1287
|
+
onStepCommitted: capturedOptions.onStepCommitted,
|
|
1288
|
+
onOutput: capturedOptions.onOutput,
|
|
1289
|
+
[voiceTraceContext]: capturedOptions[voiceTraceContext],
|
|
1290
|
+
})), control));
|
|
1291
|
+
};
|
|
1122
1292
|
const ledger = createHarnessRunExecutionLedger(spec.ports, async (input, signal, steps) => {
|
|
1123
1293
|
// Journal correlation enriches scheduling recovery, but it must not turn
|
|
1124
1294
|
// an otherwise executable durable run into a failed one when the journal
|
|
@@ -1168,6 +1338,7 @@ export function createHarness(spec) {
|
|
|
1168
1338
|
}
|
|
1169
1339
|
}, { checkpoints: spec.durability?.checkpoints, requireApprovalIdentity: requiresApprovalIdentity });
|
|
1170
1340
|
const approvalService = Object.freeze({
|
|
1341
|
+
listPending: approvals.listPending,
|
|
1171
1342
|
inspect: approvals.inspect,
|
|
1172
1343
|
async grant(id) {
|
|
1173
1344
|
let approval;
|
|
@@ -1209,8 +1380,10 @@ export function createHarness(spec) {
|
|
|
1209
1380
|
});
|
|
1210
1381
|
return Object.freeze({
|
|
1211
1382
|
turn,
|
|
1383
|
+
begin,
|
|
1212
1384
|
accept: ledger.accept,
|
|
1213
1385
|
get: ledger.get,
|
|
1386
|
+
inspectRun: (runId, options) => inspectHarnessRun(spec.ports, runId, options),
|
|
1214
1387
|
status: ledger.status,
|
|
1215
1388
|
list: ledger.list,
|
|
1216
1389
|
cancel: ledger.cancel,
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
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
|
-
export { HARNESS_APPROVAL_STATUSES, HarnessApprovalError, HarnessAuthorizationError, type HarnessApproval, type HarnessApprovalLedger, type HarnessApprovalStatus, } from "./approvals.js";
|
|
7
|
+
export { HARNESS_APPROVAL_STATUSES, HarnessApprovalError, HarnessAuthorizationError, type HarnessApproval, type HarnessApprovalLedger, type HarnessApprovalStatus, type HarnessPendingApprovalSummary, type HarnessPendingApprovalPage, type HarnessPendingApprovalListOptions, } from "./approvals.js";
|
|
7
8
|
export * from "./steps.js";
|
|
9
|
+
export type { HarnessRunInspection, HarnessRunInspectionOptions } from "./diagnostics.js";
|
|
8
10
|
export * from "./lifecycle.js";
|
|
9
11
|
export * from "./channel.js";
|
|
10
12
|
export * from "./scheduling.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";
|
package/dist/ports.d.ts
CHANGED
|
@@ -17,6 +17,20 @@ export interface AgentHarnessAuthorizationIdentity {
|
|
|
17
17
|
/** Versioned private authorization capability required for approval decisions. */
|
|
18
18
|
export interface HarnessAuthorizationPort {
|
|
19
19
|
identity(): AgentHarnessAuthorizationIdentity | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Optional explicit authority to enumerate minimal pending summaries, including
|
|
22
|
+
* pagination lookahead, for this actor/organization/run. An individual read
|
|
23
|
+
* allow is not collection authority. Hosts must opt in and enforce run access.
|
|
24
|
+
*/
|
|
25
|
+
authorizePendingApprovalSummaries?(request: Readonly<{
|
|
26
|
+
identity: AgentHarnessAuthorizationIdentity;
|
|
27
|
+
actorId: string;
|
|
28
|
+
action: "agent_harness.approvals.list_pending";
|
|
29
|
+
resource: string;
|
|
30
|
+
runId: string;
|
|
31
|
+
}>): Promise<Readonly<{
|
|
32
|
+
kind: "allow" | "deny" | "unavailable";
|
|
33
|
+
}>>;
|
|
20
34
|
authorize(request: Readonly<{
|
|
21
35
|
identity: AgentHarnessAuthorizationIdentity;
|
|
22
36
|
action: string;
|
|
@@ -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
|