@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.
Files changed (43) hide show
  1. package/README.md +26 -8
  2. package/dist/hono/agent-session.d.ts +1 -0
  3. package/dist/hono/agent-session.d.ts.map +1 -1
  4. package/dist/hono/agent-session.js +1 -1
  5. package/dist/hono/agent-session.js.map +1 -1
  6. package/dist/hono/agent-session.test.js +1 -1
  7. package/dist/hono/agent-session.test.js.map +1 -1
  8. package/dist/hono/agent.d.ts +1 -1
  9. package/dist/hono/agent.d.ts.map +1 -1
  10. package/dist/hono/agent.js +2 -2
  11. package/dist/hono/agent.js.map +1 -1
  12. package/dist/hono/bridge-job.d.ts +6 -1
  13. package/dist/hono/bridge-job.d.ts.map +1 -1
  14. package/dist/hono/bridge-job.js +36 -6
  15. package/dist/hono/bridge-job.js.map +1 -1
  16. package/dist/hono/bridge-job.test.js +144 -5
  17. package/dist/hono/bridge-job.test.js.map +1 -1
  18. package/dist/hono/publish-handoff.d.ts +10 -0
  19. package/dist/hono/publish-handoff.d.ts.map +1 -1
  20. package/dist/hono/publish-handoff.js +17 -7
  21. package/dist/hono/publish-handoff.js.map +1 -1
  22. package/dist/hono/server.d.ts.map +1 -1
  23. package/dist/hono/server.js +3 -1
  24. package/dist/hono/server.js.map +1 -1
  25. package/dist/hono/session-continuity.d.ts +8 -0
  26. package/dist/hono/session-continuity.d.ts.map +1 -1
  27. package/dist/hono/session-continuity.integration.test.js +34 -7
  28. package/dist/hono/session-continuity.integration.test.js.map +1 -1
  29. package/dist/hono/session-continuity.js +9 -6
  30. package/dist/hono/session-continuity.js.map +1 -1
  31. package/dist/hono/skill-language.d.ts +18 -0
  32. package/dist/hono/skill-language.d.ts.map +1 -0
  33. package/dist/hono/skill-language.js +66 -0
  34. package/dist/hono/skill-language.js.map +1 -0
  35. package/dist/hono/skill-language.test.d.ts +2 -0
  36. package/dist/hono/skill-language.test.d.ts.map +1 -0
  37. package/dist/hono/skill-language.test.js +105 -0
  38. package/dist/hono/skill-language.test.js.map +1 -0
  39. package/dist/review/reply-language.d.ts +7 -0
  40. package/dist/review/reply-language.d.ts.map +1 -1
  41. package/dist/review/reply-language.js +15 -0
  42. package/dist/review/reply-language.js.map +1 -1
  43. 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", resumeSessionAt: "assistant_next", updatedAtMs: 1234 },
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
- const outcome = await executor.execute({ threadId: "thread_question_timeout", turnId: "turn_question_timeout", prompt: "Ask first" }, controls);
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.