@tangle-network/agent-runtime 0.25.2 → 0.27.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/README.md +1 -1
- package/dist/agent.d.ts +1 -1
- package/dist/index.d.ts +691 -5
- package/dist/index.js +1033 -1
- package/dist/index.js.map +1 -1
- package/dist/loops.d.ts +7 -6
- package/dist/mcp/index.d.ts +5 -5
- package/dist/{otel-export-B33Cy_60.d.ts → otel-export-B2UBcPV4.d.ts} +2 -2
- package/dist/profiles.d.ts +3 -3
- package/dist/{runtime-run-D5ItCKl_.d.ts → runtime-run-B8VIiOhI.d.ts} +1 -1
- package/dist/{types-DmkRGTBn.d.ts → types-Cbe54nB7.d.ts} +3 -13
- package/dist/{types-BFgFD_sl.d.ts → types-CsCCryln.d.ts} +24 -0
- package/package.json +12 -23
package/dist/index.js
CHANGED
|
@@ -175,7 +175,14 @@ function createOpenAICompatibleBackend(options) {
|
|
|
175
175
|
method: "POST",
|
|
176
176
|
headers: {
|
|
177
177
|
Authorization: `Bearer ${options.apiKey}`,
|
|
178
|
-
"Content-Type": "application/json"
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
// Cross-gateway forwarding: when this call is part of a
|
|
180
|
+
// multi-agent conversation, the runner stamps run/turn/
|
|
181
|
+
// depth/forwarded-auth headers onto the context. They flow
|
|
182
|
+
// through to the downstream gateway verbatim so the original
|
|
183
|
+
// user gets billed, the recursion depth stays bounded, and
|
|
184
|
+
// the trace correlates across hops.
|
|
185
|
+
...context.propagatedHeaders ?? {}
|
|
179
186
|
},
|
|
180
187
|
body: requestBody,
|
|
181
188
|
signal: attemptSignal.signal
|
|
@@ -624,6 +631,1009 @@ function stringValue(value) {
|
|
|
624
631
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
625
632
|
}
|
|
626
633
|
|
|
634
|
+
// src/conversation/call-policy.ts
|
|
635
|
+
var CircuitOpenError = class extends Error {
|
|
636
|
+
constructor(participant, retryAfterMs) {
|
|
637
|
+
super(
|
|
638
|
+
`circuit open for participant '${participant}'; ${retryAfterMs}ms remaining before retry allowed`
|
|
639
|
+
);
|
|
640
|
+
this.name = "CircuitOpenError";
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
var DeadlineExceededError = class extends Error {
|
|
644
|
+
constructor(deadlineMs) {
|
|
645
|
+
super(`backend call exceeded per-attempt deadline of ${deadlineMs}ms`);
|
|
646
|
+
this.name = "DeadlineExceededError";
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
var defaultIsRetryable = (err) => {
|
|
650
|
+
if (err instanceof DeadlineExceededError) return true;
|
|
651
|
+
if (err instanceof Error) {
|
|
652
|
+
const name = err.name;
|
|
653
|
+
const message = err.message.toLowerCase();
|
|
654
|
+
if (name === "AbortError" || name === "TimeoutError") return true;
|
|
655
|
+
if (message.includes("econnreset") || message.includes("etimedout") || message.includes("econnrefused") || message.includes("socket hang up") || message.includes("network") || message.includes("fetch failed")) {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return false;
|
|
660
|
+
};
|
|
661
|
+
var CircuitBreakerState = class {
|
|
662
|
+
constructor(config) {
|
|
663
|
+
this.config = config;
|
|
664
|
+
}
|
|
665
|
+
config;
|
|
666
|
+
consecutiveFailures = 0;
|
|
667
|
+
openedAt;
|
|
668
|
+
/**
|
|
669
|
+
* Check whether the next call is allowed. Throws `CircuitOpenError` when
|
|
670
|
+
* the breaker is open and the cooldown hasn't elapsed.
|
|
671
|
+
*/
|
|
672
|
+
preflight(participant, now = Date.now()) {
|
|
673
|
+
if (!this.config || this.openedAt === void 0) return;
|
|
674
|
+
const remaining = this.config.cooldownMs - (now - this.openedAt);
|
|
675
|
+
if (remaining > 0) {
|
|
676
|
+
throw new CircuitOpenError(participant, remaining);
|
|
677
|
+
}
|
|
678
|
+
this.openedAt = void 0;
|
|
679
|
+
this.consecutiveFailures = 0;
|
|
680
|
+
}
|
|
681
|
+
recordSuccess() {
|
|
682
|
+
this.consecutiveFailures = 0;
|
|
683
|
+
this.openedAt = void 0;
|
|
684
|
+
}
|
|
685
|
+
recordFailure(now = Date.now()) {
|
|
686
|
+
if (!this.config) return;
|
|
687
|
+
this.consecutiveFailures += 1;
|
|
688
|
+
if (this.consecutiveFailures >= this.config.failuresToOpen) {
|
|
689
|
+
this.openedAt = now;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
function makePerAttemptSignal(parentSignal, deadlineMs) {
|
|
694
|
+
const controller = new AbortController();
|
|
695
|
+
let deadlineError;
|
|
696
|
+
const cleanups = [];
|
|
697
|
+
if (parentSignal) {
|
|
698
|
+
if (parentSignal.aborted) controller.abort(parentSignal.reason);
|
|
699
|
+
else {
|
|
700
|
+
const onAbort = () => controller.abort(parentSignal.reason);
|
|
701
|
+
parentSignal.addEventListener("abort", onAbort, { once: true });
|
|
702
|
+
cleanups.push(() => parentSignal.removeEventListener("abort", onAbort));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (deadlineMs !== void 0) {
|
|
706
|
+
const ms = deadlineMs;
|
|
707
|
+
const timer = setTimeout(() => {
|
|
708
|
+
deadlineError = new DeadlineExceededError(ms);
|
|
709
|
+
controller.abort(deadlineError);
|
|
710
|
+
}, ms);
|
|
711
|
+
cleanups.push(() => clearTimeout(timer));
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
signal: controller.signal,
|
|
715
|
+
dispose() {
|
|
716
|
+
for (const c of cleanups) c();
|
|
717
|
+
},
|
|
718
|
+
getDeadlineError() {
|
|
719
|
+
return deadlineError;
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
function computeBackoff(spec, attempt) {
|
|
724
|
+
if (spec === void 0) {
|
|
725
|
+
const base = 250;
|
|
726
|
+
const jitter = Math.floor(Math.random() * base);
|
|
727
|
+
return base * 2 ** (attempt - 1) + jitter;
|
|
728
|
+
}
|
|
729
|
+
if (typeof spec === "function") return Math.max(0, spec(attempt));
|
|
730
|
+
return Math.max(0, spec);
|
|
731
|
+
}
|
|
732
|
+
function sleep2(ms) {
|
|
733
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/conversation/headers.ts
|
|
737
|
+
var FORWARD_HEADERS = {
|
|
738
|
+
/** Forwarded original-user identity (`Bearer sk-tan-<user>`); downstream gateways bill against this. */
|
|
739
|
+
authorization: "x-tangle-forwarded-authorization",
|
|
740
|
+
/** Monotonically incremented on every gateway hop. Refused at MAX_DEPTH. */
|
|
741
|
+
depth: "x-tangle-forwarded-depth",
|
|
742
|
+
/** Top-level conversation run identifier, propagated through every nested call. */
|
|
743
|
+
runId: "x-tangle-runid",
|
|
744
|
+
/** This call's turn within the run; deterministic + stable across retries. */
|
|
745
|
+
turnId: "x-tangle-turnid",
|
|
746
|
+
/** When the call is *inside* another turn (recursion), the parent turn's id. */
|
|
747
|
+
parentTurnId: "x-tangle-parent-turnid",
|
|
748
|
+
/** Logical conversation peer label at the sending side, for trace stitching. */
|
|
749
|
+
speaker: "x-tangle-speaker"
|
|
750
|
+
};
|
|
751
|
+
var DEFAULT_MAX_DEPTH = 4;
|
|
752
|
+
function lc(name) {
|
|
753
|
+
return name.toLowerCase();
|
|
754
|
+
}
|
|
755
|
+
function readDepth(headers) {
|
|
756
|
+
const raw = pickHeader(headers, FORWARD_HEADERS.depth);
|
|
757
|
+
if (raw === void 0 || raw === "") return 0;
|
|
758
|
+
const n = Number(raw);
|
|
759
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
760
|
+
throw new Error(
|
|
761
|
+
`invalid ${FORWARD_HEADERS.depth} header value '${raw}' \u2014 must be a non-negative integer`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
return n;
|
|
765
|
+
}
|
|
766
|
+
function isDepthExceeded(inboundDepth, max = DEFAULT_MAX_DEPTH) {
|
|
767
|
+
return inboundDepth >= max;
|
|
768
|
+
}
|
|
769
|
+
function buildForwardHeaders(input) {
|
|
770
|
+
const out = {
|
|
771
|
+
[FORWARD_HEADERS.depth]: String(input.inboundDepth + 1),
|
|
772
|
+
[FORWARD_HEADERS.runId]: input.runId,
|
|
773
|
+
[FORWARD_HEADERS.turnId]: input.turnId,
|
|
774
|
+
[FORWARD_HEADERS.speaker]: input.speaker
|
|
775
|
+
};
|
|
776
|
+
if (input.forwardedAuthorization !== void 0) {
|
|
777
|
+
out[FORWARD_HEADERS.authorization] = input.forwardedAuthorization;
|
|
778
|
+
}
|
|
779
|
+
if (input.parentTurnId !== void 0) {
|
|
780
|
+
out[FORWARD_HEADERS.parentTurnId] = input.parentTurnId;
|
|
781
|
+
}
|
|
782
|
+
return out;
|
|
783
|
+
}
|
|
784
|
+
function pickHeader(headers, name) {
|
|
785
|
+
const target = lc(name);
|
|
786
|
+
for (const key of Object.keys(headers)) {
|
|
787
|
+
if (lc(key) === target) {
|
|
788
|
+
const value = headers[key];
|
|
789
|
+
if (Array.isArray(value)) return value[0];
|
|
790
|
+
return value;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return void 0;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/conversation/turn-id.ts
|
|
797
|
+
function turnId(runId, index, speaker) {
|
|
798
|
+
return `${runId}.t${index}.${slugifySpeaker(speaker)}`;
|
|
799
|
+
}
|
|
800
|
+
function slugifySpeaker(speaker) {
|
|
801
|
+
const cleaned = speaker.normalize("NFKD").replace(/[^\w-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
802
|
+
return cleaned || "anon";
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/conversation/run-conversation.ts
|
|
806
|
+
async function runConversation(conversation, options) {
|
|
807
|
+
let result;
|
|
808
|
+
for await (const event of runConversationStream(conversation, options)) {
|
|
809
|
+
if (options.onEvent) await options.onEvent(event);
|
|
810
|
+
if (event.type === "conversation_end") result = event.result;
|
|
811
|
+
}
|
|
812
|
+
if (!result) {
|
|
813
|
+
throw new BackendTransportError(
|
|
814
|
+
"conversation",
|
|
815
|
+
"conversation stream ended without a conversation_end event"
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
async function* runConversationStream(conversation, options) {
|
|
821
|
+
const runId = options.runId ?? `conv_${crypto.randomUUID()}`;
|
|
822
|
+
const inboundDepth = options.inboundDepth ?? 0;
|
|
823
|
+
const callerHeaders = options.propagatedHeaders ?? {};
|
|
824
|
+
const forwardedAuthorization = callerHeaders[FORWARD_HEADERS.authorization];
|
|
825
|
+
const breakers = /* @__PURE__ */ new Map();
|
|
826
|
+
for (const participant of conversation.participants) {
|
|
827
|
+
const cfg = participant.callPolicy?.circuitBreaker ?? conversation.policy.defaultCallPolicy?.circuitBreaker;
|
|
828
|
+
breakers.set(participant.name, new CircuitBreakerState(cfg));
|
|
829
|
+
}
|
|
830
|
+
let transcript = [];
|
|
831
|
+
let spentCreditsCents = 0;
|
|
832
|
+
let startedAt = nowIso();
|
|
833
|
+
let resumed = false;
|
|
834
|
+
if (options.journal) {
|
|
835
|
+
const prior = await options.journal.loadRun(runId);
|
|
836
|
+
if (prior) {
|
|
837
|
+
if (prior.halted) {
|
|
838
|
+
const replayResult = {
|
|
839
|
+
runId,
|
|
840
|
+
transcript: prior.turns,
|
|
841
|
+
turns: prior.turns.length,
|
|
842
|
+
spentCreditsCents: prior.turns.reduce(
|
|
843
|
+
(sum, t) => sum + centsFromUsd(t.usage?.costUsd ?? 0),
|
|
844
|
+
0
|
|
845
|
+
),
|
|
846
|
+
halted: prior.halted,
|
|
847
|
+
durationMs: 0,
|
|
848
|
+
startedAt: prior.startedAt,
|
|
849
|
+
endedAt: prior.endedAt ?? prior.startedAt
|
|
850
|
+
};
|
|
851
|
+
yield {
|
|
852
|
+
type: "conversation_resumed",
|
|
853
|
+
runId,
|
|
854
|
+
participants: conversation.participants.map((p) => p.name),
|
|
855
|
+
transcript: prior.turns,
|
|
856
|
+
timestamp: nowIso()
|
|
857
|
+
};
|
|
858
|
+
yield { type: "conversation_end", runId, result: replayResult, timestamp: nowIso() };
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
transcript = [...prior.turns];
|
|
862
|
+
spentCreditsCents = transcript.reduce(
|
|
863
|
+
(sum, t) => sum + centsFromUsd(t.usage?.costUsd ?? 0),
|
|
864
|
+
0
|
|
865
|
+
);
|
|
866
|
+
startedAt = prior.startedAt;
|
|
867
|
+
resumed = true;
|
|
868
|
+
} else {
|
|
869
|
+
await options.journal.beginRun(runId, startedAt);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const startedAtMs = Date.now();
|
|
873
|
+
if (resumed) {
|
|
874
|
+
yield {
|
|
875
|
+
type: "conversation_resumed",
|
|
876
|
+
runId,
|
|
877
|
+
participants: conversation.participants.map((p) => p.name),
|
|
878
|
+
// Snapshot the resumed transcript — the live `transcript` array gets
|
|
879
|
+
// pushed to as the run continues, so handing the bare reference to a
|
|
880
|
+
// subscriber would leak future writes into a past event.
|
|
881
|
+
transcript: [...transcript],
|
|
882
|
+
timestamp: nowIso()
|
|
883
|
+
};
|
|
884
|
+
} else {
|
|
885
|
+
yield {
|
|
886
|
+
type: "conversation_start",
|
|
887
|
+
runId,
|
|
888
|
+
participants: conversation.participants.map((p) => p.name),
|
|
889
|
+
seed: options.seed,
|
|
890
|
+
timestamp: startedAt
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
let currentInput = transcript.length === 0 ? options.seed : transcript[transcript.length - 1]?.text ?? options.seed;
|
|
894
|
+
let halt;
|
|
895
|
+
const initialOffset = transcript.length;
|
|
896
|
+
for (let turnIndex = initialOffset; turnIndex < conversation.policy.maxTurns; turnIndex++) {
|
|
897
|
+
if (options.signal?.aborted) {
|
|
898
|
+
halt = { kind: "abort" };
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
if (conversation.policy.maxCreditsCents !== void 0 && spentCreditsCents >= conversation.policy.maxCreditsCents) {
|
|
902
|
+
halt = {
|
|
903
|
+
kind: "max_credits",
|
|
904
|
+
spentCents: spentCreditsCents,
|
|
905
|
+
capCents: conversation.policy.maxCreditsCents
|
|
906
|
+
};
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
const speakerIdx = selectSpeaker(
|
|
910
|
+
conversation.policy.turnOrder,
|
|
911
|
+
conversation.participants.length,
|
|
912
|
+
{ transcript, turnIndex, spentCreditsCents }
|
|
913
|
+
);
|
|
914
|
+
const speaker = conversation.participants[speakerIdx];
|
|
915
|
+
if (!speaker) {
|
|
916
|
+
throw new BackendTransportError(
|
|
917
|
+
"conversation",
|
|
918
|
+
`turnOrder selector returned out-of-range index ${speakerIdx} for ${conversation.participants.length} participants`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
const tid = turnId(runId, turnIndex, speaker.name);
|
|
922
|
+
const callPolicy = speaker.callPolicy ?? conversation.policy.defaultCallPolicy;
|
|
923
|
+
const breaker = breakers.get(speaker.name);
|
|
924
|
+
if (!breaker) {
|
|
925
|
+
throw new BackendTransportError(
|
|
926
|
+
"conversation",
|
|
927
|
+
`internal: no circuit-breaker state registered for participant '${speaker.name}'`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
const isRetryable = callPolicy?.isRetryable ?? defaultIsRetryable;
|
|
931
|
+
const totalAttempts = 1 + (callPolicy?.maxRetries ?? 0);
|
|
932
|
+
yield {
|
|
933
|
+
type: "turn_start",
|
|
934
|
+
runId,
|
|
935
|
+
index: turnIndex,
|
|
936
|
+
speaker: speaker.name,
|
|
937
|
+
turnId: tid,
|
|
938
|
+
attempt: 1,
|
|
939
|
+
timestamp: nowIso()
|
|
940
|
+
};
|
|
941
|
+
let aggregator;
|
|
942
|
+
let attemptCount = 0;
|
|
943
|
+
let lastError;
|
|
944
|
+
let breakerOpenFailure;
|
|
945
|
+
for (let attempt = 1; attempt <= totalAttempts; attempt++) {
|
|
946
|
+
attemptCount = attempt;
|
|
947
|
+
try {
|
|
948
|
+
breaker.preflight(speaker.name);
|
|
949
|
+
} catch (err) {
|
|
950
|
+
breakerOpenFailure = err;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
if (attempt > 1) {
|
|
954
|
+
yield {
|
|
955
|
+
type: "turn_retry",
|
|
956
|
+
runId,
|
|
957
|
+
index: turnIndex,
|
|
958
|
+
speaker: speaker.name,
|
|
959
|
+
turnId: tid,
|
|
960
|
+
attempt,
|
|
961
|
+
reason: lastError instanceof Error ? lastError.message : String(lastError),
|
|
962
|
+
timestamp: nowIso()
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
const perAttempt = makePerAttemptSignal(options.signal, callPolicy?.perAttemptDeadlineMs);
|
|
966
|
+
const localAgg = new TurnAggregator({
|
|
967
|
+
index: turnIndex,
|
|
968
|
+
speaker: speaker.name,
|
|
969
|
+
startedAt: nowIso()
|
|
970
|
+
});
|
|
971
|
+
try {
|
|
972
|
+
for await (const delta of driveSingleAttempt({
|
|
973
|
+
speaker,
|
|
974
|
+
participants: conversation.participants,
|
|
975
|
+
input: currentInput,
|
|
976
|
+
turnIndex,
|
|
977
|
+
runId,
|
|
978
|
+
turnId: tid,
|
|
979
|
+
transcript,
|
|
980
|
+
signal: perAttempt.signal,
|
|
981
|
+
aggregator: localAgg,
|
|
982
|
+
propagatedHeaders: buildForwardHeaders({
|
|
983
|
+
inboundDepth,
|
|
984
|
+
// When the participant elects to pay for its own outbound calls,
|
|
985
|
+
// drop the forwarded user identity so the downstream gateway
|
|
986
|
+
// bills the participant's own credentials instead. The backend
|
|
987
|
+
// brings its own `Authorization` header at construction time
|
|
988
|
+
// (e.g. `createOpenAICompatibleBackend({ apiKey: sk-tan-AGENT })`);
|
|
989
|
+
// omitting the forwarded header is what flips the billing target.
|
|
990
|
+
forwardedAuthorization: resolveAuthForwarding(speaker, {
|
|
991
|
+
transcript,
|
|
992
|
+
turnIndex,
|
|
993
|
+
spentCreditsCents
|
|
994
|
+
}) ? forwardedAuthorization : void 0,
|
|
995
|
+
runId,
|
|
996
|
+
turnId: tid,
|
|
997
|
+
parentTurnId: options.parentTurnId,
|
|
998
|
+
speaker: speaker.name
|
|
999
|
+
})
|
|
1000
|
+
})) {
|
|
1001
|
+
yield {
|
|
1002
|
+
type: "turn_text_delta",
|
|
1003
|
+
runId,
|
|
1004
|
+
index: turnIndex,
|
|
1005
|
+
speaker: speaker.name,
|
|
1006
|
+
turnId: tid,
|
|
1007
|
+
text: delta.text,
|
|
1008
|
+
timestamp: delta.timestamp
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
perAttempt.dispose();
|
|
1012
|
+
breaker.recordSuccess();
|
|
1013
|
+
aggregator = localAgg;
|
|
1014
|
+
break;
|
|
1015
|
+
} catch (err) {
|
|
1016
|
+
perAttempt.dispose();
|
|
1017
|
+
breaker.recordFailure();
|
|
1018
|
+
lastError = perAttempt.getDeadlineError() ?? err;
|
|
1019
|
+
if (attempt >= totalAttempts || !isRetryable(lastError)) {
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
await sleep2(computeBackoff(callPolicy?.retryBackoffMs, attempt));
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (!aggregator) {
|
|
1026
|
+
const failure = breakerOpenFailure ?? lastError;
|
|
1027
|
+
const message = failure instanceof Error ? failure.message : String(failure);
|
|
1028
|
+
halt = { kind: "participant_error", participant: speaker.name, message };
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
const turn = aggregator.toTurn({ turnId: tid, attempts: attemptCount });
|
|
1032
|
+
transcript.push(turn);
|
|
1033
|
+
spentCreditsCents += centsFromUsd(turn.usage?.costUsd ?? 0);
|
|
1034
|
+
if (options.journal) {
|
|
1035
|
+
await options.journal.appendTurn(runId, turn);
|
|
1036
|
+
}
|
|
1037
|
+
yield { type: "turn_end", runId, turn, timestamp: nowIso() };
|
|
1038
|
+
if (conversation.policy.haltOn) {
|
|
1039
|
+
const haltCtx = {
|
|
1040
|
+
transcript,
|
|
1041
|
+
lastTurn: turn,
|
|
1042
|
+
turnIndex,
|
|
1043
|
+
spentCreditsCents
|
|
1044
|
+
};
|
|
1045
|
+
const decision = await conversation.policy.haltOn(haltCtx);
|
|
1046
|
+
if (decision === true) {
|
|
1047
|
+
halt = { kind: "predicate", reason: "predicate_true" };
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
if (typeof decision === "object" && decision !== null && decision.halted) {
|
|
1051
|
+
halt = { kind: "predicate", reason: decision.reason };
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
currentInput = turn.text;
|
|
1056
|
+
}
|
|
1057
|
+
if (!halt) halt = { kind: "max_turns", turns: transcript.length };
|
|
1058
|
+
const endedAt = nowIso();
|
|
1059
|
+
const result = {
|
|
1060
|
+
runId,
|
|
1061
|
+
transcript,
|
|
1062
|
+
turns: transcript.length,
|
|
1063
|
+
spentCreditsCents,
|
|
1064
|
+
halted: halt,
|
|
1065
|
+
durationMs: Date.now() - startedAtMs,
|
|
1066
|
+
startedAt,
|
|
1067
|
+
endedAt
|
|
1068
|
+
};
|
|
1069
|
+
if (options.journal) {
|
|
1070
|
+
await options.journal.recordHalt(runId, halt, endedAt);
|
|
1071
|
+
}
|
|
1072
|
+
yield { type: "conversation_end", runId, result, timestamp: endedAt };
|
|
1073
|
+
}
|
|
1074
|
+
async function* driveSingleAttempt(args) {
|
|
1075
|
+
const task = {
|
|
1076
|
+
id: args.turnId,
|
|
1077
|
+
intent: args.input,
|
|
1078
|
+
metadata: {
|
|
1079
|
+
runId: args.runId,
|
|
1080
|
+
turnId: args.turnId,
|
|
1081
|
+
turnIndex: args.turnIndex,
|
|
1082
|
+
speaker: args.speaker.name,
|
|
1083
|
+
participants: args.participants.map((p) => p.name)
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
const knowledge = passingReadiness(task.id);
|
|
1087
|
+
const messages = buildMessagesFor(args.speaker.name, args.transcript, args.input);
|
|
1088
|
+
const backendInput = { task, message: args.input, messages };
|
|
1089
|
+
const startCtx = {
|
|
1090
|
+
task,
|
|
1091
|
+
knowledge,
|
|
1092
|
+
signal: args.signal,
|
|
1093
|
+
runId: args.runId,
|
|
1094
|
+
turnId: args.turnId,
|
|
1095
|
+
propagatedHeaders: args.propagatedHeaders
|
|
1096
|
+
};
|
|
1097
|
+
const session = args.speaker.backend.start ? touchSession(await args.speaker.backend.start(backendInput, startCtx)) : newRuntimeSession(args.speaker.backend.kind, void 0, {
|
|
1098
|
+
runId: args.runId,
|
|
1099
|
+
turnIndex: args.turnIndex,
|
|
1100
|
+
turnId: args.turnId,
|
|
1101
|
+
speaker: args.speaker.name
|
|
1102
|
+
});
|
|
1103
|
+
const streamCtx = {
|
|
1104
|
+
task,
|
|
1105
|
+
knowledge,
|
|
1106
|
+
session,
|
|
1107
|
+
signal: args.signal,
|
|
1108
|
+
runId: args.runId,
|
|
1109
|
+
turnId: args.turnId,
|
|
1110
|
+
propagatedHeaders: args.propagatedHeaders
|
|
1111
|
+
};
|
|
1112
|
+
for await (const event of args.speaker.backend.stream(backendInput, streamCtx)) {
|
|
1113
|
+
if (args.signal.aborted) {
|
|
1114
|
+
const reason = args.signal.reason;
|
|
1115
|
+
throw reason instanceof Error ? reason : new Error("aborted");
|
|
1116
|
+
}
|
|
1117
|
+
if (event.type === "text_delta") {
|
|
1118
|
+
args.aggregator.appendText(event.text);
|
|
1119
|
+
yield { text: event.text, timestamp: event.timestamp };
|
|
1120
|
+
} else if (event.type === "llm_call") {
|
|
1121
|
+
args.aggregator.recordUsage(event);
|
|
1122
|
+
} else if (event.type === "final") {
|
|
1123
|
+
args.aggregator.adoptFinalText(event.text);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
var TurnAggregator = class {
|
|
1128
|
+
constructor(base) {
|
|
1129
|
+
this.base = base;
|
|
1130
|
+
}
|
|
1131
|
+
base;
|
|
1132
|
+
text = "";
|
|
1133
|
+
adoptedFinal = false;
|
|
1134
|
+
usage;
|
|
1135
|
+
appendText(text) {
|
|
1136
|
+
if (this.adoptedFinal) return;
|
|
1137
|
+
this.text += text;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Use the backend's `final.text` only when no streamed deltas were observed.
|
|
1141
|
+
* Some backends emit deltas AND a final summary; treating both as content
|
|
1142
|
+
* would double-count.
|
|
1143
|
+
*/
|
|
1144
|
+
adoptFinalText(text) {
|
|
1145
|
+
if (!text) return;
|
|
1146
|
+
if (this.text.length > 0) return;
|
|
1147
|
+
this.text = text;
|
|
1148
|
+
this.adoptedFinal = true;
|
|
1149
|
+
}
|
|
1150
|
+
recordUsage(event) {
|
|
1151
|
+
const u = this.usage ?? {};
|
|
1152
|
+
if (event.tokensIn !== void 0) u.tokensIn = (u.tokensIn ?? 0) + event.tokensIn;
|
|
1153
|
+
if (event.tokensOut !== void 0) u.tokensOut = (u.tokensOut ?? 0) + event.tokensOut;
|
|
1154
|
+
if (event.costUsd !== void 0) u.costUsd = (u.costUsd ?? 0) + event.costUsd;
|
|
1155
|
+
if (event.latencyMs !== void 0) u.latencyMs = event.latencyMs;
|
|
1156
|
+
if (event.model !== void 0) u.model = event.model;
|
|
1157
|
+
this.usage = u;
|
|
1158
|
+
}
|
|
1159
|
+
toTurn(meta) {
|
|
1160
|
+
return {
|
|
1161
|
+
index: this.base.index,
|
|
1162
|
+
speaker: this.base.speaker,
|
|
1163
|
+
turnId: meta.turnId,
|
|
1164
|
+
text: this.text.trim(),
|
|
1165
|
+
usage: this.usage,
|
|
1166
|
+
attempts: meta.attempts,
|
|
1167
|
+
startedAt: this.base.startedAt,
|
|
1168
|
+
endedAt: nowIso()
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
function buildMessagesFor(speakerName, transcript, currentInput) {
|
|
1173
|
+
const messages = [];
|
|
1174
|
+
for (const turn of transcript) {
|
|
1175
|
+
if (turn.speaker === speakerName) {
|
|
1176
|
+
messages.push({ role: "assistant", content: turn.text });
|
|
1177
|
+
} else {
|
|
1178
|
+
messages.push({ role: "user", content: `[${turn.speaker}] ${turn.text}` });
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (currentInput) messages.push({ role: "user", content: currentInput });
|
|
1182
|
+
return messages;
|
|
1183
|
+
}
|
|
1184
|
+
function resolveAuthForwarding(participant, state) {
|
|
1185
|
+
const decision = typeof participant.authSource === "function" ? participant.authSource(state) : participant.authSource ?? "forward-user";
|
|
1186
|
+
return decision === "forward-user";
|
|
1187
|
+
}
|
|
1188
|
+
function selectSpeaker(order, participantCount, state) {
|
|
1189
|
+
const resolved = order ?? (participantCount === 2 ? "alternate" : "round-robin");
|
|
1190
|
+
if (resolved === "alternate" || resolved === "round-robin") {
|
|
1191
|
+
return state.turnIndex % participantCount;
|
|
1192
|
+
}
|
|
1193
|
+
if (typeof resolved === "function") {
|
|
1194
|
+
const idx = resolved(state);
|
|
1195
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= participantCount) {
|
|
1196
|
+
throw new BackendTransportError(
|
|
1197
|
+
"conversation",
|
|
1198
|
+
`turnOrder function returned invalid index ${String(idx)} for ${participantCount} participants`
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
return idx;
|
|
1202
|
+
}
|
|
1203
|
+
throw new BackendTransportError("conversation", `unknown turnOrder: ${String(resolved)}`);
|
|
1204
|
+
}
|
|
1205
|
+
function centsFromUsd(usd) {
|
|
1206
|
+
return Math.round(usd * 100);
|
|
1207
|
+
}
|
|
1208
|
+
function passingReadiness(taskId) {
|
|
1209
|
+
return {
|
|
1210
|
+
taskId,
|
|
1211
|
+
readinessScore: 1,
|
|
1212
|
+
blockingMissingRequirements: [],
|
|
1213
|
+
nonBlockingGaps: [],
|
|
1214
|
+
recommendedAction: "run_agent",
|
|
1215
|
+
bundle: {
|
|
1216
|
+
taskId,
|
|
1217
|
+
requirements: [],
|
|
1218
|
+
evidenceIds: [],
|
|
1219
|
+
claimIds: [],
|
|
1220
|
+
wikiPageIds: [],
|
|
1221
|
+
userAnswers: {},
|
|
1222
|
+
missing: [],
|
|
1223
|
+
readinessScore: 1
|
|
1224
|
+
},
|
|
1225
|
+
severity: "info",
|
|
1226
|
+
reason: "conversation-mode: readiness gating not applied per-turn"
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/conversation/conversation-backend.ts
|
|
1231
|
+
function createConversationBackend(options) {
|
|
1232
|
+
const kind = options.kind ?? "conversation";
|
|
1233
|
+
return {
|
|
1234
|
+
kind,
|
|
1235
|
+
start(_input, context) {
|
|
1236
|
+
return newRuntimeSession(kind, context.requestedSessionId, {
|
|
1237
|
+
participants: options.conversation.participants.map((p) => p.name)
|
|
1238
|
+
});
|
|
1239
|
+
},
|
|
1240
|
+
async *stream(input, context) {
|
|
1241
|
+
const seed = input.message ?? input.messages?.at(-1)?.content ?? context.task.intent;
|
|
1242
|
+
const task = context.task;
|
|
1243
|
+
const session = context.session;
|
|
1244
|
+
yield { type: "backend_start", task, session, backend: kind, timestamp: nowIso() };
|
|
1245
|
+
let finalText = "";
|
|
1246
|
+
let totalCostUsd = 0;
|
|
1247
|
+
let totalTokensIn = 0;
|
|
1248
|
+
let totalTokensOut = 0;
|
|
1249
|
+
const inboundDepth = parseInboundDepth(context.propagatedHeaders);
|
|
1250
|
+
for await (const event of runConversationStream(options.conversation, {
|
|
1251
|
+
seed,
|
|
1252
|
+
signal: context.signal,
|
|
1253
|
+
runId: context.runId,
|
|
1254
|
+
propagatedHeaders: context.propagatedHeaders,
|
|
1255
|
+
inboundDepth,
|
|
1256
|
+
parentTurnId: context.turnId
|
|
1257
|
+
})) {
|
|
1258
|
+
if (event.type === "turn_end") {
|
|
1259
|
+
const tagged = `[${event.turn.speaker}] ${event.turn.text}
|
|
1260
|
+
`;
|
|
1261
|
+
finalText += tagged;
|
|
1262
|
+
yield { type: "text_delta", task, session, text: tagged, timestamp: event.timestamp };
|
|
1263
|
+
if (event.turn.usage) {
|
|
1264
|
+
const u = event.turn.usage;
|
|
1265
|
+
if (u.costUsd !== void 0) totalCostUsd += u.costUsd;
|
|
1266
|
+
if (u.tokensIn !== void 0) totalTokensIn += u.tokensIn;
|
|
1267
|
+
if (u.tokensOut !== void 0) totalTokensOut += u.tokensOut;
|
|
1268
|
+
yield {
|
|
1269
|
+
type: "llm_call",
|
|
1270
|
+
task,
|
|
1271
|
+
session,
|
|
1272
|
+
model: u.model ?? `${kind}/${event.turn.speaker}`,
|
|
1273
|
+
tokensIn: u.tokensIn,
|
|
1274
|
+
tokensOut: u.tokensOut,
|
|
1275
|
+
costUsd: u.costUsd,
|
|
1276
|
+
latencyMs: u.latencyMs,
|
|
1277
|
+
timestamp: event.timestamp
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
} else if (event.type === "conversation_end") {
|
|
1281
|
+
const halt = event.result.halted;
|
|
1282
|
+
yield {
|
|
1283
|
+
type: "final",
|
|
1284
|
+
task,
|
|
1285
|
+
session,
|
|
1286
|
+
status: halt.kind === "participant_error" ? "failed" : "completed",
|
|
1287
|
+
reason: describeHalt(halt),
|
|
1288
|
+
text: finalText.trim(),
|
|
1289
|
+
metadata: {
|
|
1290
|
+
conversationRunId: event.result.runId,
|
|
1291
|
+
turns: event.result.turns,
|
|
1292
|
+
spentCreditsCents: event.result.spentCreditsCents,
|
|
1293
|
+
halted: halt,
|
|
1294
|
+
durationMs: event.result.durationMs,
|
|
1295
|
+
tokensIn: totalTokensIn,
|
|
1296
|
+
tokensOut: totalTokensOut,
|
|
1297
|
+
costUsd: totalCostUsd
|
|
1298
|
+
},
|
|
1299
|
+
timestamp: event.timestamp
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
yield { type: "backend_end", task, session, backend: kind, timestamp: nowIso() };
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
function parseInboundDepth(headers) {
|
|
1308
|
+
if (!headers) return 0;
|
|
1309
|
+
const raw = headers[FORWARD_HEADERS.depth];
|
|
1310
|
+
if (raw === void 0) return 0;
|
|
1311
|
+
try {
|
|
1312
|
+
return readDepth({ [FORWARD_HEADERS.depth]: raw });
|
|
1313
|
+
} catch {
|
|
1314
|
+
return 0;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function describeHalt(halt) {
|
|
1318
|
+
switch (halt.kind) {
|
|
1319
|
+
case "max_turns":
|
|
1320
|
+
return `max_turns (${halt.turns})`;
|
|
1321
|
+
case "max_credits":
|
|
1322
|
+
return `max_credits (${halt.spentCents}/${halt.capCents}\xA2)`;
|
|
1323
|
+
case "predicate":
|
|
1324
|
+
return `predicate: ${halt.reason}`;
|
|
1325
|
+
case "abort":
|
|
1326
|
+
return "abort";
|
|
1327
|
+
case "participant_error":
|
|
1328
|
+
return `participant_error[${halt.participant}]: ${halt.message}`;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/conversation/define-conversation.ts
|
|
1333
|
+
function defineConversation(input) {
|
|
1334
|
+
if (input.participants.length < 2) {
|
|
1335
|
+
throw new ValidationError(
|
|
1336
|
+
`Conversation requires at least 2 participants; received ${input.participants.length}.`
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1340
|
+
for (const p of input.participants) {
|
|
1341
|
+
if (!p.name || p.name.trim() === "") {
|
|
1342
|
+
throw new ValidationError("Conversation participant.name must be a non-empty string.");
|
|
1343
|
+
}
|
|
1344
|
+
if (seen.has(p.name)) {
|
|
1345
|
+
throw new ValidationError(
|
|
1346
|
+
`Conversation participant names must be unique within a Conversation; '${p.name}' appears more than once.`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
seen.add(p.name);
|
|
1350
|
+
if (!p.backend || typeof p.backend.stream !== "function") {
|
|
1351
|
+
throw new ValidationError(
|
|
1352
|
+
`Conversation participant '${p.name}' is missing a backend with a stream() method.`
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
const policy = normalizePolicy(input.policy, input.participants.length);
|
|
1357
|
+
return {
|
|
1358
|
+
participants: input.participants,
|
|
1359
|
+
policy
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
function normalizePolicy(policy, participantCount) {
|
|
1363
|
+
if (!Number.isInteger(policy.maxTurns) || policy.maxTurns < 1) {
|
|
1364
|
+
throw new ValidationError(
|
|
1365
|
+
`ConversationPolicy.maxTurns must be a positive integer; received ${String(policy.maxTurns)}.`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
if (policy.maxCreditsCents !== void 0 && (!Number.isFinite(policy.maxCreditsCents) || policy.maxCreditsCents < 0)) {
|
|
1369
|
+
throw new ValidationError(
|
|
1370
|
+
`ConversationPolicy.maxCreditsCents must be a non-negative finite number when set; received ${String(
|
|
1371
|
+
policy.maxCreditsCents
|
|
1372
|
+
)}.`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
const turnOrder = policy.turnOrder ?? (participantCount === 2 ? "alternate" : "round-robin");
|
|
1376
|
+
if (turnOrder === "alternate" && participantCount !== 2) {
|
|
1377
|
+
throw new ValidationError(
|
|
1378
|
+
`ConversationPolicy.turnOrder 'alternate' requires exactly 2 participants; received ${participantCount}. Use 'round-robin' or a custom selector for N-party conversations.`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
return { ...policy, turnOrder };
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/conversation/journal.ts
|
|
1385
|
+
var InMemoryConversationJournal = class {
|
|
1386
|
+
entries = /* @__PURE__ */ new Map();
|
|
1387
|
+
async loadRun(runId) {
|
|
1388
|
+
const entry = this.entries.get(runId);
|
|
1389
|
+
if (!entry) return void 0;
|
|
1390
|
+
return {
|
|
1391
|
+
runId: entry.runId,
|
|
1392
|
+
startedAt: entry.startedAt,
|
|
1393
|
+
halted: entry.halted,
|
|
1394
|
+
endedAt: entry.endedAt,
|
|
1395
|
+
turns: [...entry.turns]
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
async beginRun(runId, startedAt) {
|
|
1399
|
+
const existing = this.entries.get(runId);
|
|
1400
|
+
if (existing) {
|
|
1401
|
+
if (existing.startedAt !== startedAt) {
|
|
1402
|
+
throw new Error(
|
|
1403
|
+
`runId '${runId}' already exists with startedAt=${existing.startedAt}; refusing to overwrite with ${startedAt}`
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
this.entries.set(runId, { runId, startedAt, turns: [] });
|
|
1409
|
+
}
|
|
1410
|
+
async appendTurn(runId, turn) {
|
|
1411
|
+
const entry = this.entries.get(runId);
|
|
1412
|
+
if (!entry) {
|
|
1413
|
+
throw new Error(
|
|
1414
|
+
`appendTurn called for unknown runId '${runId}'; call beginRun first or use the runner which handles it`
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
if (entry.halted) {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
`cannot append turn to halted run '${runId}' (halt reason: ${JSON.stringify(entry.halted)})`
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
entry.turns.push(turn);
|
|
1423
|
+
}
|
|
1424
|
+
async recordHalt(runId, halt, endedAt) {
|
|
1425
|
+
const entry = this.entries.get(runId);
|
|
1426
|
+
if (!entry) {
|
|
1427
|
+
throw new Error(`recordHalt called for unknown runId '${runId}'`);
|
|
1428
|
+
}
|
|
1429
|
+
entry.halted = halt;
|
|
1430
|
+
entry.endedAt = endedAt;
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
var FileConversationJournal = class {
|
|
1434
|
+
constructor(path) {
|
|
1435
|
+
this.path = path;
|
|
1436
|
+
}
|
|
1437
|
+
path;
|
|
1438
|
+
async loadRun(runId) {
|
|
1439
|
+
const fs = await import("fs/promises");
|
|
1440
|
+
let text;
|
|
1441
|
+
try {
|
|
1442
|
+
text = await fs.readFile(this.path, "utf8");
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
if (isNoEntError(err)) return void 0;
|
|
1445
|
+
throw err;
|
|
1446
|
+
}
|
|
1447
|
+
const lines = text.split("\n").filter((line) => line.length > 0);
|
|
1448
|
+
let entry;
|
|
1449
|
+
for (const line of lines) {
|
|
1450
|
+
const record = JSON.parse(line);
|
|
1451
|
+
if (record.runId !== runId) continue;
|
|
1452
|
+
if (record.kind === "begin") {
|
|
1453
|
+
entry = { runId, startedAt: record.startedAt, turns: [] };
|
|
1454
|
+
} else if (record.kind === "turn") {
|
|
1455
|
+
if (!entry) {
|
|
1456
|
+
throw new Error(
|
|
1457
|
+
`journal corrupted: turn record for runId '${runId}' precedes its begin record`
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
entry.turns.push(record.turn);
|
|
1461
|
+
} else if (record.kind === "halt") {
|
|
1462
|
+
if (!entry) {
|
|
1463
|
+
throw new Error(
|
|
1464
|
+
`journal corrupted: halt record for runId '${runId}' precedes its begin record`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
entry.halted = record.halted;
|
|
1468
|
+
entry.endedAt = record.endedAt;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return entry;
|
|
1472
|
+
}
|
|
1473
|
+
async beginRun(runId, startedAt) {
|
|
1474
|
+
const existing = await this.loadRun(runId);
|
|
1475
|
+
if (existing) {
|
|
1476
|
+
if (existing.startedAt !== startedAt) {
|
|
1477
|
+
throw new Error(
|
|
1478
|
+
`runId '${runId}' already exists in ${this.path} with startedAt=${existing.startedAt}; refusing to overwrite with ${startedAt}`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
await this.appendRecord({ kind: "begin", runId, startedAt });
|
|
1484
|
+
}
|
|
1485
|
+
async appendTurn(runId, turn) {
|
|
1486
|
+
await this.appendRecord({ kind: "turn", runId, turn });
|
|
1487
|
+
}
|
|
1488
|
+
async recordHalt(runId, halt, endedAt) {
|
|
1489
|
+
await this.appendRecord({ kind: "halt", runId, halted: halt, endedAt });
|
|
1490
|
+
}
|
|
1491
|
+
async appendRecord(record) {
|
|
1492
|
+
const fs = await import("fs/promises");
|
|
1493
|
+
const path = await import("path");
|
|
1494
|
+
await fs.mkdir(path.dirname(this.path), { recursive: true });
|
|
1495
|
+
const fh = await fs.open(this.path, "a");
|
|
1496
|
+
try {
|
|
1497
|
+
await fh.write(`${JSON.stringify(record)}
|
|
1498
|
+
`);
|
|
1499
|
+
await fh.sync();
|
|
1500
|
+
} finally {
|
|
1501
|
+
await fh.close();
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
function isNoEntError(err) {
|
|
1506
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/conversation/journal-sql.ts
|
|
1510
|
+
function d1ToSqlAdapter(db) {
|
|
1511
|
+
return {
|
|
1512
|
+
async exec(sql, params = []) {
|
|
1513
|
+
const stmt = db.prepare(sql);
|
|
1514
|
+
const bound = params.length > 0 ? stmt.bind(...params) : stmt;
|
|
1515
|
+
const result = await bound.run();
|
|
1516
|
+
const meta = result.meta;
|
|
1517
|
+
return { rowsAffected: meta?.rows_written ?? meta?.changes ?? 0 };
|
|
1518
|
+
},
|
|
1519
|
+
async query(sql, params = []) {
|
|
1520
|
+
const stmt = db.prepare(sql);
|
|
1521
|
+
const bound = params.length > 0 ? stmt.bind(...params) : stmt;
|
|
1522
|
+
const result = await bound.all();
|
|
1523
|
+
return result.results ?? [];
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
var RUNS_TABLE_DDL = (table) => `
|
|
1528
|
+
CREATE TABLE IF NOT EXISTS ${table}_runs (
|
|
1529
|
+
run_id TEXT PRIMARY KEY,
|
|
1530
|
+
started_at TEXT NOT NULL,
|
|
1531
|
+
halted_kind TEXT,
|
|
1532
|
+
halted_payload TEXT,
|
|
1533
|
+
ended_at TEXT
|
|
1534
|
+
)
|
|
1535
|
+
`;
|
|
1536
|
+
var TURNS_TABLE_DDL = (table) => `
|
|
1537
|
+
CREATE TABLE IF NOT EXISTS ${table}_turns (
|
|
1538
|
+
run_id TEXT NOT NULL,
|
|
1539
|
+
turn_index INTEGER NOT NULL,
|
|
1540
|
+
payload TEXT NOT NULL,
|
|
1541
|
+
PRIMARY KEY (run_id, turn_index)
|
|
1542
|
+
)
|
|
1543
|
+
`;
|
|
1544
|
+
var TURNS_INDEX_DDL = (table) => `
|
|
1545
|
+
CREATE INDEX IF NOT EXISTS idx_${table}_turns_run ON ${table}_turns (run_id, turn_index)
|
|
1546
|
+
`;
|
|
1547
|
+
var SqlConversationJournal = class {
|
|
1548
|
+
/**
|
|
1549
|
+
* @param db SQL adapter (D1, postgres, sqlite, libSQL — all work)
|
|
1550
|
+
* @param table Table-name prefix; the journal creates `${table}_runs` and
|
|
1551
|
+
* `${table}_turns`. Lets multiple journals share a database
|
|
1552
|
+
* without colliding (e.g. one per product surface).
|
|
1553
|
+
*/
|
|
1554
|
+
constructor(db, table = "agent_runtime_journal") {
|
|
1555
|
+
this.db = db;
|
|
1556
|
+
this.table = table;
|
|
1557
|
+
}
|
|
1558
|
+
db;
|
|
1559
|
+
table;
|
|
1560
|
+
/**
|
|
1561
|
+
* Create the journal's tables if absent. Idempotent. Call once at deploy
|
|
1562
|
+
* (or at app boot) — running on every request is harmless but adds latency.
|
|
1563
|
+
*/
|
|
1564
|
+
async migrate() {
|
|
1565
|
+
await this.db.exec(RUNS_TABLE_DDL(this.table));
|
|
1566
|
+
await this.db.exec(TURNS_TABLE_DDL(this.table));
|
|
1567
|
+
await this.db.exec(TURNS_INDEX_DDL(this.table));
|
|
1568
|
+
}
|
|
1569
|
+
async loadRun(runId) {
|
|
1570
|
+
const runs = await this.db.query(
|
|
1571
|
+
`SELECT run_id, started_at, halted_kind, halted_payload, ended_at FROM ${this.table}_runs WHERE run_id = ?`,
|
|
1572
|
+
[runId]
|
|
1573
|
+
);
|
|
1574
|
+
const row = runs[0];
|
|
1575
|
+
if (!row) return void 0;
|
|
1576
|
+
const turns = await this.db.query(
|
|
1577
|
+
`SELECT payload, turn_index FROM ${this.table}_turns WHERE run_id = ? ORDER BY turn_index ASC`,
|
|
1578
|
+
[runId]
|
|
1579
|
+
);
|
|
1580
|
+
return {
|
|
1581
|
+
runId: row.run_id,
|
|
1582
|
+
startedAt: row.started_at,
|
|
1583
|
+
halted: row.halted_payload ? JSON.parse(row.halted_payload) : void 0,
|
|
1584
|
+
endedAt: row.ended_at ?? void 0,
|
|
1585
|
+
turns: turns.map((t) => JSON.parse(t.payload))
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
async beginRun(runId, startedAt) {
|
|
1589
|
+
const existing = await this.db.query(
|
|
1590
|
+
`SELECT started_at FROM ${this.table}_runs WHERE run_id = ?`,
|
|
1591
|
+
[runId]
|
|
1592
|
+
);
|
|
1593
|
+
if (existing.length > 0) {
|
|
1594
|
+
if (existing[0]?.started_at !== startedAt) {
|
|
1595
|
+
throw new Error(
|
|
1596
|
+
`runId '${runId}' already exists with startedAt=${existing[0]?.started_at}; refusing to overwrite with ${startedAt}`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
await this.db.exec(`INSERT INTO ${this.table}_runs (run_id, started_at) VALUES (?, ?)`, [
|
|
1602
|
+
runId,
|
|
1603
|
+
startedAt
|
|
1604
|
+
]);
|
|
1605
|
+
}
|
|
1606
|
+
async appendTurn(runId, turn) {
|
|
1607
|
+
const halted = await this.db.query(
|
|
1608
|
+
`SELECT halted_kind FROM ${this.table}_runs WHERE run_id = ?`,
|
|
1609
|
+
[runId]
|
|
1610
|
+
);
|
|
1611
|
+
if (halted.length === 0) {
|
|
1612
|
+
throw new Error(
|
|
1613
|
+
`appendTurn called for unknown runId '${runId}'; call beginRun first or use the runner which handles it`
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
if (halted[0]?.halted_kind) {
|
|
1617
|
+
throw new Error(
|
|
1618
|
+
`cannot append turn to halted run '${runId}' (halt kind: ${halted[0]?.halted_kind})`
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
await this.db.exec(
|
|
1622
|
+
`INSERT INTO ${this.table}_turns (run_id, turn_index, payload) VALUES (?, ?, ?)`,
|
|
1623
|
+
[runId, turn.index, JSON.stringify(turn)]
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
async recordHalt(runId, halt, endedAt) {
|
|
1627
|
+
const rs = await this.db.exec(
|
|
1628
|
+
`UPDATE ${this.table}_runs SET halted_kind = ?, halted_payload = ?, ended_at = ? WHERE run_id = ?`,
|
|
1629
|
+
[halt.kind, JSON.stringify(halt), endedAt, runId]
|
|
1630
|
+
);
|
|
1631
|
+
if (rs.rowsAffected === 0) {
|
|
1632
|
+
throw new Error(`recordHalt called for unknown runId '${runId}'`);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
};
|
|
1636
|
+
|
|
627
1637
|
// src/durable/chat-engine.ts
|
|
628
1638
|
var encoder = new TextEncoder();
|
|
629
1639
|
function encodeLine(event) {
|
|
@@ -1681,37 +2691,59 @@ function stripNewlines(value) {
|
|
|
1681
2691
|
export {
|
|
1682
2692
|
AgentEvalError,
|
|
1683
2693
|
BackendTransportError,
|
|
2694
|
+
CircuitBreakerState,
|
|
2695
|
+
CircuitOpenError,
|
|
1684
2696
|
ConfigError,
|
|
2697
|
+
DEFAULT_MAX_DEPTH,
|
|
1685
2698
|
DEFAULT_ROUTER_BASE_URL,
|
|
2699
|
+
DeadlineExceededError,
|
|
2700
|
+
FORWARD_HEADERS,
|
|
2701
|
+
FileConversationJournal,
|
|
2702
|
+
InMemoryConversationJournal,
|
|
1686
2703
|
InMemoryRuntimeSessionStore,
|
|
1687
2704
|
JudgeError,
|
|
1688
2705
|
NotFoundError,
|
|
1689
2706
|
RuntimeRunStateError,
|
|
2707
|
+
SqlConversationJournal,
|
|
1690
2708
|
ValidationError,
|
|
2709
|
+
buildForwardHeaders,
|
|
1691
2710
|
cleanModelId,
|
|
2711
|
+
computeBackoff,
|
|
2712
|
+
createConversationBackend,
|
|
1692
2713
|
createIterableBackend,
|
|
1693
2714
|
createOpenAICompatibleBackend,
|
|
1694
2715
|
createOtelExporter,
|
|
1695
2716
|
createRuntimeEventCollector,
|
|
1696
2717
|
createRuntimeStreamEventCollector,
|
|
1697
2718
|
createSandboxPromptBackend,
|
|
2719
|
+
d1ToSqlAdapter,
|
|
1698
2720
|
decideKnowledgeReadiness,
|
|
2721
|
+
defaultIsRetryable,
|
|
2722
|
+
defineConversation,
|
|
1699
2723
|
deriveExecutionId,
|
|
1700
2724
|
getModels,
|
|
1701
2725
|
handleChatTurn,
|
|
2726
|
+
isDepthExceeded,
|
|
1702
2727
|
loopEventToOtelSpan,
|
|
2728
|
+
makePerAttemptSignal,
|
|
1703
2729
|
mcpToolsForRuntimeMcp,
|
|
1704
2730
|
mcpToolsForRuntimeMcpSubset,
|
|
2731
|
+
readDepth,
|
|
1705
2732
|
readinessServerSentEvent,
|
|
1706
2733
|
resolveChatModel,
|
|
1707
2734
|
resolveRouterBaseUrl,
|
|
1708
2735
|
runAgentTask,
|
|
1709
2736
|
runAgentTaskStream,
|
|
2737
|
+
runConversation,
|
|
2738
|
+
runConversationStream,
|
|
1710
2739
|
runtimeStreamServerSentEvent,
|
|
1711
2740
|
sanitizeAgentRuntimeEvent,
|
|
1712
2741
|
sanitizeKnowledgeReadinessReport,
|
|
1713
2742
|
sanitizeRuntimeStreamEvent,
|
|
2743
|
+
sleep2 as sleep,
|
|
2744
|
+
slugifySpeaker,
|
|
1714
2745
|
startRuntimeRun,
|
|
2746
|
+
turnId,
|
|
1715
2747
|
validateChatModelId
|
|
1716
2748
|
};
|
|
1717
2749
|
//# sourceMappingURL=index.js.map
|