@koda-sl/baker-bridge 0.152.0-dev.1a19b1876 → 0.152.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 +33 -7
- package/dist/hono/agent-session.d.ts +0 -1
- package/dist/hono/agent-session.d.ts.map +1 -1
- package/dist/hono/agent-session.js +0 -7
- package/dist/hono/agent-session.js.map +1 -1
- package/dist/hono/agent-session.test.js +1 -7
- package/dist/hono/agent-session.test.js.map +1 -1
- package/dist/hono/agent.d.ts.map +1 -1
- package/dist/hono/agent.js +5 -8
- package/dist/hono/agent.js.map +1 -1
- package/dist/hono/bridge-job.d.ts +9 -1
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +151 -20
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +233 -2
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/convex.d.ts +2 -1
- package/dist/hono/convex.d.ts.map +1 -1
- package/dist/hono/convex.js +4 -0
- package/dist/hono/convex.js.map +1 -1
- package/dist/hono/main-sync.d.ts.map +1 -1
- package/dist/hono/main-sync.js +18 -5
- package/dist/hono/main-sync.js.map +1 -1
- package/dist/hono/main-sync.test.js +12 -0
- package/dist/hono/main-sync.test.js.map +1 -1
- package/dist/hono/relay.d.ts +8 -1
- package/dist/hono/relay.d.ts.map +1 -1
- package/dist/hono/relay.js +3 -0
- package/dist/hono/relay.js.map +1 -1
- package/dist/hono/served-model.d.ts +23 -0
- package/dist/hono/served-model.d.ts.map +1 -0
- package/dist/hono/served-model.js +82 -0
- package/dist/hono/served-model.js.map +1 -0
- package/dist/hono/served-model.test.d.ts +2 -0
- package/dist/hono/served-model.test.d.ts.map +1 -0
- package/dist/hono/served-model.test.js +29 -0
- package/dist/hono/served-model.test.js.map +1 -0
- package/dist/hono/session-continuity.d.ts +30 -0
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.integration.test.js +201 -5
- package/dist/hono/session-continuity.integration.test.js.map +1 -1
- package/dist/hono/session-continuity.js +99 -8
- package/dist/hono/session-continuity.js.map +1 -1
- package/dist/hono/session-store.d.ts +2 -0
- package/dist/hono/session-store.d.ts.map +1 -1
- package/dist/hono/session-store.js +6 -0
- package/dist/hono/session-store.js.map +1 -1
- package/dist/hono/session-usage-baseline.d.ts +1 -0
- package/dist/hono/session-usage-baseline.d.ts.map +1 -1
- package/dist/hono/session-usage-baseline.js +17 -4
- package/dist/hono/session-usage-baseline.js.map +1 -1
- package/dist/hono/turn-events.d.ts +1 -1
- package/dist/hono/turn-events.d.ts.map +1 -1
- package/dist/hono/turn-events.js.map +1 -1
- package/dist/review/digest.d.ts +35 -0
- package/dist/review/digest.d.ts.map +1 -1
- package/dist/review/digest.js +171 -27
- package/dist/review/digest.js.map +1 -1
- package/dist/review/digest.test.js +130 -1
- package/dist/review/digest.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -69,6 +69,7 @@ function recordingRelay() {
|
|
|
69
69
|
const compactionUpdates = [];
|
|
70
70
|
const reviewDigests = [];
|
|
71
71
|
const retractedEvents = [];
|
|
72
|
+
const toolOutcomeBatches = [];
|
|
72
73
|
const contextUsageUpdates = [];
|
|
73
74
|
return {
|
|
74
75
|
assistantEvents,
|
|
@@ -78,6 +79,7 @@ function recordingRelay() {
|
|
|
78
79
|
contextUsageUpdates,
|
|
79
80
|
reviewDigests,
|
|
80
81
|
retractedEvents,
|
|
82
|
+
toolOutcomeBatches,
|
|
81
83
|
startPreview: () => undefined,
|
|
82
84
|
previewText: () => undefined,
|
|
83
85
|
previewTools: () => undefined,
|
|
@@ -99,6 +101,10 @@ function recordingRelay() {
|
|
|
99
101
|
retractedEvents.push({ target, eventIds });
|
|
100
102
|
return Promise.resolve();
|
|
101
103
|
},
|
|
104
|
+
toolOutcomes: (_target, outcomes) => {
|
|
105
|
+
toolOutcomeBatches.push(outcomes);
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
},
|
|
102
108
|
complete: () => Promise.resolve(),
|
|
103
109
|
inputRequest: () => Promise.resolve(true),
|
|
104
110
|
inputResolved: () => Promise.resolve(),
|
|
@@ -262,6 +268,24 @@ function memoryInFlightTurns(initial) {
|
|
|
262
268
|
},
|
|
263
269
|
};
|
|
264
270
|
}
|
|
271
|
+
// The CLI's own report of a failed model request, written as an assistant
|
|
272
|
+
// message — the shape production stores (`model: "<synthetic>"`, `error`).
|
|
273
|
+
function apiErrorMessage(uuid, text, error) {
|
|
274
|
+
return {
|
|
275
|
+
type: "assistant",
|
|
276
|
+
uuid,
|
|
277
|
+
session_id: "session_api_error",
|
|
278
|
+
parent_tool_use_id: null,
|
|
279
|
+
error,
|
|
280
|
+
is_api_error_message: true,
|
|
281
|
+
message: {
|
|
282
|
+
role: "assistant",
|
|
283
|
+
model: "<synthetic>",
|
|
284
|
+
content: [{ type: "text", text }],
|
|
285
|
+
stop_reason: "stop_sequence",
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
265
289
|
function resultMessage(costUsd, sessionId, modelUsage) {
|
|
266
290
|
return {
|
|
267
291
|
type: "result",
|
|
@@ -939,6 +963,40 @@ describe("BridgeJobExecutor", () => {
|
|
|
939
963
|
],
|
|
940
964
|
});
|
|
941
965
|
});
|
|
966
|
+
// OpenRouter answers under its own slug. A CLI resuming a transcript that names
|
|
967
|
+
// the slug drops every earlier thinking block from its first request, which
|
|
968
|
+
// breaks the prompt cache right after the system prompt (BAKER-331).
|
|
969
|
+
it("hands the CLI a transcript that names the model the conversation asked for", async () => {
|
|
970
|
+
const configDir = await mkdtemp(join(tmpdir(), "bridge-served-model-"));
|
|
971
|
+
try {
|
|
972
|
+
const transcriptPath = join(configDir, "projects", "-home-user-repo", "session_resume.jsonl");
|
|
973
|
+
await mkdir(join(configDir, "projects", "-home-user-repo"), { recursive: true });
|
|
974
|
+
await writeFile(transcriptPath, `${JSON.stringify({
|
|
975
|
+
type: "assistant",
|
|
976
|
+
requestedModel: "claude-opus-5-5",
|
|
977
|
+
message: { id: "msg_1", model: "anthropic/claude-opus-5.5", content: [{ type: "thinking", thinking: "t" }] },
|
|
978
|
+
})}\n`);
|
|
979
|
+
vi.stubEnv("CLAUDE_CONFIG_DIR", configDir);
|
|
980
|
+
const resumedFrom = [];
|
|
981
|
+
const executor = createExecutor({
|
|
982
|
+
loadedBoundary: cleanBoundary("session_resume"),
|
|
983
|
+
queryResults: [
|
|
984
|
+
{
|
|
985
|
+
async *[Symbol.asyncIterator]() {
|
|
986
|
+
await Promise.resolve();
|
|
987
|
+
resumedFrom.push(readFileSync(transcriptPath, "utf8"));
|
|
988
|
+
yield resultMessage(0.2, "session_next");
|
|
989
|
+
},
|
|
990
|
+
},
|
|
991
|
+
],
|
|
992
|
+
});
|
|
993
|
+
await executor.execute({ threadId: "thread_resume", turnId: "turn_resume", prompt: "Continue", history: history([]) }, activeControls());
|
|
994
|
+
expect(JSON.parse(resumedFrom[0] ?? "{}").message.model).toBe("claude-opus-5-5");
|
|
995
|
+
}
|
|
996
|
+
finally {
|
|
997
|
+
await rm(configDir, { recursive: true, force: true });
|
|
998
|
+
}
|
|
999
|
+
});
|
|
942
1000
|
it("publishes without the conversation, and without summarizing it first", async () => {
|
|
943
1001
|
const queryInputs = [];
|
|
944
1002
|
const savedBoundaries = [];
|
|
@@ -1739,6 +1797,28 @@ describe("BridgeJobExecutor", () => {
|
|
|
1739
1797
|
expect(queryInputs).toHaveLength(0);
|
|
1740
1798
|
expect(told).toBe(0);
|
|
1741
1799
|
});
|
|
1800
|
+
it("keeps the note for the next turn when the user stops this one before its prompt goes out", async () => {
|
|
1801
|
+
const queryInputs = [];
|
|
1802
|
+
const controls = activeControls();
|
|
1803
|
+
let told = 0;
|
|
1804
|
+
const executor = createExecutor({
|
|
1805
|
+
queryInputs,
|
|
1806
|
+
mcpServersProvider: () => {
|
|
1807
|
+
controls.deactivate();
|
|
1808
|
+
return Promise.resolve(undefined);
|
|
1809
|
+
},
|
|
1810
|
+
catchUpWithMain: () => Promise.resolve({
|
|
1811
|
+
inWorkspace: true,
|
|
1812
|
+
changed: ["src/pages/home/hero.md"],
|
|
1813
|
+
told: () => {
|
|
1814
|
+
told += 1;
|
|
1815
|
+
return Promise.resolve();
|
|
1816
|
+
},
|
|
1817
|
+
}),
|
|
1818
|
+
});
|
|
1819
|
+
await executor.execute({ threadId: "thread_stopped_late", turnId: "turn_stopped_late", prompt: "Cambia el hero", history: history([]) }, controls);
|
|
1820
|
+
expect(told).toBe(0);
|
|
1821
|
+
});
|
|
1742
1822
|
it("leaves the workspace of a finished Session alone while it is reviewed", async () => {
|
|
1743
1823
|
let caughtUp = false;
|
|
1744
1824
|
const executor = createExecutor({
|
|
@@ -1891,6 +1971,36 @@ describe("BridgeJobExecutor", () => {
|
|
|
1891
1971
|
expect(queryInputs[0]?.options).toMatchObject({ resume: "session_interrupted", forkSession: true });
|
|
1892
1972
|
expect(relay.contextSources.map((entry) => entry.source)).toEqual(["interrupted_resume"]);
|
|
1893
1973
|
});
|
|
1974
|
+
it("tells a turn sent again after a restart what another chat published while it was down", async () => {
|
|
1975
|
+
const queryInputs = [];
|
|
1976
|
+
let told = 0;
|
|
1977
|
+
const executor = createExecutor({
|
|
1978
|
+
loadedBoundary: cleanBoundary("session_clean"),
|
|
1979
|
+
queryInputs,
|
|
1980
|
+
inFlightTurns: memoryInFlightTurns({
|
|
1981
|
+
version: 1,
|
|
1982
|
+
turnId: "turn_interrupted_sync",
|
|
1983
|
+
sessionId: "session_interrupted",
|
|
1984
|
+
updatedAtMs: 1,
|
|
1985
|
+
}),
|
|
1986
|
+
catchUpWithMain: () => Promise.resolve({
|
|
1987
|
+
inWorkspace: false,
|
|
1988
|
+
changed: ["src/pages/home/hero.md"],
|
|
1989
|
+
told: () => {
|
|
1990
|
+
told += 1;
|
|
1991
|
+
return Promise.resolve();
|
|
1992
|
+
},
|
|
1993
|
+
}),
|
|
1994
|
+
});
|
|
1995
|
+
await executor.execute({
|
|
1996
|
+
threadId: "thread_interrupted_sync",
|
|
1997
|
+
turnId: "turn_interrupted_sync",
|
|
1998
|
+
prompt: "Rebuild the five pages",
|
|
1999
|
+
}, activeControls());
|
|
2000
|
+
expect(queryInputs[0]?.prompt).toBe(INTERRUPTED_RESUME_PROMPT);
|
|
2001
|
+
expect(queryInputs[0]?.reminder).toContain("- src/pages/home/hero.md");
|
|
2002
|
+
expect(told).toBe(1);
|
|
2003
|
+
});
|
|
1894
2004
|
it("runs the turn the ordinary way when the interrupted session cannot be resumed", async () => {
|
|
1895
2005
|
const relay = recordingRelay();
|
|
1896
2006
|
const queryInputs = [];
|
|
@@ -1919,6 +2029,23 @@ describe("BridgeJobExecutor", () => {
|
|
|
1919
2029
|
expect(clearCalls).toEqual([]);
|
|
1920
2030
|
expect(relay.contextSources.map((entry) => entry.source)).toEqual(["interrupted_resume", "local_resume"]);
|
|
1921
2031
|
});
|
|
2032
|
+
it("keeps the chat's conversation when an interrupted publish that cannot be resumed is run again", async () => {
|
|
2033
|
+
const executor = createExecutor({
|
|
2034
|
+
loadedBoundary: cleanBoundary("session_clean"),
|
|
2035
|
+
inFlightTurns: memoryInFlightTurns({
|
|
2036
|
+
version: 1,
|
|
2037
|
+
turnId: "turn_publish",
|
|
2038
|
+
sessionId: "session_never_written",
|
|
2039
|
+
updatedAtMs: 1,
|
|
2040
|
+
}),
|
|
2041
|
+
queryResults: [
|
|
2042
|
+
scriptedError(new Error("No conversation found with session ID: session_never_written")),
|
|
2043
|
+
scriptedMessages(assistantMessage("assistant_published", "Published", "session_publish_only"), resultMessage(0.2, "session_publish_only")),
|
|
2044
|
+
],
|
|
2045
|
+
});
|
|
2046
|
+
const outcome = await executor.execute({ threadId: "thread_publish", turnId: "turn_publish", prompt: "/publish" }, activeControls());
|
|
2047
|
+
expect(outcome).toMatchObject({ kind: "completed", cleanBoundary: undefined });
|
|
2048
|
+
});
|
|
1922
2049
|
it("keeps the running turn's session on record until the turn ends", async () => {
|
|
1923
2050
|
const recorded = [];
|
|
1924
2051
|
const inFlightTurns = memoryInFlightTurns();
|
|
@@ -2199,7 +2326,7 @@ describe("BridgeJobExecutor", () => {
|
|
|
2199
2326
|
expect(savedBoundaries).toEqual([]);
|
|
2200
2327
|
expect(clearCalls).toContainEqual({ threadId: "thread_result_error", reason: "result_error" });
|
|
2201
2328
|
});
|
|
2202
|
-
it("
|
|
2329
|
+
it("reports a provider's server error in product language, not its status line", async () => {
|
|
2203
2330
|
const clearCalls = [];
|
|
2204
2331
|
const executor = createExecutor({
|
|
2205
2332
|
clearCalls,
|
|
@@ -2225,10 +2352,86 @@ describe("BridgeJobExecutor", () => {
|
|
|
2225
2352
|
kind: "errored",
|
|
2226
2353
|
costUsd: 0.17,
|
|
2227
2354
|
model: "claude-test",
|
|
2228
|
-
errors: ["
|
|
2355
|
+
errors: [expect.stringContaining("on our side")],
|
|
2229
2356
|
});
|
|
2230
2357
|
expect(clearCalls).toContainEqual({ threadId: "thread_provider_result_error", reason: "result_error" });
|
|
2231
2358
|
});
|
|
2359
|
+
it("never shows the marketer a model provider's billing error, as a reply or as the turn's error", async () => {
|
|
2360
|
+
const providerError = "API Error: 402 Insufficient credits. Add more using https://openrouter.ai/settings/credits";
|
|
2361
|
+
const relay = recordingRelay();
|
|
2362
|
+
const executor = createExecutor({
|
|
2363
|
+
relay,
|
|
2364
|
+
queryResults: [
|
|
2365
|
+
scriptedMessages(apiErrorMessage("assistant_402", providerError, "unknown"), {
|
|
2366
|
+
type: "result",
|
|
2367
|
+
subtype: "success",
|
|
2368
|
+
is_error: true,
|
|
2369
|
+
result: providerError,
|
|
2370
|
+
api_error_status: 402,
|
|
2371
|
+
total_cost_usd: 0,
|
|
2372
|
+
session_id: "session_402",
|
|
2373
|
+
}),
|
|
2374
|
+
],
|
|
2375
|
+
});
|
|
2376
|
+
const outcome = await executor.execute({ threadId: "thread_402", turnId: "turn_402", prompt: "Build the creatives", history: history([]) }, activeControls());
|
|
2377
|
+
const shown = JSON.stringify([relay.assistantEvents, outcome.kind === "errored" ? outcome.errors : []]);
|
|
2378
|
+
expect(outcome.kind).toBe("errored");
|
|
2379
|
+
expect(shown).not.toMatch(/API Error|openrouter|402/i);
|
|
2380
|
+
});
|
|
2381
|
+
it("never shows the marketer the provider refusing Baker's own model key", async () => {
|
|
2382
|
+
const executor = createExecutor({
|
|
2383
|
+
queryResults: [
|
|
2384
|
+
scriptedMessages({
|
|
2385
|
+
type: "result",
|
|
2386
|
+
subtype: "success",
|
|
2387
|
+
is_error: true,
|
|
2388
|
+
result: 'API Error: 401 {"error":{"message":"User not found.","code":401}}',
|
|
2389
|
+
api_error_status: 401,
|
|
2390
|
+
total_cost_usd: 0,
|
|
2391
|
+
session_id: "session_revoked_key",
|
|
2392
|
+
}),
|
|
2393
|
+
],
|
|
2394
|
+
});
|
|
2395
|
+
const outcome = await executor.execute({ threadId: "thread_revoked_key", turnId: "turn_revoked_key", prompt: "Write headlines", history: history([]) }, activeControls());
|
|
2396
|
+
expect(outcome.kind === "errored" ? outcome.errors : []).toEqual([expect.stringContaining("on our side")]);
|
|
2397
|
+
});
|
|
2398
|
+
it("tells the marketer how to fix an image the provider could not read, not to send the same message again", async () => {
|
|
2399
|
+
const imageError = 'API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"Could not process image"}}';
|
|
2400
|
+
const executor = createExecutor({
|
|
2401
|
+
queryResults: [
|
|
2402
|
+
scriptedMessages({
|
|
2403
|
+
type: "result",
|
|
2404
|
+
subtype: "success",
|
|
2405
|
+
is_error: true,
|
|
2406
|
+
result: imageError,
|
|
2407
|
+
api_error_status: 400,
|
|
2408
|
+
total_cost_usd: 0,
|
|
2409
|
+
session_id: "session_bad_image",
|
|
2410
|
+
}),
|
|
2411
|
+
],
|
|
2412
|
+
});
|
|
2413
|
+
const outcome = await executor.execute({ threadId: "thread_bad_image", turnId: "turn_bad_image", prompt: "Use this photo", history: history([]) }, activeControls());
|
|
2414
|
+
expect(outcome.kind === "errored" ? outcome.errors : []).toEqual([
|
|
2415
|
+
expect.stringContaining("extension lies about its format"),
|
|
2416
|
+
]);
|
|
2417
|
+
});
|
|
2418
|
+
it("keeps a server error the turn recovered from out of the chat, and in the turn events", async () => {
|
|
2419
|
+
const providerError = "API Error: Server error mid-response. The response above may be incomplete.";
|
|
2420
|
+
const relay = recordingRelay();
|
|
2421
|
+
const turnEvents = [];
|
|
2422
|
+
const executor = createExecutor({
|
|
2423
|
+
relay,
|
|
2424
|
+
turnEvents,
|
|
2425
|
+
queryResults: [
|
|
2426
|
+
scriptedMessages(apiErrorMessage("assistant_server_error", providerError, "server_error"), assistantMessage("assistant_recovered", "Your three headlines are ready.", "session_recovered"), resultMessage(0.3, "session_recovered")),
|
|
2427
|
+
],
|
|
2428
|
+
});
|
|
2429
|
+
const outcome = await executor.execute({ threadId: "thread_recovered", turnId: "turn_recovered", prompt: "Write headlines", history: history([]) }, activeControls());
|
|
2430
|
+
expect(outcome.kind).toBe("completed");
|
|
2431
|
+
expect(JSON.stringify(relay.assistantEvents)).not.toContain("API Error");
|
|
2432
|
+
expect(JSON.stringify(relay.assistantEvents)).toContain("Your three headlines are ready.");
|
|
2433
|
+
expect(turnEvents).toContainEqual(expect.objectContaining({ event: "api_error", detail: { error: "server_error", text: providerError } }));
|
|
2434
|
+
});
|
|
2232
2435
|
it("closes SDK queries on error and inactive exits but not on natural clean completion", async () => {
|
|
2233
2436
|
const naturalCloseCalls = [];
|
|
2234
2437
|
const naturalExecutor = createExecutor({
|
|
@@ -4495,6 +4698,34 @@ describe("BridgeJobExecutor model refusals", () => {
|
|
|
4495
4698
|
errors: ["I couldn't help with that request. Try rephrasing what you need."],
|
|
4496
4699
|
});
|
|
4497
4700
|
});
|
|
4701
|
+
// Tool results come back in user events, which are not stored as messages, so
|
|
4702
|
+
// their outcome is relayed on its own: a Session rebuilt from history needs it.
|
|
4703
|
+
it("relays how each tool call came out, bounded", async () => {
|
|
4704
|
+
const relay = recordingRelay();
|
|
4705
|
+
const toolResult = {
|
|
4706
|
+
type: "user",
|
|
4707
|
+
message: {
|
|
4708
|
+
role: "user",
|
|
4709
|
+
content: [
|
|
4710
|
+
{ type: "tool_result", tool_use_id: "toolu_1", is_error: true, content: "Meta refused the rule" },
|
|
4711
|
+
{ type: "tool_result", tool_use_id: "toolu_2", content: [{ type: "text", text: "x".repeat(5000) }] },
|
|
4712
|
+
],
|
|
4713
|
+
},
|
|
4714
|
+
parent_tool_use_id: null,
|
|
4715
|
+
session_id: "session_tools",
|
|
4716
|
+
};
|
|
4717
|
+
const executor = createExecutor({
|
|
4718
|
+
relay,
|
|
4719
|
+
queryResults: [scriptedMessages(toolResult, resultMessage(0.01, "session_tools"))],
|
|
4720
|
+
});
|
|
4721
|
+
await executor.execute({ threadId: "thread_tools", turnId: "turn_tools", prompt: "Hi", history: history([]) }, activeControls());
|
|
4722
|
+
expect(relay.toolOutcomeBatches).toEqual([
|
|
4723
|
+
[
|
|
4724
|
+
{ toolUseId: "toolu_1", isError: true, text: "Meta refused the rule" },
|
|
4725
|
+
{ toolUseId: "toolu_2", isError: false, text: "x".repeat(2000) },
|
|
4726
|
+
],
|
|
4727
|
+
]);
|
|
4728
|
+
});
|
|
4498
4729
|
// A fallback model retried the refused request. What the refused leg already
|
|
4499
4730
|
// posted is superseded, so it leaves the chat instead of sitting above the
|
|
4500
4731
|
// answer that replaced it.
|