@koda-sl/baker-bridge 0.159.0 → 0.161.0-dev.61e0a90f3
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 +26 -8
- package/dist/hono/agent-session.d.ts +1 -0
- package/dist/hono/agent-session.d.ts.map +1 -1
- package/dist/hono/agent-session.js +1 -1
- package/dist/hono/agent-session.js.map +1 -1
- package/dist/hono/agent-session.test.js +1 -1
- package/dist/hono/agent-session.test.js.map +1 -1
- package/dist/hono/agent.d.ts +1 -1
- package/dist/hono/agent.d.ts.map +1 -1
- package/dist/hono/agent.js +2 -2
- package/dist/hono/agent.js.map +1 -1
- package/dist/hono/bridge-job.d.ts +6 -1
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +36 -6
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +144 -5
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/publish-handoff.d.ts +10 -0
- package/dist/hono/publish-handoff.d.ts.map +1 -1
- package/dist/hono/publish-handoff.js +17 -7
- package/dist/hono/publish-handoff.js.map +1 -1
- package/dist/hono/server.d.ts.map +1 -1
- package/dist/hono/server.js +3 -1
- package/dist/hono/server.js.map +1 -1
- package/dist/hono/session-continuity.d.ts +8 -0
- package/dist/hono/session-continuity.d.ts.map +1 -1
- package/dist/hono/session-continuity.integration.test.js +34 -7
- package/dist/hono/session-continuity.integration.test.js.map +1 -1
- package/dist/hono/session-continuity.js +9 -6
- package/dist/hono/session-continuity.js.map +1 -1
- package/dist/hono/skill-language.d.ts +18 -0
- package/dist/hono/skill-language.d.ts.map +1 -0
- package/dist/hono/skill-language.js +66 -0
- package/dist/hono/skill-language.js.map +1 -0
- package/dist/hono/skill-language.test.d.ts +2 -0
- package/dist/hono/skill-language.test.d.ts.map +1 -0
- package/dist/hono/skill-language.test.js +105 -0
- package/dist/hono/skill-language.test.js.map +1 -0
- package/dist/review/reply-language.d.ts +7 -0
- package/dist/review/reply-language.d.ts.map +1 -1
- package/dist/review/reply-language.js +15 -0
- package/dist/review/reply-language.js.map +1 -1
- package/package.json +1 -1
|
@@ -924,7 +924,7 @@ describe("BridgeJobExecutor", () => {
|
|
|
924
924
|
kind: "completed",
|
|
925
925
|
costUsd: 0.1,
|
|
926
926
|
model: "claude-test",
|
|
927
|
-
cleanBoundary: { version: 2, sessionId: "session_next",
|
|
927
|
+
cleanBoundary: { version: 2, sessionId: "session_next", updatedAtMs: 1234 },
|
|
928
928
|
});
|
|
929
929
|
expect(savedBoundaries).toEqual([]);
|
|
930
930
|
});
|
|
@@ -2532,9 +2532,9 @@ describe("BridgeJobExecutor", () => {
|
|
|
2532
2532
|
});
|
|
2533
2533
|
// BAKER-1025 (AskUserQuestion), the request_tag_input sighting, and the ExitPlanMode and
|
|
2534
2534
|
// request_connection sightings: each held the turn ~900 s before the same deny, although
|
|
2535
|
-
// the card stays up and a later answer resumes the chat as a new turn anyway.
|
|
2535
|
+
// the card stays up and a later answer resumes the chat as a new turn anyway. A question
|
|
2536
|
+
// card is the exception since it picks for the user (BAKER-3550): see the test below.
|
|
2536
2537
|
it.each([
|
|
2537
|
-
{ toolName: "AskUserQuestion", input: { questions: [{ question: "Which budget?" }] }, planMode: false },
|
|
2538
2538
|
{ toolName: "ExitPlanMode", input: { plan: "Draft flow" }, planMode: true },
|
|
2539
2539
|
{
|
|
2540
2540
|
toolName: "mcp__baker_ui__request_tag_input",
|
|
@@ -2592,10 +2592,15 @@ describe("BridgeJobExecutor", () => {
|
|
|
2592
2592
|
return Promise.resolve();
|
|
2593
2593
|
};
|
|
2594
2594
|
let guardResult;
|
|
2595
|
+
let reachedTool = () => undefined;
|
|
2596
|
+
const toolReached = new Promise((resolve) => {
|
|
2597
|
+
reachedTool = resolve;
|
|
2598
|
+
});
|
|
2595
2599
|
const executor = new BridgeJobExecutor({
|
|
2596
2600
|
sdk: {
|
|
2597
2601
|
query: (input) => ({
|
|
2598
2602
|
async *[Symbol.asyncIterator]() {
|
|
2603
|
+
reachedTool();
|
|
2599
2604
|
guardResult = await input.options.canUseTool?.("AskUserQuestion", { questions: [{ label: "Confirm?" }] }, {
|
|
2600
2605
|
toolUseID: "tool_1",
|
|
2601
2606
|
requestId: "req_1",
|
|
@@ -2612,10 +2617,20 @@ describe("BridgeJobExecutor", () => {
|
|
|
2612
2617
|
clear: async () => undefined,
|
|
2613
2618
|
},
|
|
2614
2619
|
relay,
|
|
2615
|
-
askUserQuestionTimeoutMs: 1,
|
|
2616
2620
|
});
|
|
2617
2621
|
const controls = activeControls();
|
|
2618
|
-
|
|
2622
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] });
|
|
2623
|
+
let outcome;
|
|
2624
|
+
try {
|
|
2625
|
+
const running = executor.execute({ threadId: "thread_question_timeout", turnId: "turn_question_timeout", prompt: "Ask first" }, controls);
|
|
2626
|
+
await toolReached;
|
|
2627
|
+
await vi.advanceTimersByTimeAsync(120_000);
|
|
2628
|
+
await vi.runOnlyPendingTimersAsync();
|
|
2629
|
+
outcome = await running;
|
|
2630
|
+
}
|
|
2631
|
+
finally {
|
|
2632
|
+
vi.useRealTimers();
|
|
2633
|
+
}
|
|
2619
2634
|
// The unanswered prompt denies the tool with guidance — the turn completes
|
|
2620
2635
|
// cleanly so the user can answer with a normal message later.
|
|
2621
2636
|
expect(guardResult).toMatchObject({ behavior: "deny" });
|
|
@@ -2630,6 +2645,93 @@ describe("BridgeJobExecutor", () => {
|
|
|
2630
2645
|
// recovery turn. Posting input-resolved here would clear it.
|
|
2631
2646
|
expect(inputResolvedCalls).toBe(0);
|
|
2632
2647
|
});
|
|
2648
|
+
// BAKER-3550: a question card answers itself with the recommended option after a
|
|
2649
|
+
// countdown, so the turn must still be waiting when that pick arrives.
|
|
2650
|
+
it("holds a question card's turn past the countdown that picks for the user, then lets it go", async () => {
|
|
2651
|
+
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] });
|
|
2652
|
+
try {
|
|
2653
|
+
let guardResult;
|
|
2654
|
+
let reachedTool = () => undefined;
|
|
2655
|
+
const toolReached = new Promise((resolve) => {
|
|
2656
|
+
reachedTool = resolve;
|
|
2657
|
+
});
|
|
2658
|
+
const controls = activeControls();
|
|
2659
|
+
const executor = new BridgeJobExecutor({
|
|
2660
|
+
sdk: {
|
|
2661
|
+
query: (queryInput) => ({
|
|
2662
|
+
async *[Symbol.asyncIterator]() {
|
|
2663
|
+
reachedTool();
|
|
2664
|
+
guardResult = await queryInput.options.canUseTool?.("AskUserQuestion", { questions: [{ question: "Which budget?", options: [{ label: "Low" }, { label: "High" }] }] }, {
|
|
2665
|
+
toolUseID: "tool_countdown",
|
|
2666
|
+
requestId: "req_countdown",
|
|
2667
|
+
signal: queryInput.options.abortController?.signal ?? new AbortController().signal,
|
|
2668
|
+
});
|
|
2669
|
+
yield resultMessage(0, "session_countdown");
|
|
2670
|
+
},
|
|
2671
|
+
}),
|
|
2672
|
+
},
|
|
2673
|
+
model: "claude-test",
|
|
2674
|
+
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
2675
|
+
relay: recordingRelay(),
|
|
2676
|
+
});
|
|
2677
|
+
const outcome = executor.execute({ threadId: "thread_countdown", turnId: "turn_countdown", prompt: "Ask first" }, controls);
|
|
2678
|
+
await toolReached;
|
|
2679
|
+
await vi.advanceTimersByTimeAsync(65_000);
|
|
2680
|
+
expect(guardResult).toBeUndefined();
|
|
2681
|
+
expect(controls.pendingQuestions.has("tool_countdown")).toBe(true);
|
|
2682
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
2683
|
+
expect(guardResult).toMatchObject({ behavior: "deny", message: expect.stringContaining("has not answered") });
|
|
2684
|
+
await vi.runOnlyPendingTimersAsync();
|
|
2685
|
+
await outcome;
|
|
2686
|
+
}
|
|
2687
|
+
finally {
|
|
2688
|
+
vi.useRealTimers();
|
|
2689
|
+
}
|
|
2690
|
+
});
|
|
2691
|
+
it("carries on the same turn with the recommended option Baker picked, and tells the agent nobody answered", async () => {
|
|
2692
|
+
const relay = recordingRelay();
|
|
2693
|
+
let guardResult;
|
|
2694
|
+
const controls = activeControls();
|
|
2695
|
+
const question = "Which colour should the ad use?";
|
|
2696
|
+
const executor = new BridgeJobExecutor({
|
|
2697
|
+
sdk: {
|
|
2698
|
+
query: (input) => ({
|
|
2699
|
+
async *[Symbol.asyncIterator]() {
|
|
2700
|
+
const decision = input.options.canUseTool?.("AskUserQuestion", {
|
|
2701
|
+
questions: [
|
|
2702
|
+
{ question, header: "Colour", options: [{ label: "Blue" }, { label: "Teal (Recommended)" }] },
|
|
2703
|
+
],
|
|
2704
|
+
}, {
|
|
2705
|
+
toolUseID: "tool_auto",
|
|
2706
|
+
requestId: "req_auto",
|
|
2707
|
+
signal: input.options.abortController?.signal ?? new AbortController().signal,
|
|
2708
|
+
});
|
|
2709
|
+
while (!controls.pendingQuestions.has("tool_auto")) {
|
|
2710
|
+
await Promise.resolve();
|
|
2711
|
+
}
|
|
2712
|
+
controls.pendingQuestions
|
|
2713
|
+
.get("tool_auto")
|
|
2714
|
+
?.resolve({ answers: { q0: "Teal (Recommended)" }, chosenBy: "auto" });
|
|
2715
|
+
guardResult = await decision;
|
|
2716
|
+
yield resultMessage(0, "session_auto");
|
|
2717
|
+
},
|
|
2718
|
+
}),
|
|
2719
|
+
},
|
|
2720
|
+
model: "claude-test",
|
|
2721
|
+
sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
|
|
2722
|
+
relay,
|
|
2723
|
+
});
|
|
2724
|
+
await executor.execute({ threadId: "thread_auto", turnId: "turn_auto", prompt: "Make an ad" }, controls);
|
|
2725
|
+
expect(guardResult).toMatchObject({
|
|
2726
|
+
behavior: "allow",
|
|
2727
|
+
updatedInput: {
|
|
2728
|
+
answers: { [question]: "Teal (Recommended)" },
|
|
2729
|
+
annotations: {
|
|
2730
|
+
[question]: { notes: expect.stringMatching(/nobody answered.*picked the recommended option automatically/i) },
|
|
2731
|
+
},
|
|
2732
|
+
},
|
|
2733
|
+
});
|
|
2734
|
+
});
|
|
2633
2735
|
// Without this the run deadlocks: the wait expires, the deny tells the agent to
|
|
2634
2736
|
// end its turn, and the pending card it leaves behind parks the thread on a
|
|
2635
2737
|
// question nobody will ever answer — stranding the Session and its Task.
|
|
@@ -4617,6 +4719,43 @@ describe("BridgeJobExecutor review mode", () => {
|
|
|
4617
4719
|
expect(options.settings ?? {}).not.toHaveProperty("disableAllHooks");
|
|
4618
4720
|
expect(options.settings ?? {}).not.toHaveProperty("permissions");
|
|
4619
4721
|
});
|
|
4722
|
+
// BAKER-3462: after an English skill body, the next line went out in English to a Spanish chat.
|
|
4723
|
+
const spanishTurn = "Prepara cuatro anuncios nuevos para la campaña de verano y súbelos hoy mismo";
|
|
4724
|
+
async function contextAfterSkillLoad(options) {
|
|
4725
|
+
const hook = options.hooks?.PostToolUse?.find((matcher) => matcher.matcher === "Skill")?.hooks[0];
|
|
4726
|
+
const output = await hook?.({
|
|
4727
|
+
hook_event_name: "PostToolUse",
|
|
4728
|
+
tool_name: "Skill",
|
|
4729
|
+
tool_input: { skill: "creative-canvas-ads" },
|
|
4730
|
+
tool_response: "Launching skill: creative-canvas-ads",
|
|
4731
|
+
tool_use_id: "t1",
|
|
4732
|
+
session_id: "s",
|
|
4733
|
+
transcript_path: "",
|
|
4734
|
+
cwd: "/",
|
|
4735
|
+
}, "t1", { signal: new AbortController().signal });
|
|
4736
|
+
return output && "hookSpecificOutput" in output && output.hookSpecificOutput?.hookEventName === "PostToolUse"
|
|
4737
|
+
? output.hookSpecificOutput.additionalContext
|
|
4738
|
+
: undefined;
|
|
4739
|
+
}
|
|
4740
|
+
it("restates a Spanish chat's language to the agent right after a skill loads", async () => {
|
|
4741
|
+
const queryInputs = [];
|
|
4742
|
+
const executor = createExecutor({ queryInputs });
|
|
4743
|
+
await executor.execute({ threadId: "t", turnId: "turn_1", prompt: spanishTurn, currentSender: { name: "Fernando" } }, activeControls());
|
|
4744
|
+
const options = queryInputs[0]?.options;
|
|
4745
|
+
if (!options)
|
|
4746
|
+
throw new Error("expected the turn to start one query");
|
|
4747
|
+
expect((await contextAfterSkillLoad(options))?.startsWith("Baker: the language of this chat.")).toBe(true);
|
|
4748
|
+
});
|
|
4749
|
+
it("adds no language note to a review, which loads no skill and writes to nobody", async () => {
|
|
4750
|
+
const queryInputs = [];
|
|
4751
|
+
const executor = createExecutor({ queryInputs });
|
|
4752
|
+
await executor.execute(reviewJob({ prompt: spanishTurn, currentSender: { name: "Fernando" } }), activeControls());
|
|
4753
|
+
const options = queryInputs[0]?.options;
|
|
4754
|
+
if (!options)
|
|
4755
|
+
throw new Error("expected the review to start one query");
|
|
4756
|
+
expect(options.hooks?.PostToolUse?.some((matcher) => matcher.matcher === "Skill")).toBe(false);
|
|
4757
|
+
expect(options.hooks?.UserPromptExpansion).toBeUndefined();
|
|
4758
|
+
});
|
|
4620
4759
|
it("denies a mutating shell command and says why, so the reviewer reports it", async () => {
|
|
4621
4760
|
// The Session it is reading is over. A write would change the very evidence
|
|
4622
4761
|
// its findings cite, and nobody is left to review what the reviewer did.
|