@koda-sl/baker-bridge 0.109.0-dev.fa751dbf1 → 0.109.0-dev.fbf7b641a
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 +4 -14
- package/dist/hono/agent.d.ts.map +1 -1
- package/dist/hono/agent.js +4 -9
- package/dist/hono/agent.js.map +1 -1
- package/dist/hono/attachments.d.ts +6 -1
- package/dist/hono/attachments.d.ts.map +1 -1
- package/dist/hono/attachments.js +9 -4
- package/dist/hono/attachments.js.map +1 -1
- package/dist/hono/attachments.test.js +5 -0
- package/dist/hono/attachments.test.js.map +1 -1
- package/dist/hono/baker-ui-server.test.d.ts +2 -0
- package/dist/hono/baker-ui-server.test.d.ts.map +1 -0
- package/dist/hono/baker-ui-server.test.js +46 -0
- package/dist/hono/baker-ui-server.test.js.map +1 -0
- package/dist/hono/bridge-job.d.ts +10 -11
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +38 -45
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +94 -164
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/session-continuity.d.ts +0 -1
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.js +2 -5
- package/dist/hono/session-continuity.js.map +1 -1
- package/dist/hono/tag-input.d.ts.map +1 -1
- package/dist/hono/tag-input.js +4 -0
- package/dist/hono/tag-input.js.map +1 -1
- package/dist/hono/types.d.ts +7 -6
- package/dist/hono/types.d.ts.map +1 -1
- package/dist/review/digest.d.ts +7 -1
- package/dist/review/digest.d.ts.map +1 -1
- package/dist/review/digest.js +80 -16
- package/dist/review/digest.js.map +1 -1
- package/dist/review/digest.test.js +83 -0
- package/dist/review/digest.test.js.map +1 -1
- package/package.json +2 -1
- package/dist/hono/main-sync.d.ts +0 -46
- package/dist/hono/main-sync.d.ts.map +0 -1
- package/dist/hono/main-sync.js +0 -409
- package/dist/hono/main-sync.js.map +0 -1
- package/dist/hono/main-sync.test.d.ts +0 -2
- package/dist/hono/main-sync.test.d.ts.map +0 -1
- package/dist/hono/main-sync.test.js +0 -276
- package/dist/hono/main-sync.test.js.map +0 -1
|
@@ -229,7 +229,7 @@ function createExecutor(options) {
|
|
|
229
229
|
now: options.now,
|
|
230
230
|
readUsageBaseline: async (sessionId) => options.usageBaselines?.[sessionId],
|
|
231
231
|
stalledCliExitMs: 0,
|
|
232
|
-
|
|
232
|
+
prepareAttachments: options.prepareAttachments,
|
|
233
233
|
});
|
|
234
234
|
}
|
|
235
235
|
function resultMessage(costUsd, sessionId, modelUsage) {
|
|
@@ -1112,6 +1112,18 @@ describe("BridgeJobExecutor", () => {
|
|
|
1112
1112
|
await executor.execute({ threadId: "thread_bedrock", turnId: "turn_bedrock", prompt: "Build a landing", history: history([]) }, activeControls());
|
|
1113
1113
|
expect(queryInputs[0]?.options.env).toMatchObject({ CLAUDE_CODE_MODEL_CAPABILITIES: "-per_turn_effort" });
|
|
1114
1114
|
});
|
|
1115
|
+
// Behind the OpenRouter base URL the CLI loads every MCP tool schema up front:
|
|
1116
|
+
// a connected Make toolkit put ~180 schemas (~115k tokens) into every request of
|
|
1117
|
+
// a Session that called two of them. Tool search defers them to their names.
|
|
1118
|
+
it("defers MCP tool schemas behind tool search instead of loading them into every request", async () => {
|
|
1119
|
+
const queryInputs = [];
|
|
1120
|
+
const executor = createExecutor({
|
|
1121
|
+
queryInputs,
|
|
1122
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_tool_search"))],
|
|
1123
|
+
});
|
|
1124
|
+
await executor.execute({ threadId: "thread_tool_search", turnId: "turn_tool_search", prompt: "Check Make", history: history([]) }, activeControls());
|
|
1125
|
+
expect(queryInputs[0]?.options.env).toMatchObject({ ENABLE_TOOL_SEARCH: "true" });
|
|
1126
|
+
});
|
|
1115
1127
|
it("silences the CLI task reminders and ANSI colour in the agent subprocess", async () => {
|
|
1116
1128
|
const queryInputs = [];
|
|
1117
1129
|
const executor = createExecutor({
|
|
@@ -1473,198 +1485,87 @@ describe("BridgeJobExecutor", () => {
|
|
|
1473
1485
|
expect(queryInputs[0]?.prompt).toBe("/publish");
|
|
1474
1486
|
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1475
1487
|
});
|
|
1476
|
-
it("
|
|
1488
|
+
it("keeps the viewing-reference context beside the message when a stale session falls back to rehydration", async () => {
|
|
1477
1489
|
const queryInputs = [];
|
|
1478
|
-
const syncedThreads = [];
|
|
1479
|
-
const syncedPublishes = [];
|
|
1480
1490
|
const executor = createExecutor({
|
|
1491
|
+
loadedBoundary: cleanBoundary("session_stale_viewing"),
|
|
1481
1492
|
queryInputs,
|
|
1482
|
-
queryResults: [
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
syncedPublishes.push(...published.map((entry) => entry.sha));
|
|
1487
|
-
return Promise.resolve({
|
|
1488
|
-
recovered: "<system-reminder>\nBaker: this workspace now includes work published since your last look.\n</system-reminder>",
|
|
1489
|
-
});
|
|
1490
|
-
},
|
|
1491
|
-
refuseWrite: () => null,
|
|
1492
|
-
afterTurn: () => Promise.resolve(),
|
|
1493
|
-
},
|
|
1493
|
+
queryResults: [
|
|
1494
|
+
scriptedError(new Error("Cannot resume: session not found")),
|
|
1495
|
+
scriptedMessages(resultMessage(0, "session_next_viewing")),
|
|
1496
|
+
],
|
|
1494
1497
|
});
|
|
1495
1498
|
await executor.execute({
|
|
1496
|
-
threadId: "
|
|
1497
|
-
turnId: "
|
|
1498
|
-
prompt: "
|
|
1499
|
-
history: history([]),
|
|
1499
|
+
threadId: "thread_stale_viewing",
|
|
1500
|
+
turnId: "turn_stale_viewing",
|
|
1501
|
+
prompt: "quita el pricing",
|
|
1502
|
+
history: history([{ role: "user", text: "Persisted fact", createdAt: 1 }]),
|
|
1500
1503
|
currentReference: { type: "landing", slug: "homepage", title: "Homepage" },
|
|
1501
|
-
publishedElsewhere: [
|
|
1502
|
-
{
|
|
1503
|
-
sha: "abc123",
|
|
1504
|
-
chatId: "sibling",
|
|
1505
|
-
title: "New hero",
|
|
1506
|
-
publishedAt: 1,
|
|
1507
|
-
paths: ["src/pages/hero/index.astro"],
|
|
1508
|
-
},
|
|
1509
|
-
],
|
|
1510
1504
|
}, activeControls());
|
|
1511
|
-
expect(
|
|
1512
|
-
expect(
|
|
1513
|
-
expect(queryInputs[
|
|
1514
|
-
expect(queryInputs[0]?.reminder).toContain("Baker: this workspace now includes work published");
|
|
1515
|
-
expect(queryInputs[0]?.reminder).toContain('landing "Homepage" (slug: homepage)');
|
|
1516
|
-
});
|
|
1517
|
-
it("realigns the workspace once the turn ends, after whatever the agent did to it", async () => {
|
|
1518
|
-
const queryInputs = [];
|
|
1519
|
-
const realignedAfter = [];
|
|
1520
|
-
const executor = createExecutor({
|
|
1521
|
-
queryInputs,
|
|
1522
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_after_turn"))],
|
|
1523
|
-
mainSync: {
|
|
1524
|
-
beforeTurn: () => Promise.resolve({}),
|
|
1525
|
-
refuseWrite: () => null,
|
|
1526
|
-
afterTurn: () => {
|
|
1527
|
-
realignedAfter.push(queryInputs.length);
|
|
1528
|
-
return Promise.resolve();
|
|
1529
|
-
},
|
|
1530
|
-
},
|
|
1531
|
-
});
|
|
1532
|
-
await executor.execute({ threadId: "thread_after_turn", turnId: "turn_after_turn", prompt: "edit the hero", history: history([]) }, activeControls());
|
|
1533
|
-
expect(realignedAfter).toEqual([1]);
|
|
1534
|
-
});
|
|
1535
|
-
it("does not move the workspace under a publish command", async () => {
|
|
1536
|
-
const queryInputs = [];
|
|
1537
|
-
const syncedThreads = [];
|
|
1538
|
-
const executor = createExecutor({
|
|
1539
|
-
queryInputs,
|
|
1540
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_publish_main"))],
|
|
1541
|
-
mainSync: {
|
|
1542
|
-
beforeTurn: (threadId) => {
|
|
1543
|
-
syncedThreads.push(threadId);
|
|
1544
|
-
return Promise.resolve({ resumed: "<system-reminder>\nBaker: moved\n</system-reminder>" });
|
|
1545
|
-
},
|
|
1546
|
-
refuseWrite: () => null,
|
|
1547
|
-
afterTurn: () => Promise.resolve(),
|
|
1548
|
-
},
|
|
1549
|
-
});
|
|
1550
|
-
await executor.execute({ threadId: "thread_publish_main", turnId: "turn_publish_main", prompt: "/publish", history: history([]) }, activeControls());
|
|
1551
|
-
expect(syncedThreads).toEqual([]);
|
|
1552
|
-
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1553
|
-
});
|
|
1554
|
-
it("tells a resumed session only what is new, since it remembers every earlier note", async () => {
|
|
1555
|
-
const queryInputs = [];
|
|
1556
|
-
const executor = createExecutor({
|
|
1557
|
-
loadedBoundary: cleanBoundary("session_resumed_main"),
|
|
1558
|
-
queryInputs,
|
|
1559
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_resumed_main_next"))],
|
|
1560
|
-
mainSync: {
|
|
1561
|
-
beforeTurn: () => Promise.resolve({ recovered: "<system-reminder>\nBaker: standing\n</system-reminder>" }),
|
|
1562
|
-
refuseWrite: () => null,
|
|
1563
|
-
afterTurn: () => Promise.resolve(),
|
|
1564
|
-
},
|
|
1565
|
-
});
|
|
1566
|
-
await executor.execute({ threadId: "thread_resumed_main", turnId: "turn_resumed_main", prompt: "hola", history: history([]) }, activeControls());
|
|
1567
|
-
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1505
|
+
expect(queryInputs[1]?.prompt).toContain("Current user request\nquita el pricing");
|
|
1506
|
+
expect(queryInputs[1]?.prompt).not.toContain("preview pane");
|
|
1507
|
+
expect(queryInputs[1]?.reminder).toContain('landing "Homepage" (slug: homepage)');
|
|
1568
1508
|
});
|
|
1569
|
-
|
|
1509
|
+
// A late answer to a blocking prompt continues a request whose files the
|
|
1510
|
+
// Session received with it. A rebuilt Session only has that request's history,
|
|
1511
|
+
// which names the files but cannot open them, so they are handed over again.
|
|
1512
|
+
it("hands over the continued request's files when the Session is rebuilt", async () => {
|
|
1570
1513
|
const queryInputs = [];
|
|
1571
1514
|
const executor = createExecutor({
|
|
1572
|
-
loadedBoundary: cleanBoundary("session_stale_main"),
|
|
1573
1515
|
queryInputs,
|
|
1574
|
-
|
|
1575
|
-
scriptedError(new Error("Cannot resume: session not found")),
|
|
1576
|
-
scriptedMessages(resultMessage(0, "session_stale_main_next")),
|
|
1577
|
-
],
|
|
1578
|
-
mainSync: {
|
|
1579
|
-
beforeTurn: () => Promise.resolve({ recovered: "<system-reminder>\nBaker: standing\n</system-reminder>" }),
|
|
1580
|
-
refuseWrite: () => null,
|
|
1581
|
-
afterTurn: () => Promise.resolve(),
|
|
1582
|
-
},
|
|
1516
|
+
prepareAttachments: async (attachments, attachedTo) => `\n\nFILES for ${attachedTo}: ${attachments.map((attachment) => attachment.filename).join(", ")}`,
|
|
1583
1517
|
});
|
|
1584
1518
|
await executor.execute({
|
|
1585
|
-
threadId: "
|
|
1586
|
-
turnId: "
|
|
1587
|
-
prompt: "
|
|
1588
|
-
|
|
1519
|
+
threadId: "thread_continued_files",
|
|
1520
|
+
turnId: "turn_continued_files",
|
|
1521
|
+
prompt: "They have now replied: HubSpot",
|
|
1522
|
+
continuedAttachments: [
|
|
1523
|
+
{ url: "https://files.example/logo.png", contentType: "image/png", filename: "logo.png" },
|
|
1524
|
+
],
|
|
1525
|
+
history: history([{ role: "user", text: "Build a page with my logo", createdAt: 1 }]),
|
|
1589
1526
|
}, activeControls());
|
|
1590
|
-
expect(queryInputs[0]?.
|
|
1591
|
-
expect(queryInputs[1]?.reminder).toContain("Baker: standing");
|
|
1527
|
+
expect(queryInputs[0]?.prompt).toContain("FILES for continued_request: logo.png");
|
|
1592
1528
|
});
|
|
1593
|
-
it("
|
|
1594
|
-
const workspace = await mkdtemp(join(tmpdir(), "baker-job-skill-main-"));
|
|
1595
|
-
await mkdir(join(workspace, ".claude", "skills", "landing"), { recursive: true });
|
|
1596
|
-
projectRootMock.value = workspace;
|
|
1529
|
+
it("does not hand the continued request's files over again to a resumed Session", async () => {
|
|
1597
1530
|
const queryInputs = [];
|
|
1598
|
-
const syncs = [];
|
|
1599
1531
|
const executor = createExecutor({
|
|
1532
|
+
loadedBoundary: cleanBoundary("session_holds_files"),
|
|
1600
1533
|
queryInputs,
|
|
1601
|
-
|
|
1602
|
-
mainSync: {
|
|
1603
|
-
beforeTurn: (threadId, _published, turn) => {
|
|
1604
|
-
syncs.push({ threadId, ...turn });
|
|
1605
|
-
return Promise.resolve({});
|
|
1606
|
-
},
|
|
1607
|
-
refuseWrite: () => null,
|
|
1608
|
-
afterTurn: () => Promise.resolve(),
|
|
1609
|
-
},
|
|
1610
|
-
});
|
|
1611
|
-
await executor.execute({ threadId: "thread_skill_main", turnId: "turn_skill_main", prompt: "/landing pricing", history: history([]) }, activeControls());
|
|
1612
|
-
expect(syncs).toEqual([{ threadId: "thread_skill_main", withNote: false }]);
|
|
1613
|
-
expect(queryInputs[0]?.prompt).toBe("/landing pricing");
|
|
1614
|
-
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1615
|
-
});
|
|
1616
|
-
it("refuses a write that would create a page another chat already published, with the reason for the agent", async () => {
|
|
1617
|
-
let guardResult;
|
|
1618
|
-
const executor = new BridgeJobExecutor({
|
|
1619
|
-
sdk: {
|
|
1620
|
-
query: (input) => ({
|
|
1621
|
-
async *[Symbol.asyncIterator]() {
|
|
1622
|
-
guardResult = await input.options.canUseTool?.("Write", { file_path: "/workspace/src/pages/sales-v2/index.astro", content: "v2" }, {
|
|
1623
|
-
toolUseID: "tool_write",
|
|
1624
|
-
requestId: "req_write",
|
|
1625
|
-
signal: input.options.abortController?.signal ?? new AbortController().signal,
|
|
1626
|
-
});
|
|
1627
|
-
yield resultMessage(0, "session_page_clash");
|
|
1628
|
-
},
|
|
1629
|
-
}),
|
|
1630
|
-
},
|
|
1631
|
-
model: "claude-test",
|
|
1632
|
-
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
1633
|
-
relay: recordingRelay(),
|
|
1634
|
-
mainSync: {
|
|
1635
|
-
beforeTurn: () => Promise.resolve({}),
|
|
1636
|
-
refuseWrite: (threadId, filePath) => threadId === "thread_page_clash" && filePath.endsWith("src/pages/sales-v2/index.astro")
|
|
1637
|
-
? "`src/pages/sales-v2` was already published by another chat"
|
|
1638
|
-
: null,
|
|
1639
|
-
afterTurn: () => Promise.resolve(),
|
|
1640
|
-
},
|
|
1641
|
-
});
|
|
1642
|
-
await executor.execute({ threadId: "thread_page_clash", turnId: "turn_page_clash", prompt: "build sales v2" }, activeControls());
|
|
1643
|
-
expect(guardResult).toEqual({
|
|
1644
|
-
behavior: "deny",
|
|
1645
|
-
message: "`src/pages/sales-v2` was already published by another chat",
|
|
1534
|
+
prepareAttachments: async (attachments) => `\n\nFILES: ${attachments.map((attachment) => attachment.filename).join(", ")}`,
|
|
1646
1535
|
});
|
|
1536
|
+
await executor.execute({
|
|
1537
|
+
threadId: "thread_resumed_files",
|
|
1538
|
+
turnId: "turn_resumed_files",
|
|
1539
|
+
prompt: "They have now replied: HubSpot",
|
|
1540
|
+
continuedAttachments: [
|
|
1541
|
+
{ url: "https://files.example/logo.png", contentType: "image/png", filename: "logo.png" },
|
|
1542
|
+
],
|
|
1543
|
+
history: history([{ role: "user", text: "Build a page with my logo", createdAt: 1 }]),
|
|
1544
|
+
}, activeControls());
|
|
1545
|
+
expect(queryInputs[0]?.prompt).toBe("They have now replied: HubSpot");
|
|
1647
1546
|
});
|
|
1648
|
-
it("
|
|
1547
|
+
it("hands the continued request's files over when a stale Session falls back to history", async () => {
|
|
1649
1548
|
const queryInputs = [];
|
|
1650
1549
|
const executor = createExecutor({
|
|
1651
|
-
loadedBoundary: cleanBoundary("
|
|
1550
|
+
loadedBoundary: cleanBoundary("session_stale_files"),
|
|
1652
1551
|
queryInputs,
|
|
1653
1552
|
queryResults: [
|
|
1654
1553
|
scriptedError(new Error("Cannot resume: session not found")),
|
|
1655
|
-
scriptedMessages(resultMessage(0, "
|
|
1554
|
+
scriptedMessages(resultMessage(0, "session_rebuilt_files")),
|
|
1656
1555
|
],
|
|
1556
|
+
prepareAttachments: async (attachments) => `\n\nFILES: ${attachments.map((attachment) => attachment.filename).join(", ")}`,
|
|
1657
1557
|
});
|
|
1658
1558
|
await executor.execute({
|
|
1659
|
-
threadId: "
|
|
1660
|
-
turnId: "
|
|
1661
|
-
prompt: "
|
|
1662
|
-
|
|
1663
|
-
|
|
1559
|
+
threadId: "thread_stale_files",
|
|
1560
|
+
turnId: "turn_stale_files",
|
|
1561
|
+
prompt: "They have now replied: HubSpot",
|
|
1562
|
+
continuedAttachments: [
|
|
1563
|
+
{ url: "https://files.example/logo.png", contentType: "image/png", filename: "logo.png" },
|
|
1564
|
+
],
|
|
1565
|
+
history: history([{ role: "user", text: "Build a page with my logo", createdAt: 1 }]),
|
|
1664
1566
|
}, activeControls());
|
|
1665
|
-
expect(queryInputs[
|
|
1666
|
-
expect(queryInputs[1]?.prompt).
|
|
1667
|
-
expect(queryInputs[1]?.reminder).toContain('landing "Homepage" (slug: homepage)');
|
|
1567
|
+
expect(queryInputs[0]?.prompt).not.toContain("FILES");
|
|
1568
|
+
expect(queryInputs[1]?.prompt).toContain("Current user request\nThey have now replied: HubSpot\n\nFILES: logo.png");
|
|
1668
1569
|
});
|
|
1669
1570
|
it("clears an invalid clean boundary and retries once with rehydrated history", async () => {
|
|
1670
1571
|
const relay = recordingRelay();
|
|
@@ -3347,6 +3248,35 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
|
|
|
3347
3248
|
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
|
|
3348
3249
|
expect(endInputAt.count).toBe(3);
|
|
3349
3250
|
});
|
|
3251
|
+
// The production shape: the model refused, the CLI answered with an error
|
|
3252
|
+
// result and then waited on stdin like after any other result. The error was
|
|
3253
|
+
// no longer thrown, and only a clean result ended input, so the turn sat idle
|
|
3254
|
+
// until the 30-minute session watchdog reported a lost connection.
|
|
3255
|
+
it("ends input after the error result of a refused turn", async () => {
|
|
3256
|
+
const endInputAt = { count: -1 };
|
|
3257
|
+
const refusal = {
|
|
3258
|
+
type: "system",
|
|
3259
|
+
subtype: "model_refusal_no_fallback",
|
|
3260
|
+
original_model: "claude-opus-5-5",
|
|
3261
|
+
api_refusal_category: "reasoning_extraction",
|
|
3262
|
+
session_id: "session_refused",
|
|
3263
|
+
};
|
|
3264
|
+
const erroredResult = {
|
|
3265
|
+
type: "result",
|
|
3266
|
+
subtype: "error_during_execution",
|
|
3267
|
+
is_error: true,
|
|
3268
|
+
errors: ["API Error: Claude Code is unable to respond to this request."],
|
|
3269
|
+
total_cost_usd: 0.01,
|
|
3270
|
+
session_id: "session_refused",
|
|
3271
|
+
};
|
|
3272
|
+
const executor = createExecutor({ queryResults: [cliLikeQuery(endInputAt, refusal, erroredResult)] });
|
|
3273
|
+
const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
|
|
3274
|
+
expect(outcome).toMatchObject({
|
|
3275
|
+
kind: "errored",
|
|
3276
|
+
errors: ["I couldn't help with that request. Try rephrasing what you need."],
|
|
3277
|
+
});
|
|
3278
|
+
expect(endInputAt.count).toBe(2);
|
|
3279
|
+
});
|
|
3350
3280
|
it("does not hold the turn open for ambient skip_transcript tasks", async () => {
|
|
3351
3281
|
const endInputAt = { count: -1 };
|
|
3352
3282
|
const executor = createExecutor({
|