@koda-sl/baker-bridge 0.71.0 → 0.71.2-dev.5e66c2900
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 +42 -13
- package/dist/hono/bridge-job.d.ts +32 -1
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +109 -26
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +156 -4
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/context-usage.d.ts +18 -0
- package/dist/hono/context-usage.d.ts.map +1 -1
- package/dist/hono/context-usage.js +51 -0
- package/dist/hono/context-usage.js.map +1 -1
- package/dist/hono/context-usage.test.js +23 -1
- package/dist/hono/context-usage.test.js.map +1 -1
- package/dist/hono/convex.d.ts +1 -1
- package/dist/hono/convex.d.ts.map +1 -1
- package/dist/hono/convex.js.map +1 -1
- package/dist/hono/relay.d.ts +1 -1
- package/dist/hono/relay.d.ts.map +1 -1
- package/dist/hono/relay.js.map +1 -1
- package/dist/hono/session-continuity.d.ts +4 -3
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.js +6 -5
- package/dist/hono/session-continuity.js.map +1 -1
- package/package.json +1 -1
|
@@ -216,6 +216,13 @@ function resultMessage(costUsd, sessionId, modelUsage) {
|
|
|
216
216
|
...(modelUsage ? { modelUsage } : {}),
|
|
217
217
|
};
|
|
218
218
|
}
|
|
219
|
+
function streamEvent(event, parentToolUseId) {
|
|
220
|
+
return {
|
|
221
|
+
type: "stream_event",
|
|
222
|
+
event,
|
|
223
|
+
parent_tool_use_id: parentToolUseId ?? null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
219
226
|
function assistantMessage(uuid, text, sessionId, extras) {
|
|
220
227
|
return {
|
|
221
228
|
type: "assistant",
|
|
@@ -573,6 +580,75 @@ describe("BridgeJobExecutor", () => {
|
|
|
573
580
|
{ target: { threadId: "thread_zero", turnId: "turn_zero" }, tokens: 96000, window: 200000 },
|
|
574
581
|
]);
|
|
575
582
|
});
|
|
583
|
+
// The shape production actually sends: OpenRouter zeroes `message_start` and
|
|
584
|
+
// the assembled assistant message with it, and reports the real occupancy only
|
|
585
|
+
// on `message_delta`. Read from the message alone, the meter never had a single
|
|
586
|
+
// reading to show.
|
|
587
|
+
it("reads the meter off the stream when the assistant message reports no usage", async () => {
|
|
588
|
+
const relay = recordingRelay();
|
|
589
|
+
const executor = createExecutor({
|
|
590
|
+
relay,
|
|
591
|
+
queryResults: [
|
|
592
|
+
scriptedMessages(streamEvent({
|
|
593
|
+
type: "message_start",
|
|
594
|
+
message: { usage: { input_tokens: 0, cache_read_input_tokens: null, cache_creation_input_tokens: null } },
|
|
595
|
+
}), streamEvent({
|
|
596
|
+
type: "message_delta",
|
|
597
|
+
delta: { stop_reason: "end_turn" },
|
|
598
|
+
usage: { input_tokens: 5000, output_tokens: 300, cache_read_input_tokens: 90000 },
|
|
599
|
+
}), assistantMessage("assistant_zeroed", "Done.", "session_next", {
|
|
600
|
+
usage: { input_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
|
|
601
|
+
}), resultMessage(0.02, "session_next", {
|
|
602
|
+
"claude-main": {
|
|
603
|
+
inputTokens: 0,
|
|
604
|
+
outputTokens: 0,
|
|
605
|
+
cacheReadInputTokens: 0,
|
|
606
|
+
cacheCreationInputTokens: 0,
|
|
607
|
+
webSearchRequests: 0,
|
|
608
|
+
costUSD: 0.02,
|
|
609
|
+
contextWindow: 200000,
|
|
610
|
+
maxOutputTokens: 32000,
|
|
611
|
+
},
|
|
612
|
+
})),
|
|
613
|
+
],
|
|
614
|
+
});
|
|
615
|
+
await executor.execute({ threadId: "thread_stream_usage", turnId: "turn_stream_usage", prompt: "hi", history: history([]) }, activeControls());
|
|
616
|
+
expect(relay.contextUsageUpdates).toEqual([
|
|
617
|
+
{ target: { threadId: "thread_stream_usage", turnId: "turn_stream_usage" }, tokens: 95000, window: 200000 },
|
|
618
|
+
]);
|
|
619
|
+
});
|
|
620
|
+
it("ignores subagent stream usage — only the main conversation drives the meter", async () => {
|
|
621
|
+
const relay = recordingRelay();
|
|
622
|
+
const executor = createExecutor({
|
|
623
|
+
relay,
|
|
624
|
+
queryResults: [
|
|
625
|
+
scriptedMessages(streamEvent({
|
|
626
|
+
type: "message_delta",
|
|
627
|
+
delta: { stop_reason: "end_turn" },
|
|
628
|
+
usage: { input_tokens: 5000, cache_read_input_tokens: 90000 },
|
|
629
|
+
}), streamEvent({
|
|
630
|
+
type: "message_delta",
|
|
631
|
+
delta: { stop_reason: "end_turn" },
|
|
632
|
+
usage: { input_tokens: 400000, cache_read_input_tokens: 0 },
|
|
633
|
+
}, "toolu_subagent"), resultMessage(0.02, "session_next", {
|
|
634
|
+
"claude-main": {
|
|
635
|
+
inputTokens: 0,
|
|
636
|
+
outputTokens: 0,
|
|
637
|
+
cacheReadInputTokens: 0,
|
|
638
|
+
cacheCreationInputTokens: 0,
|
|
639
|
+
webSearchRequests: 0,
|
|
640
|
+
costUSD: 0.02,
|
|
641
|
+
contextWindow: 200000,
|
|
642
|
+
maxOutputTokens: 32000,
|
|
643
|
+
},
|
|
644
|
+
})),
|
|
645
|
+
],
|
|
646
|
+
});
|
|
647
|
+
await executor.execute({ threadId: "thread_sub_stream", turnId: "turn_sub_stream", prompt: "hi", history: history([]) }, activeControls());
|
|
648
|
+
expect(relay.contextUsageUpdates).toEqual([
|
|
649
|
+
{ target: { threadId: "thread_sub_stream", turnId: "turn_sub_stream" }, tokens: 95000, window: 200000 },
|
|
650
|
+
]);
|
|
651
|
+
});
|
|
576
652
|
it("ignores subagent request usage — only the main conversation drives the meter", async () => {
|
|
577
653
|
const relay = recordingRelay();
|
|
578
654
|
const executor = createExecutor({
|
|
@@ -747,11 +823,13 @@ describe("BridgeJobExecutor", () => {
|
|
|
747
823
|
await executor.execute({ threadId: "thread_ordinary", turnId: "turn_ordinary", prompt: "Make the hero blue", history: history([]) }, activeControls());
|
|
748
824
|
expect(queryInputs[0]?.options.settings ?? {}).not.toHaveProperty("autoCompactEnabled");
|
|
749
825
|
});
|
|
750
|
-
it("
|
|
826
|
+
it("restarts publish on a fresh session when the conversation no longer fits", async () => {
|
|
751
827
|
const queryInputs = [];
|
|
828
|
+
const clearCalls = [];
|
|
752
829
|
const executor = createExecutor({
|
|
753
830
|
loadedBoundary: cleanBoundary("session_overflow"),
|
|
754
831
|
queryInputs,
|
|
832
|
+
clearCalls,
|
|
755
833
|
queryResults: [
|
|
756
834
|
// The CLI announces its session id before the first model request, so the
|
|
757
835
|
// refusal arrives with an id but no work done — the retry still owes the
|
|
@@ -761,15 +839,43 @@ describe("BridgeJobExecutor", () => {
|
|
|
761
839
|
subtype: "init",
|
|
762
840
|
session_id: "session_overflow_live",
|
|
763
841
|
}),
|
|
764
|
-
scriptedMessages(resultMessage(0.4, "
|
|
842
|
+
scriptedMessages(resultMessage(0.4, "session_restarted")),
|
|
765
843
|
],
|
|
766
844
|
});
|
|
767
845
|
const outcome = await executor.execute({ threadId: "thread_overflow", turnId: "turn_overflow", prompt: "/publish", history: history([]) }, activeControls());
|
|
768
846
|
expect(outcome.kind).toBe("completed");
|
|
769
847
|
expect(queryInputs[0]?.options.settings).toMatchObject({ autoCompactEnabled: false });
|
|
770
|
-
|
|
848
|
+
// The oversized session is abandoned, not summarized: the retry resumes
|
|
849
|
+
// nothing and delivers the `/publish` the turn still owes.
|
|
850
|
+
expect(queryInputs[1]?.options.resume).toBeUndefined();
|
|
771
851
|
expect(queryInputs[1]?.prompt).toBe("/publish");
|
|
772
|
-
expect(
|
|
852
|
+
expect(clearCalls).toEqual([{ threadId: "thread_overflow", reason: "sdk_error" }]);
|
|
853
|
+
});
|
|
854
|
+
it("does not silently restart an ordinary message that overflows", async () => {
|
|
855
|
+
const queryInputs = [];
|
|
856
|
+
const executor = createExecutor({
|
|
857
|
+
loadedBoundary: cleanBoundary("session_ordinary_overflow"),
|
|
858
|
+
queryInputs,
|
|
859
|
+
queryResults: [
|
|
860
|
+
contextOverflowQuery({
|
|
861
|
+
type: "system",
|
|
862
|
+
subtype: "init",
|
|
863
|
+
session_id: "session_ordinary_overflow_live",
|
|
864
|
+
}),
|
|
865
|
+
],
|
|
866
|
+
});
|
|
867
|
+
const outcome = await executor.execute({
|
|
868
|
+
threadId: "thread_ordinary_overflow",
|
|
869
|
+
turnId: "turn_ordinary_overflow",
|
|
870
|
+
prompt: "Make the hero blue",
|
|
871
|
+
history: history([{ role: "user", text: "The brand colour is teal", createdAt: 1 }]),
|
|
872
|
+
}, activeControls());
|
|
873
|
+
// A restart would answer from rehydrated history instead of the live session,
|
|
874
|
+
// and an ordinary message — unlike `/publish` — is answered *from* that
|
|
875
|
+
// context. Silently trading it for a shorter one degrades the reply with no
|
|
876
|
+
// explanation, so this turn says so instead.
|
|
877
|
+
expect(queryInputs).toHaveLength(1);
|
|
878
|
+
expect(outcome.kind).toBe("errored");
|
|
773
879
|
});
|
|
774
880
|
it("resumes the in-flight publish session when the overflow lands mid-turn", async () => {
|
|
775
881
|
const queryInputs = [];
|
|
@@ -808,6 +914,52 @@ describe("BridgeJobExecutor", () => {
|
|
|
808
914
|
// progress — the retry still owes the turn its `/publish`.
|
|
809
915
|
expect(queryInputs[1]?.prompt).toBe("/publish");
|
|
810
916
|
});
|
|
917
|
+
// How the failure actually reached production: the CLI does not throw, it ends
|
|
918
|
+
// the turn on an error *result* carrying the provider's refusal.
|
|
919
|
+
it("restarts publish when the overflow arrives as an error result", async () => {
|
|
920
|
+
const relay = recordingRelay();
|
|
921
|
+
const queryInputs = [];
|
|
922
|
+
const executor = createExecutor({
|
|
923
|
+
loadedBoundary: cleanBoundary("session_result_overflow"),
|
|
924
|
+
relay,
|
|
925
|
+
queryInputs,
|
|
926
|
+
queryResults: [
|
|
927
|
+
scriptedMessages({ type: "system", subtype: "init", session_id: "session_result_overflow_live" }, assistantMessage("assistant_result_refusal", "Prompt is too long", "session_result_overflow_live"), {
|
|
928
|
+
type: "result",
|
|
929
|
+
subtype: "error_during_execution",
|
|
930
|
+
is_error: true,
|
|
931
|
+
errors: ["Prompt is too long"],
|
|
932
|
+
total_cost_usd: 0.2,
|
|
933
|
+
session_id: "session_result_overflow_live",
|
|
934
|
+
}),
|
|
935
|
+
scriptedMessages(assistantMessage("assistant_result_published", "Your changes are live.", "session_result_restarted"), resultMessage(0.4, "session_result_restarted")),
|
|
936
|
+
],
|
|
937
|
+
});
|
|
938
|
+
const outcome = await executor.execute({ threadId: "thread_result_overflow", turnId: "turn_result_overflow", prompt: "/publish", history: history([]) }, activeControls());
|
|
939
|
+
expect(outcome.kind).toBe("completed");
|
|
940
|
+
expect(queryInputs[1]?.options.resume).toBeUndefined();
|
|
941
|
+
expect(queryInputs[1]?.prompt).toBe("/publish");
|
|
942
|
+
expect(JSON.stringify(relay.assistantEvents)).not.toContain("Prompt is too long");
|
|
943
|
+
});
|
|
944
|
+
it("reports a turn the restart could not save in product language", async () => {
|
|
945
|
+
const relay = recordingRelay();
|
|
946
|
+
const executor = createExecutor({
|
|
947
|
+
loadedBoundary: cleanBoundary("session_doomed"),
|
|
948
|
+
relay,
|
|
949
|
+
queryResults: [
|
|
950
|
+
contextOverflowQuery({ type: "system", subtype: "init", session_id: "session_doomed_live" }, assistantMessage("assistant_doomed", "Prompt is too long", "session_doomed_live")),
|
|
951
|
+
// Even the restarted session is refused: the turn is out of moves.
|
|
952
|
+
contextOverflowQuery(assistantMessage("assistant_doomed_again", "Prompt is too long", "session_doomed_restarted")),
|
|
953
|
+
],
|
|
954
|
+
});
|
|
955
|
+
const outcome = await executor.execute({ threadId: "thread_doomed", turnId: "turn_doomed", prompt: "/publish", history: history([]) }, activeControls());
|
|
956
|
+
expect(outcome.kind).toBe("errored");
|
|
957
|
+
const errors = outcome.kind === "errored" ? outcome.errors.join(" ") : "";
|
|
958
|
+
expect(errors).not.toContain("Prompt is too long");
|
|
959
|
+
expect(errors).toContain("Send your message again");
|
|
960
|
+
// Said once, by the product — the provider's own words never reach the chat.
|
|
961
|
+
expect(JSON.stringify(relay.assistantEvents)).not.toContain("Prompt is too long");
|
|
962
|
+
});
|
|
811
963
|
it("does not resurrect a dropped refusal as end-of-turn text", async () => {
|
|
812
964
|
const relay = recordingRelay();
|
|
813
965
|
const executor = createExecutor({
|