@koda-sl/baker-bridge 0.161.0 → 0.163.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.
Files changed (41) hide show
  1. package/README.md +15 -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 +8 -1
  13. package/dist/hono/bridge-job.d.ts.map +1 -1
  14. package/dist/hono/bridge-job.js +47 -5
  15. package/dist/hono/bridge-job.js.map +1 -1
  16. package/dist/hono/bridge-job.test.js +134 -5
  17. package/dist/hono/bridge-job.test.js.map +1 -1
  18. package/dist/hono/jev.d.ts +4 -1
  19. package/dist/hono/jev.d.ts.map +1 -1
  20. package/dist/hono/jev.js +5 -2
  21. package/dist/hono/jev.js.map +1 -1
  22. package/dist/hono/jev.test.js +2 -1
  23. package/dist/hono/jev.test.js.map +1 -1
  24. package/dist/hono/openrouter-attribution.d.ts +31 -0
  25. package/dist/hono/openrouter-attribution.d.ts.map +1 -0
  26. package/dist/hono/openrouter-attribution.js +70 -0
  27. package/dist/hono/openrouter-attribution.js.map +1 -0
  28. package/dist/hono/openrouter-attribution.test.d.ts +2 -0
  29. package/dist/hono/openrouter-attribution.test.d.ts.map +1 -0
  30. package/dist/hono/openrouter-attribution.test.js +48 -0
  31. package/dist/hono/openrouter-attribution.test.js.map +1 -0
  32. package/dist/hono/openrouter-session.d.ts +11 -5
  33. package/dist/hono/openrouter-session.d.ts.map +1 -1
  34. package/dist/hono/openrouter-session.js +9 -5
  35. package/dist/hono/openrouter-session.js.map +1 -1
  36. package/dist/hono/openrouter-session.test.js +9 -0
  37. package/dist/hono/openrouter-session.test.js.map +1 -1
  38. package/dist/hono/server.d.ts.map +1 -1
  39. package/dist/hono/server.js +3 -1
  40. package/dist/hono/server.js.map +1 -1
  41. package/package.json +1 -1
@@ -18,7 +18,7 @@ vi.mock("./runtime-files.ts", async (importOriginal) => {
18
18
  });
19
19
  import { AVATAR_CASTING_TOOL_NAME } from "./avatar-casting.js";
20
20
  import { FACE_PICK_QUESTION } from "./avatar-face-question.js";
21
- import { BridgeJobExecutor, createStallDetector, isContextOverflowRefusal, isControlChannelDeny, parsePlanDecision, remapAnswersToQuestionText, } from "./bridge-job.js";
21
+ import { agentFeature, BridgeJobExecutor, createStallDetector, isContextOverflowRefusal, isControlChannelDeny, parsePlanDecision, remapAnswersToQuestionText, } from "./bridge-job.js";
22
22
  import { CONNECTION_INPUT_TOOL_NAME } from "./connection-input.js";
23
23
  import { CREDENTIAL_INPUT_TOOL_NAME } from "./credential-input.js";
24
24
  import { FLOW_INPUT_TOOL_NAME } from "./flow-input.js";
@@ -1244,6 +1244,9 @@ describe("BridgeJobExecutor", () => {
1244
1244
  });
