@koda-sl/baker-bridge 0.108.0-dev.7e326a90a → 0.108.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 -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/bridge-job.d.ts +10 -11
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +30 -39
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +82 -140
- 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/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 -41
- package/dist/hono/main-sync.d.ts.map +0 -1
- package/dist/hono/main-sync.js +0 -390
- 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 -266
- 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) {
|
|
@@ -1473,174 +1473,87 @@ describe("BridgeJobExecutor", () => {
|
|
|
1473
1473
|
expect(queryInputs[0]?.prompt).toBe("/publish");
|
|
1474
1474
|
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1475
1475
|
});
|
|
1476
|
-
it("
|
|
1476
|
+
it("keeps the viewing-reference context beside the message when a stale session falls back to rehydration", async () => {
|
|
1477
1477
|
const queryInputs = [];
|
|
1478
|
-
const syncedThreads = [];
|
|
1479
|
-
const syncedPublishes = [];
|
|
1480
1478
|
const executor = createExecutor({
|
|
1479
|
+
loadedBoundary: cleanBoundary("session_stale_viewing"),
|
|
1481
1480
|
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
|
-
},
|
|
1481
|
+
queryResults: [
|
|
1482
|
+
scriptedError(new Error("Cannot resume: session not found")),
|
|
1483
|
+
scriptedMessages(resultMessage(0, "session_next_viewing")),
|
|
1484
|
+
],
|
|
1493
1485
|
});
|
|
1494
1486
|
await executor.execute({
|
|
1495
|
-
threadId: "
|
|
1496
|
-
turnId: "
|
|
1497
|
-
prompt: "
|
|
1498
|
-
history: history([]),
|
|
1487
|
+
threadId: "thread_stale_viewing",
|
|
1488
|
+
turnId: "turn_stale_viewing",
|
|
1489
|
+
prompt: "quita el pricing",
|
|
1490
|
+
history: history([{ role: "user", text: "Persisted fact", createdAt: 1 }]),
|
|
1499
1491
|
currentReference: { type: "landing", slug: "homepage", title: "Homepage" },
|
|
1500
|
-
publishedElsewhere: [
|
|
1501
|
-
{
|
|
1502
|
-
sha: "abc123",
|
|
1503
|
-
chatId: "sibling",
|
|
1504
|
-
title: "New hero",
|
|
1505
|
-
publishedAt: 1,
|
|
1506
|
-
paths: ["src/pages/hero/index.astro"],
|
|
1507
|
-
},
|
|
1508
|
-
],
|
|
1509
1492
|
}, activeControls());
|
|
1510
|
-
expect(
|
|
1511
|
-
expect(
|
|
1512
|
-
expect(queryInputs[
|
|
1513
|
-
expect(queryInputs[0]?.reminder).toContain("Baker: this workspace now includes work published");
|
|
1514
|
-
expect(queryInputs[0]?.reminder).toContain('landing "Homepage" (slug: homepage)');
|
|
1515
|
-
});
|
|
1516
|
-
it("does not move the workspace under a publish command", async () => {
|
|
1517
|
-
const queryInputs = [];
|
|
1518
|
-
const syncedThreads = [];
|
|
1519
|
-
const executor = createExecutor({
|
|
1520
|
-
queryInputs,
|
|
1521
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_publish_main"))],
|
|
1522
|
-
mainSync: {
|
|
1523
|
-
beforeTurn: (threadId) => {
|
|
1524
|
-
syncedThreads.push(threadId);
|
|
1525
|
-
return Promise.resolve({ resumed: "<system-reminder>\nBaker: moved\n</system-reminder>" });
|
|
1526
|
-
},
|
|
1527
|
-
refuseWrite: () => null,
|
|
1528
|
-
},
|
|
1529
|
-
});
|
|
1530
|
-
await executor.execute({ threadId: "thread_publish_main", turnId: "turn_publish_main", prompt: "/publish", history: history([]) }, activeControls());
|
|
1531
|
-
expect(syncedThreads).toEqual([]);
|
|
1532
|
-
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1493
|
+
expect(queryInputs[1]?.prompt).toContain("Current user request\nquita el pricing");
|
|
1494
|
+
expect(queryInputs[1]?.prompt).not.toContain("preview pane");
|
|
1495
|
+
expect(queryInputs[1]?.reminder).toContain('landing "Homepage" (slug: homepage)');
|
|
1533
1496
|
});
|
|
1534
|
-
|
|
1497
|
+
// A late answer to a blocking prompt continues a request whose files the
|
|
1498
|
+
// Session received with it. A rebuilt Session only has that request's history,
|
|
1499
|
+
// which names the files but cannot open them, so they are handed over again.
|
|
1500
|
+
it("hands over the continued request's files when the Session is rebuilt", async () => {
|
|
1535
1501
|
const queryInputs = [];
|
|
1536
1502
|
const executor = createExecutor({
|
|
1537
|
-
loadedBoundary: cleanBoundary("session_resumed_main"),
|
|
1538
1503
|
queryInputs,
|
|
1539
|
-
|
|
1540
|
-
mainSync: {
|
|
1541
|
-
beforeTurn: () => Promise.resolve({ recovered: "<system-reminder>\nBaker: standing\n</system-reminder>" }),
|
|
1542
|
-
refuseWrite: () => null,
|
|
1543
|
-
},
|
|
1544
|
-
});
|
|
1545
|
-
await executor.execute({ threadId: "thread_resumed_main", turnId: "turn_resumed_main", prompt: "hola", history: history([]) }, activeControls());
|
|
1546
|
-
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1547
|
-
});
|
|
1548
|
-
it("repeats the standing workspace notice when a stale session falls back to rehydration", async () => {
|
|
1549
|
-
const queryInputs = [];
|
|
1550
|
-
const executor = createExecutor({
|
|
1551
|
-
loadedBoundary: cleanBoundary("session_stale_main"),
|
|
1552
|
-
queryInputs,
|
|
1553
|
-
queryResults: [
|
|
1554
|
-
scriptedError(new Error("Cannot resume: session not found")),
|
|
1555
|
-
scriptedMessages(resultMessage(0, "session_stale_main_next")),
|
|
1556
|
-
],
|
|
1557
|
-
mainSync: {
|
|
1558
|
-
beforeTurn: () => Promise.resolve({ recovered: "<system-reminder>\nBaker: standing\n</system-reminder>" }),
|
|
1559
|
-
refuseWrite: () => null,
|
|
1560
|
-
},
|
|
1504
|
+
prepareAttachments: async (attachments, attachedTo) => `\n\nFILES for ${attachedTo}: ${attachments.map((attachment) => attachment.filename).join(", ")}`,
|
|
1561
1505
|
});
|
|
1562
1506
|
await executor.execute({
|
|
1563
|
-
threadId: "
|
|
1564
|
-
turnId: "
|
|
1565
|
-
prompt: "
|
|
1566
|
-
|
|
1507
|
+
threadId: "thread_continued_files",
|
|
1508
|
+
turnId: "turn_continued_files",
|
|
1509
|
+
prompt: "They have now replied: HubSpot",
|
|
1510
|
+
continuedAttachments: [
|
|
1511
|
+
{ url: "https://files.example/logo.png", contentType: "image/png", filename: "logo.png" },
|
|
1512
|
+
],
|
|
1513
|
+
history: history([{ role: "user", text: "Build a page with my logo", createdAt: 1 }]),
|
|
1567
1514
|
}, activeControls());
|
|
1568
|
-
expect(queryInputs[0]?.
|
|
1569
|
-
expect(queryInputs[1]?.reminder).toContain("Baker: standing");
|
|
1515
|
+
expect(queryInputs[0]?.prompt).toContain("FILES for continued_request: logo.png");
|
|
1570
1516
|
});
|
|
1571
|
-
it("
|
|
1572
|
-
const workspace = await mkdtemp(join(tmpdir(), "baker-job-skill-main-"));
|
|
1573
|
-
await mkdir(join(workspace, ".claude", "skills", "landing"), { recursive: true });
|
|
1574
|
-
projectRootMock.value = workspace;
|
|
1517
|
+
it("does not hand the continued request's files over again to a resumed Session", async () => {
|
|
1575
1518
|
const queryInputs = [];
|
|
1576
|
-
const syncs = [];
|
|
1577
1519
|
const executor = createExecutor({
|
|
1520
|
+
loadedBoundary: cleanBoundary("session_holds_files"),
|
|
1578
1521
|
queryInputs,
|
|
1579
|
-
|
|
1580
|
-
mainSync: {
|
|
1581
|
-
beforeTurn: (threadId, _published, turn) => {
|
|
1582
|
-
syncs.push({ threadId, ...turn });
|
|
1583
|
-
return Promise.resolve({});
|
|
1584
|
-
},
|
|
1585
|
-
refuseWrite: () => null,
|
|
1586
|
-
},
|
|
1587
|
-
});
|
|
1588
|
-
await executor.execute({ threadId: "thread_skill_main", turnId: "turn_skill_main", prompt: "/landing pricing", history: history([]) }, activeControls());
|
|
1589
|
-
expect(syncs).toEqual([{ threadId: "thread_skill_main", withNote: false }]);
|
|
1590
|
-
expect(queryInputs[0]?.prompt).toBe("/landing pricing");
|
|
1591
|
-
expect(queryInputs[0]?.reminder).toBeUndefined();
|
|
1592
|
-
});
|
|
1593
|
-
it("refuses a write that would create a page another chat already published, with the reason for the agent", async () => {
|
|
1594
|
-
let guardResult;
|
|
1595
|
-
const executor = new BridgeJobExecutor({
|
|
1596
|
-
sdk: {
|
|
1597
|
-
query: (input) => ({
|
|
1598
|
-
async *[Symbol.asyncIterator]() {
|
|
1599
|
-
guardResult = await input.options.canUseTool?.("Write", { file_path: "/workspace/src/pages/sales-v2/index.astro", content: "v2" }, {
|
|
1600
|
-
toolUseID: "tool_write",
|
|
1601
|
-
requestId: "req_write",
|
|
1602
|
-
signal: input.options.abortController?.signal ?? new AbortController().signal,
|
|
1603
|
-
});
|
|
1604
|
-
yield resultMessage(0, "session_page_clash");
|
|
1605
|
-
},
|
|
1606
|
-
}),
|
|
1607
|
-
},
|
|
1608
|
-
model: "claude-test",
|
|
1609
|
-
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
1610
|
-
relay: recordingRelay(),
|
|
1611
|
-
mainSync: {
|
|
1612
|
-
beforeTurn: () => Promise.resolve({}),
|
|
1613
|
-
refuseWrite: (threadId, filePath) => threadId === "thread_page_clash" && filePath.endsWith("src/pages/sales-v2/index.astro")
|
|
1614
|
-
? "`src/pages/sales-v2` was already published by another chat"
|
|
1615
|
-
: null,
|
|
1616
|
-
},
|
|
1617
|
-
});
|
|
1618
|
-
await executor.execute({ threadId: "thread_page_clash", turnId: "turn_page_clash", prompt: "build sales v2" }, activeControls());
|
|
1619
|
-
expect(guardResult).toEqual({
|
|
1620
|
-
behavior: "deny",
|
|
1621
|
-
message: "`src/pages/sales-v2` was already published by another chat",
|
|
1522
|
+
prepareAttachments: async (attachments) => `\n\nFILES: ${attachments.map((attachment) => attachment.filename).join(", ")}`,
|
|
1622
1523
|
});
|
|
1524
|
+
await executor.execute({
|
|
1525
|
+
threadId: "thread_resumed_files",
|
|
1526
|
+
turnId: "turn_resumed_files",
|
|
1527
|
+
prompt: "They have now replied: HubSpot",
|
|
1528
|
+
continuedAttachments: [
|
|
1529
|
+
{ url: "https://files.example/logo.png", contentType: "image/png", filename: "logo.png" },
|
|
1530
|
+
],
|
|
1531
|
+
history: history([{ role: "user", text: "Build a page with my logo", createdAt: 1 }]),
|
|
1532
|
+
}, activeControls());
|
|
1533
|
+
expect(queryInputs[0]?.prompt).toBe("They have now replied: HubSpot");
|
|
1623
1534
|
});
|
|
1624
|
-
it("
|
|
1535
|
+
it("hands the continued request's files over when a stale Session falls back to history", async () => {
|
|
1625
1536
|
const queryInputs = [];
|
|
1626
1537
|
const executor = createExecutor({
|
|
1627
|
-
loadedBoundary: cleanBoundary("
|
|
1538
|
+
loadedBoundary: cleanBoundary("session_stale_files"),
|
|
1628
1539
|
queryInputs,
|
|
1629
1540
|
queryResults: [
|
|
1630
1541
|
scriptedError(new Error("Cannot resume: session not found")),
|
|
1631
|
-
scriptedMessages(resultMessage(0, "
|
|
1542
|
+
scriptedMessages(resultMessage(0, "session_rebuilt_files")),
|
|
1632
1543
|
],
|
|
1544
|
+
prepareAttachments: async (attachments) => `\n\nFILES: ${attachments.map((attachment) => attachment.filename).join(", ")}`,
|
|
1633
1545
|
});
|
|
1634
1546
|
await executor.execute({
|
|
1635
|
-
threadId: "
|
|
1636
|
-
turnId: "
|
|
1637
|
-
prompt: "
|
|
1638
|
-
|
|
1639
|
-
|
|
1547
|
+
threadId: "thread_stale_files",
|
|
1548
|
+
turnId: "turn_stale_files",
|
|
1549
|
+
prompt: "They have now replied: HubSpot",
|
|
1550
|
+
continuedAttachments: [
|
|
1551
|
+
{ url: "https://files.example/logo.png", contentType: "image/png", filename: "logo.png" },
|
|
1552
|
+
],
|
|
1553
|
+
history: history([{ role: "user", text: "Build a page with my logo", createdAt: 1 }]),
|
|
1640
1554
|
}, activeControls());
|
|
1641
|
-
expect(queryInputs[
|
|
1642
|
-
expect(queryInputs[1]?.prompt).
|
|
1643
|
-
expect(queryInputs[1]?.reminder).toContain('landing "Homepage" (slug: homepage)');
|
|
1555
|
+
expect(queryInputs[0]?.prompt).not.toContain("FILES");
|
|
1556
|
+
expect(queryInputs[1]?.prompt).toContain("Current user request\nThey have now replied: HubSpot\n\nFILES: logo.png");
|
|
1644
1557
|
});
|
|
1645
1558
|
it("clears an invalid clean boundary and retries once with rehydrated history", async () => {
|
|
1646
1559
|
const relay = recordingRelay();
|
|
@@ -3323,6 +3236,35 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
|
|
|
3323
3236
|
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
|
|
3324
3237
|
expect(endInputAt.count).toBe(3);
|
|
3325
3238
|
});
|
|
3239
|
+
// The production shape: the model refused, the CLI answered with an error
|
|
3240
|
+
// result and then waited on stdin like after any other result. The error was
|
|
3241
|
+
// no longer thrown, and only a clean result ended input, so the turn sat idle
|
|
3242
|
+
// until the 30-minute session watchdog reported a lost connection.
|
|
3243
|
+
it("ends input after the error result of a refused turn", async () => {
|
|
3244
|
+
const endInputAt = { count: -1 };
|
|
3245
|
+
const refusal = {
|
|
3246
|
+
type: "system",
|
|
3247
|
+
subtype: "model_refusal_no_fallback",
|
|
3248
|
+
original_model: "claude-opus-5-5",
|
|
3249
|
+
api_refusal_category: "reasoning_extraction",
|
|
3250
|
+
session_id: "session_refused",
|
|
3251
|
+
};
|
|
3252
|
+
const erroredResult = {
|
|
3253
|
+
type: "result",
|
|
3254
|
+
subtype: "error_during_execution",
|
|
3255
|
+
is_error: true,
|
|
3256
|
+
errors: ["API Error: Claude Code is unable to respond to this request."],
|
|
3257
|
+
total_cost_usd: 0.01,
|
|
3258
|
+
session_id: "session_refused",
|
|
3259
|
+
};
|
|
3260
|
+
const executor = createExecutor({ queryResults: [cliLikeQuery(endInputAt, refusal, erroredResult)] });
|
|
3261
|
+
const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
|
|
3262
|
+
expect(outcome).toMatchObject({
|
|
3263
|
+
kind: "errored",
|
|
3264
|
+
errors: ["I couldn't help with that request. Try rephrasing what you need."],
|
|
3265
|
+
});
|
|
3266
|
+
expect(endInputAt.count).toBe(2);
|
|
3267
|
+
});
|
|
3326
3268
|
it("does not hold the turn open for ambient skip_transcript tasks", async () => {
|
|
3327
3269
|
const endInputAt = { count: -1 };
|
|
3328
3270
|
const executor = createExecutor({
|