@koda-sl/baker-bridge 0.144.0 → 0.145.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.
@@ -2121,6 +2121,60 @@ describe("BridgeJobExecutor", () => {
2121
2121
  await inactiveExecutor.execute({ threadId: "thread_inactive_close", turnId: "turn_inactive_close", prompt: "Stop", history: history([]) }, inactiveControls);
2122
2122
  expect(inactiveCloseCalls).toEqual(["close"]);
2123
2123
  });
2124
+ // BAKER-1025 (AskUserQuestion), the request_tag_input sighting, and the ExitPlanMode and
2125
+ // request_connection sightings: each held the turn ~900 s before the same deny, although
2126
+ // the card stays up and a later answer resumes the chat as a new turn anyway.
2127
+ it.each([
2128
+ { toolName: "AskUserQuestion", input: { questions: [{ question: "Which budget?" }] }, planMode: false },
2129
+ { toolName: "ExitPlanMode", input: { plan: "Draft flow" }, planMode: true },
2130
+ {
2131
+ toolName: "mcp__baker_ui__request_tag_input",
2132
+ input: { changes: [{ mode: "create", tagType: "clarity" }] },
2133
+ planMode: false,
2134
+ },
2135
+ {
2136
+ toolName: CONNECTION_INPUT_TOOL_NAME,
2137
+ input: { connections: [{ platform: "hubspot", reason: "Send leads to HubSpot" }] },
2138
+ planMode: false,
2139
+ },
2140
+ ])("releases the turn within a minute when $toolName goes unanswered", async ({ toolName, input, planMode }) => {
2141
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] });
2142
+ try {
2143
+ let guardResult;
2144
+ let reachedTool = () => undefined;
2145
+ const toolReached = new Promise((resolve) => {
2146
+ reachedTool = resolve;
2147
+ });
2148
+ const executor = new BridgeJobExecutor({
2149
+ sdk: {
2150
+ query: (queryInput) => ({
2151
+ async *[Symbol.asyncIterator]() {
2152
+ reachedTool();
2153
+ guardResult = await queryInput.options.canUseTool?.(toolName, input, {
2154
+ toolUseID: "tool_unanswered",
2155
+ requestId: "req_unanswered",
2156
+ signal: queryInput.options.abortController?.signal ?? new AbortController().signal,
2157
+ });
2158
+ yield resultMessage(0, "session_unanswered");
2159
+ },
2160
+ }),
2161
+ },
2162
+ model: "claude-test",
2163
+ sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
2164
+ relay: recordingRelay(),
2165
+ });
2166
+ const controls = activeControls();
2167
+ const outcome = executor.execute({ threadId: "thread_unanswered", turnId: "turn_unanswered", prompt: "Ask first", planMode }, controls);
2168
+ await toolReached;
2169
+ await vi.advanceTimersByTimeAsync(59_000);
2170
+ expect(guardResult).toMatchObject({ behavior: "deny", message: expect.stringContaining("has not answered") });
2171
+ await vi.runOnlyPendingTimersAsync();
2172
+ await outcome;
2173
+ }
2174
+ finally {
2175
+ vi.useRealTimers();
2176
+ }
2177
+ });
2124
2178
  it("denies an unanswered AskUserQuestion gracefully instead of erroring the turn", async () => {
2125
2179
  const relay = recordingRelay();
2126
2180
  let inputResolvedCalls = 0;
@@ -2245,6 +2299,57 @@ describe("BridgeJobExecutor", () => {
2245
2299
  expect(castingRequests).toHaveLength(1);
2246
2300
  expect(castingRequests[0]).toMatchObject({ avatarHandle: "elena" });
2247
2301
  });
2302
+ it("keeps the ad behind an unpicked casting until its portraits render and the auto-pick lands", async () => {
2303
+ // The portraits take about 40 s to render and the auto-pick waits 30 s more, so a
2304
+ // casting resolves itself around 70 s. The question cards' short wait must not let the
2305
+ // next tool run before the face is chosen.
2306
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "setInterval", "clearInterval"] });
2307
+ try {
2308
+ const controls = activeControls();
2309
+ let nextToolResult;
2310
+ let castingOpened = () => undefined;
2311
+ const opened = new Promise((resolve) => {
2312
+ castingOpened = resolve;
2313
+ });
2314
+ const relay = recordingRelay();
2315
+ relay.avatarCastingRequest = () => {
2316
+ castingOpened();
2317
+ return Promise.resolve(true);
2318
+ };
2319
+ const executor = new BridgeJobExecutor({
2320
+ sdk: {
2321
+ query: (input) => ({
2322
+ async *[Symbol.asyncIterator]() {
2323
+ const call = (name, toolInput, id) => input.options.canUseTool?.(name, toolInput, {
2324
+ toolUseID: id,
2325
+ requestId: `req_${id}`,
2326
+ signal: input.options.abortController?.signal ?? new AbortController().signal,
2327
+ });
2328
+ await call("Bash", { command: 'baker avatars create --name "Elena" --subject "a woman"' }, "t1");
2329
+ nextToolResult = await call("Bash", { command: "baker studio animate 'Elena talks' --avatar elena" }, "t2");
2330
+ yield resultMessage(0, "session_slow_cast");
2331
+ },
2332
+ }),
2333
+ },
2334
+ model: "claude-test",
2335
+ sessionStore: { load: async () => undefined, save: async () => undefined, clear: async () => undefined },
2336
+ relay,
2337
+ });
2338
+ const outcome = executor.execute({ threadId: "thread_sc", turnId: "turn_sc", prompt: "Crea un avatar" }, controls);
2339
+ await opened;
2340
+ await vi.advanceTimersByTimeAsync(90_000);
2341
+ expect(nextToolResult).toBeUndefined();
2342
+ for (const pending of controls.pendingAvatarCastings.values()) {
2343
+ pending.resolve({ status: "cast", avatarHandle: "elena", imageId: "img_auto" });
2344
+ }
2345
+ await vi.runOnlyPendingTimersAsync();
2346
+ await outcome;
2347
+ expect(nextToolResult).toMatchObject({ behavior: "allow" });
2348
+ }
2349
+ finally {
2350
+ vi.useRealTimers();
2351
+ }
2352
+ });
2248
2353
  it("hands the agent's own casting call the face the user picked, instead of a second picker", async () => {
2249
2354
  // An agent may still call it right after `baker avatars create` (the tool now says the
2250
2355
  // picker opens by itself, but the call is tolerated). The auto-cast owed for that create used