1245
1245
  it("propagates the inherited process env (OpenRouter routing) to the agent subprocess", async () => {
1246
1246
  vi.stubEnv("ANTHROPIC_AUTH_TOKEN", "sk-or-test-key");
1247
+ vi.stubEnv("BAKER_ENVIRONMENT", "production");
1248
+ vi.stubEnv("BAKER_RUNTIME_COMPANY_ID", "company_env");
1249
+ vi.stubEnv("BAKER_CHAT_ID", "chat_env");
1247
1250
  const queryInputs = [];
1248
1251
  const executor = createExecutor({
1249
1252
  queryInputs,
@@ -1265,8 +1268,25 @@ describe("BridgeJobExecutor", () => {
1265
1268
  allow_fallbacks: true,
1266
1269
  },
1267
1270
  session_id: "thread_env",
1271
+ user: "agent.turn | company:company_env | chat:chat_env",
1272
+ trace: {
1273
+ trace_name: "agent.turn",
1274
+ generation_name: "agent.turn",
1275
+ feature: "agent.turn",
1276
+ service: "agent",
1277
+ environment: "production",
1278
+ company_id: "company_env",
1279
+ chat_id: "chat_env",
1280
+ },
1268
1281
  plugins: [{ id: "web", enabled: false }],
1269
1282
  });
1283
+ // The same tag as headers, which is what names the app in OpenRouter.
1284
+ expect(env?.ANTHROPIC_CUSTOM_HEADERS).toBe([
1285
+ "HTTP-Referer: https://agent.production.withbaker.com",
1286
+ "X-OpenRouter-Title: Baker agent (production)",
1287
+ "X-Title: Baker agent (production)",
1288
+ "x-session-id: thread_env",
1289
+ ].join("\n"));
1270
1290
  });
1271
1291
  // OpenRouter has no Bedrock endpoint for a per-message effort, so while the CLI
1272
1292
  // sends one every Opus 5.5 request skips Bedrock and bills OpenRouter credits.
@@ -2532,9 +2552,9 @@ describe("BridgeJobExecutor", () => {
2532
2552
  });
2533
2553
  // BAKER-1025 (AskUserQuestion), the request_tag_input sighting, and the ExitPlanMode and
2534
2554
  // 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.
2555
+ // the card stays up and a later answer resumes the chat as a new turn anyway. A question
2556
+ // card is the exception since it picks for the user (BAKER-3550): see the test below.
2536
2557
  it.each([
2537
- { toolName: "AskUserQuestion", input: { questions: [{ question: "Which budget?" }] }, planMode: false },
2538
2558
  { toolName: "ExitPlanMode", input: { plan: "Draft flow" }, planMode: true },
2539
2559
  {
2540
2560
  toolName: "mcp__baker_ui__request_tag_input",
@@ -2592,10 +2612,15 @@ describe("BridgeJobExecutor", () => {
2592
2612
  return Promise.resolve();
2593
2613
  };
2594
2614
  let guardResult;
2615
+ let reachedTool = () => undefined;
2616
+ const toolReached = new Promise((resolve) => {
2617
+ reachedTool = resolve;
2618
+ });
2595
2619
  const executor = new BridgeJobExecutor({
2596
2620
  sdk: {
2597
2621
  query: (input) => ({
2598
2622
  async *[Symbol.asyncIterator]() {
2623
+ reachedTool();
2599
2624
  guardResult = await input.options.canUseTool?.("AskUserQuestion", { questions: [{ label: "Confirm?" }] }, {
2600
2625
  toolUseID: "tool_1",
2601
2626
  requestId: "req_1",
@@ -2612,10 +2637,20 @@ describe("BridgeJobExecutor", () => {
2612
2637
  clear: async () => undefined,
2613
2638
  },
2614
2639
  relay,
2615
- askUserQuestionTimeoutMs: 1,
2616
2640
  });
2617
2641
  const controls = activeControls();
2618
- const outcome = await executor.execute({ threadId: "thread_question_timeout", turnId: "turn_question_timeout", prompt: "Ask first" }, controls);
2642
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] });
2643
+ let outcome;
2644
+ try {
2645
+ const running = executor.execute({ threadId: "thread_question_timeout", turnId: "turn_question_timeout", prompt: "Ask first" }, controls);
2646
+ await toolReached;
2647
+ await vi.advanceTimersByTimeAsync(120_000);
2648
+ await vi.runOnlyPendingTimersAsync();
2649
+ outcome = await running;
2650
+ }
2651
+ finally {
2652
+ vi.useRealTimers();
2653
+ }
2619
2654
  // The unanswered prompt denies the tool with guidance — the turn completes
2620
2655
  // cleanly so the user can answer with a normal message later.
2621
2656
  expect(guardResult).toMatchObject({ behavior: "deny" });
@@ -2630,6 +2665,93 @@ describe("BridgeJobExecutor", () => {
2630
2665
  // recovery turn. Posting input-resolved here would clear it.
2631
2666
  expect(inputResolvedCalls).toBe(0);
2632
2667
  });
2668
+ // BAKER-3550: a question card answers itself with the recommended option after a
2669
+ // countdown, so the turn must still be waiting when that pick arrives.
2670
+ it("holds a question card's turn past the countdown that picks for the user, then lets it go", async () => {
2671
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] });
2672
+ try {
2673
+ let guardResult;
2674
+ let reachedTool = () => undefined;
2675
+ const toolReached = new Promise((resolve) => {
2676
+ reachedTool = resolve;
2677
+ });
2678
+ const controls = activeControls();
2679
+ const executor = new BridgeJobExecutor({
2680
+ sdk: {
2681
+ query: (queryInput) => ({
2682
+ async *[Symbol.asyncIterator]() {
2683
+ reachedTool();
2684
+ guardResult = await queryInput.options.canUseTool?.("AskUserQuestion", { questions: [{ question: "Which budget?", options: [{ label: "Low" }, { label: "High" }] }] }, {
2685
+ toolUseID: "tool_countdown",
2686
+ requestId: "req_countdown",
2687
+ signal: queryInput.options.abortController?.signal ?? new AbortController().signal,
2688
+ });
2689
+ yield resultMessage(0, "session_countdown");
2690
+ },
2691
+ }),
2692
+ },
2693
+ model: "claude-test",
2694
+ sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
2695
+ relay: recordingRelay(),
2696
+ });
2697
+ const outcome = executor.execute({ threadId: "thread_countdown", turnId: "turn_countdown", prompt: "Ask first" }, controls);
2698
+ await toolReached;
2699
+ await vi.advanceTimersByTimeAsync(65_000);
2700
+ expect(guardResult).toBeUndefined();
2701
+ expect(controls.pendingQuestions.has("tool_countdown")).toBe(true);
2702
+ await vi.advanceTimersByTimeAsync(60_000);
2703
+ expect(guardResult).toMatchObject({ behavior: "deny", message: expect.stringContaining("has not answered") });
2704
+ await vi.runOnlyPendingTimersAsync();
2705
+ await outcome;
2706
+ }
2707
+ finally {
2708
+ vi.useRealTimers();
2709
+ }
2710
+ });
2711
+ it("carries on the same turn with the recommended option Baker picked, and tells the agent nobody answered", async () => {
2712
+ const relay = recordingRelay();
2713
+ let guardResult;
2714
+ const controls = activeControls();
2715
+ const question = "Which colour should the ad use?";
2716
+ const executor = new BridgeJobExecutor({
2717
+ sdk: {
2718
+ query: (input) => ({
2719
+ async *[Symbol.asyncIterator]() {
2720
+ const decision = input.options.canUseTool?.("AskUserQuestion", {
2721
+ questions: [
2722
+ { question, header: "Colour", options: [{ label: "Blue" }, { label: "Teal (Recommended)" }] },
2723
+ ],
2724
+ }, {
2725
+ toolUseID: "tool_auto",
2726
+ requestId: "req_auto",
2727
+ signal: input.options.abortController?.signal ?? new AbortController().signal,
2728
+ });
2729
+ while (!controls.pendingQuestions.has("tool_auto")) {
2730
+ await Promise.resolve();
2731
+ }
2732
+ controls.pendingQuestions
2733
+ .get("tool_auto")
2734
+ ?.resolve({ answers: { q0: "Teal (Recommended)" }, chosenBy: "auto" });
2735
+ guardResult = await decision;
2736
+ yield resultMessage(0, "session_auto");
2737
+ },
2738
+ }),
2739
+ },
2740
+ model: "claude-test",
2741
+ sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
2742
+ relay,
2743
+ });
2744
+ await executor.execute({ threadId: "thread_auto", turnId: "turn_auto", prompt: "Make an ad" }, controls);
2745
+ expect(guardResult).toMatchObject({
2746
+ behavior: "allow",
2747
+ updatedInput: {
2748
+ answers: { [question]: "Teal (Recommended)" },
2749
+ annotations: {
2750
+ [question]: { notes: expect.stringMatching(/nobody answered.*picked the recommended option automatically/i) },
2751
+ },
2752
+ },
2753
+ });
2754
+ });
2633
2755
  // Without this the run deadlocks: the wait expires, the deny tells the agent to
2634
2756
  // end its turn, and the pending card it leaves behind parks the thread on a
2635
2757
  // question nobody will ever answer — stranding the Session and its Task.
@@ -4864,4 +4986,11 @@ describe("BridgeJobExecutor model refusals", () => {
4864
4986
  expect(outcome).toMatchObject({ kind: "completed" });
4865
4987
  });
4866
4988
  });
4989
+ describe("agentFeature", () => {
4990
+ it("tells OpenRouter a Session review, a plan and a chat turn apart", () => {
4991
+ expect(agentFeature({ review: {} })).toBe("chatReview.session");
4992
+ expect(agentFeature({ planMode: true })).toBe("agent.plan");
4993
+ expect(agentFeature({})).toBe("agent.turn");
4994
+ });
4995
+ });
4867
4996
  //# sourceMappingURL=bridge-job.test.js.map