@koda-sl/baker-bridge 0.108.0-dev.0306d9ad9 → 0.108.0-dev.92a938f8d
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 +30 -32
- package/dist/hono/agent-session.d.ts +0 -1
- package/dist/hono/agent-session.d.ts.map +1 -1
- package/dist/hono/agent-session.js +2 -13
- package/dist/hono/agent-session.js.map +1 -1
- package/dist/hono/agent-session.test.js +6 -43
- package/dist/hono/agent-session.test.js.map +1 -1
- package/dist/hono/agent-tasks.d.ts.map +1 -1
- package/dist/hono/agent-tasks.js +3 -7
- package/dist/hono/agent-tasks.js.map +1 -1
- package/dist/hono/agent-tasks.test.js +0 -15
- package/dist/hono/agent-tasks.test.js.map +1 -1
- 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/agent.test.js +9 -9
- package/dist/hono/avatar-casting.js +2 -2
- package/dist/hono/avatar-casting.js.map +1 -1
- package/dist/hono/bridge-job.d.ts +15 -20
- package/dist/hono/bridge-job.d.ts.map +1 -1
- package/dist/hono/bridge-job.js +138 -110
- package/dist/hono/bridge-job.js.map +1 -1
- package/dist/hono/bridge-job.test.js +175 -76
- package/dist/hono/bridge-job.test.js.map +1 -1
- package/dist/hono/connection-input.d.ts +8 -1
- package/dist/hono/connection-input.d.ts.map +1 -1
- package/dist/hono/connection-input.js +4 -2
- package/dist/hono/connection-input.js.map +1 -1
- package/dist/hono/convex.d.ts +1 -0
- package/dist/hono/convex.d.ts.map +1 -1
- package/dist/hono/convex.js +4 -0
- package/dist/hono/convex.js.map +1 -1
- package/dist/hono/convex.test.js +3 -3
- package/dist/hono/mcp-servers.d.ts +0 -2
- package/dist/hono/mcp-servers.d.ts.map +1 -1
- package/dist/hono/mcp-servers.js +16 -17
- package/dist/hono/mcp-servers.js.map +1 -1
- package/dist/hono/mcp-servers.test.js +38 -10
- package/dist/hono/mcp-servers.test.js.map +1 -1
- package/dist/hono/model-capabilities.d.ts.map +1 -1
- package/dist/hono/model-capabilities.js +19 -28
- package/dist/hono/model-capabilities.js.map +1 -1
- package/dist/hono/model-capabilities.test.js +15 -37
- package/dist/hono/model-capabilities.test.js.map +1 -1
- package/dist/hono/relay.d.ts +2 -1
- package/dist/hono/relay.d.ts.map +1 -1
- package/dist/hono/relay.js +3 -0
- package/dist/hono/relay.js.map +1 -1
- package/dist/hono/relay.test.js +22 -6
- package/dist/hono/relay.test.js.map +1 -1
- package/dist/hono/session-continuity.integration.test.js +4 -4
- package/dist/hono/tag-fields.js +1 -1
- package/dist/hono/tag-fields.js.map +1 -1
- package/dist/hono/tag-input.js +1 -1
- package/dist/hono/tag-input.js.map +1 -1
- package/dist/hono/turn-events.d.ts +1 -1
- package/dist/hono/turn-events.d.ts.map +1 -1
- package/dist/hono/turn-events.js.map +1 -1
- package/package.json +1 -1
|
@@ -16,7 +16,7 @@ vi.mock("./runtime-files.ts", async (importOriginal) => {
|
|
|
16
16
|
};
|
|
17
17
|
});
|
|
18
18
|
import { AVATAR_CASTING_TOOL_NAME } from "./avatar-casting.js";
|
|
19
|
-
import { BridgeJobExecutor, createStallDetector, isContextOverflowRefusal, isControlChannelDeny,
|
|
19
|
+
import { BridgeJobExecutor, createStallDetector, isContextOverflowRefusal, isControlChannelDeny, parsePlanDecision, remapAnswersToQuestionText, } from "./bridge-job.js";
|
|
20
20
|
import { CONNECTION_INPUT_TOOL_NAME } from "./connection-input.js";
|
|
21
21
|
import { FLOW_INPUT_TOOL_NAME } from "./flow-input.js";
|
|
22
22
|
import { bridgeRuntimeFiles } from "./runtime-files.js";
|
|
@@ -63,6 +63,7 @@ function recordingRelay() {
|
|
|
63
63
|
const agentActivityUpdates = [];
|
|
64
64
|
const compactionUpdates = [];
|
|
65
65
|
const reviewDigests = [];
|
|
66
|
+
const retractedEvents = [];
|
|
66
67
|
const contextUsageUpdates = [];
|
|
67
68
|
return {
|
|
68
69
|
assistantEvents,
|
|
@@ -71,6 +72,7 @@ function recordingRelay() {
|
|
|
71
72
|
compactionUpdates,
|
|
72
73
|
contextUsageUpdates,
|
|
73
74
|
reviewDigests,
|
|
75
|
+
retractedEvents,
|
|
74
76
|
startPreview: () => undefined,
|
|
75
77
|
previewText: () => undefined,
|
|
76
78
|
previewTools: () => undefined,
|
|
@@ -86,6 +88,10 @@ function recordingRelay() {
|
|
|
86
88
|
assistantEvents.push({ target, event, model });
|
|
87
89
|
return Promise.resolve();
|
|
88
90
|
},
|
|
91
|
+
retractEvents: (target, eventIds) => {
|
|
92
|
+
retractedEvents.push({ target, eventIds });
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
},
|
|
89
95
|
complete: () => Promise.resolve(),
|
|
90
96
|
inputRequest: () => Promise.resolve(true),
|
|
91
97
|
inputResolved: () => Promise.resolve(),
|
|
@@ -308,8 +314,8 @@ describe("isControlChannelDeny", () => {
|
|
|
308
314
|
})).toBe(false);
|
|
309
315
|
});
|
|
310
316
|
it("does not match a PreToolUse hook block that quotes the envelope in its explanation", () => {
|
|
311
|
-
// Live-run finding: the scaffold's guard-background-commands hook
|
|
312
|
-
//
|
|
317
|
+
// Live-run finding: the scaffold's guard-background-commands hook explains
|
|
318
|
+
// WHY it blocks a backgrounded subagent by quoting the failure it prevents.
|
|
313
319
|
// Its block arrives as an is_error tool_result, so an envelope-substring
|
|
314
320
|
// match counted it as a deny — three blocked spawns in one turn would have
|
|
315
321
|
// tripped the breaker and errored a perfectly healthy turn.
|
|
@@ -745,13 +751,13 @@ describe("BridgeJobExecutor", () => {
|
|
|
745
751
|
const relay = recordingRelay();
|
|
746
752
|
const executor = createExecutor({
|
|
747
753
|
relay,
|
|
748
|
-
model: "claude-opus-
|
|
754
|
+
model: "claude-opus-5-5",
|
|
749
755
|
queryResults: [
|
|
750
756
|
scriptedMessages(assistantMessage("assistant_1", "Done.", "session_next", {
|
|
751
757
|
usage: { input_tokens: 4000, cache_read_input_tokens: 96000 },
|
|
752
758
|
}),
|
|
753
759
|
// The runtime reports Opus's catalog window as 200k through the
|
|
754
|
-
// OpenRouter passthrough — wrong. Opus
|
|
760
|
+
// OpenRouter passthrough — wrong. Opus 5.5 is 1M on OpenRouter.
|
|
755
761
|
resultMessage(0.02, "session_next", {
|
|
756
762
|
"claude-main": {
|
|
757
763
|
inputTokens: 4000,
|
|
@@ -1163,7 +1169,8 @@ describe("BridgeJobExecutor", () => {
|
|
|
1163
1169
|
// Read off a session's own `init` tool list, the CLI offers these to every
|
|
1164
1170
|
// Session. Each one reaches past what a marketer's chat is for: the worktree
|
|
1165
1171
|
// pair rearranges the branches Publish owns, `Workflow` fans out a fleet on
|
|
1166
|
-
// the organization's OpenRouter key
|
|
1172
|
+
// the organization's OpenRouter key, and `SendMessage` addresses peers a
|
|
1173
|
+
// single-session Runtime does not have.
|
|
1167
1174
|
it("removes the tools that reach outside what a Session is", async () => {
|
|
1168
1175
|
const queryInputs = [];
|
|
1169
1176
|
const executor = createExecutor({
|
|
@@ -1172,12 +1179,10 @@ describe("BridgeJobExecutor", () => {
|
|
|
1172
1179
|
});
|
|
1173
1180
|
await executor.execute({ threadId: "thread_reach", turnId: "turn_reach", prompt: "Build a landing", history: history([]) }, activeControls());
|
|
1174
1181
|
const disallowed = queryInputs[0]?.options.disallowedTools ?? [];
|
|
1175
|
-
expect(disallowed).toEqual(expect.arrayContaining(["EnterWorktree", "ExitWorktree", "Workflow"]));
|
|
1182
|
+
expect(disallowed).toEqual(expect.arrayContaining(["EnterWorktree", "ExitWorktree", "Workflow", "SendMessage"]));
|
|
1176
1183
|
// `Task` is the subagent spawn itself — the scaffold's own fan-out workers
|
|
1177
|
-
// ride it, so blocking multi-agent orchestration must not catch it.
|
|
1178
|
-
// `SendMessage` is how the agent follows up with a subagent it already ran.
|
|
1184
|
+
// ride it, so blocking multi-agent orchestration must not catch it.
|
|
1179
1185
|
expect(disallowed).not.toContain("Task");
|
|
1180
|
-
expect(disallowed).not.toContain("SendMessage");
|
|
1181
1186
|
});
|
|
1182
1187
|
it("allowlists the workspace's own skills so the CLI's built-in developer skills stay out of the session", async () => {
|
|
1183
1188
|
const workspace = await mkdtemp(join(tmpdir(), "baker-job-skills-"));
|
|
@@ -1365,42 +1370,31 @@ describe("BridgeJobExecutor", () => {
|
|
|
1365
1370
|
await executor.execute({ threadId: "thread_uw", turnId: "turn_uw", prompt: "Hi", history: history([]) }, activeControls());
|
|
1366
1371
|
expect((queryInputs[0]?.options.settings ?? {})).not.toHaveProperty("autoCompactWindow");
|
|
1367
1372
|
});
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
it("
|
|
1372
|
-
vi.stubEnv("BAKER_CHAT_ID", "");
|
|
1373
|
+
// No output style is sent. `Concise` was pinned against Opus 5's preamble; on
|
|
1374
|
+
// Opus 5.5 it suppressed the one-line progress notes a marketer reads while a
|
|
1375
|
+
// long turn works. The voice rules live in the scaffold CLAUDE.md instead.
|
|
1376
|
+
it("sends no output style, so the default voice and its progress notes survive", async () => {
|
|
1373
1377
|
const queryInputs = [];
|
|
1374
1378
|
const executor = createExecutor({
|
|
1375
1379
|
queryInputs,
|
|
1376
|
-
|
|
1377
|
-
queryResults: [scriptedMessages(resultMessage(0, "session_no_settings"))],
|
|
1380
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_style"))],
|
|
1378
1381
|
});
|
|
1379
|
-
await executor.execute({ threadId: "
|
|
1380
|
-
expect(queryInputs[0]?.options.settings).
|
|
1381
|
-
});
|
|
1382
|
-
// The CLI accepts an unrecognized output-style name silently — a misspelling
|
|
1383
|
-
// returns a clean 200 on the default voice, with nothing at runtime to notice
|
|
1384
|
-
// it. Verified against the CLI: `{"outputStyle":"NotARealStyle"}` succeeded.
|
|
1385
|
-
// So the literal is pinned here rather than left to review.
|
|
1386
|
-
it("names the built-in Concise style exactly", () => {
|
|
1387
|
-
expect(OUTPUT_STYLE).toBe("Concise");
|
|
1382
|
+
await executor.execute({ threadId: "thread_style", turnId: "turn_style_1", prompt: "Hi", history: history([]) }, activeControls());
|
|
1383
|
+
expect((queryInputs[0]?.options.settings ?? {})).not.toHaveProperty("outputStyle");
|
|
1388
1384
|
});
|
|
1389
|
-
|
|
1385
|
+
// Nothing to put in the settings tier (unknown model window, no chat id for the
|
|
1386
|
+
// memory directory) means no `settings` key at all: the SDK forwards even `{}`
|
|
1387
|
+
// to the CLI as a `--settings` flag tier.
|
|
1388
|
+
it("omits the settings tier when nothing in it applies", async () => {
|
|
1389
|
+
vi.stubEnv("BAKER_CHAT_ID", "");
|
|
1390
1390
|
const queryInputs = [];
|
|
1391
1391
|
const executor = createExecutor({
|
|
1392
1392
|
queryInputs,
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
scriptedMessages(resultMessage(0, "session_style")),
|
|
1396
|
-
],
|
|
1393
|
+
model: "claude-unknown",
|
|
1394
|
+
queryResults: [scriptedMessages(resultMessage(0, "session_no_settings"))],
|
|
1397
1395
|
});
|
|
1398
|
-
await executor.execute({ threadId: "
|
|
1399
|
-
|
|
1400
|
-
expect(queryInputs[0]?.options.settings).toMatchObject({ outputStyle: "Concise" });
|
|
1401
|
-
// Constant across turns — the style is part of the system prompt, so a
|
|
1402
|
-
// per-turn value would re-write the cache prefix on every request.
|
|
1403
|
-
expect(queryInputs[1]?.options.settings).toMatchObject({ outputStyle: "Concise" });
|
|
1396
|
+
await executor.execute({ threadId: "thread_ns", turnId: "turn_ns", prompt: "Hi", history: history([]) }, activeControls());
|
|
1397
|
+
expect(queryInputs[0]?.options).not.toHaveProperty("settings");
|
|
1404
1398
|
});
|
|
1405
1399
|
it("runs exact publish commands with the prompt passed through verbatim", async () => {
|
|
1406
1400
|
const queryInputs = [];
|
|
@@ -1929,8 +1923,8 @@ describe("BridgeJobExecutor", () => {
|
|
|
1929
1923
|
expect(castingRequests[0]).toMatchObject({ avatarHandle: "elena" });
|
|
1930
1924
|
});
|
|
1931
1925
|
it("hands the agent's own casting call the face the user picked, instead of a second picker", async () => {
|
|
1932
|
-
//
|
|
1933
|
-
//
|
|
1926
|
+
// An agent may still call it right after `baker avatars create` (the tool now says the
|
|
1927
|
+
// picker opens by itself, but the call is tolerated). The auto-cast owed for that create used
|
|
1934
1928
|
// to run under its own tool-use id and then let the explicit call through unhandled —
|
|
1935
1929
|
// which returns the tool's `{ status: "declined" }` default. The agent was told the
|
|
1936
1930
|
// user had rejected all four faces moments after they picked one, and the remedy in
|
|
@@ -3096,29 +3090,6 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
|
|
|
3096
3090
|
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.2 });
|
|
3097
3091
|
expect(endInputAt.count).toBe(4);
|
|
3098
3092
|
});
|
|
3099
|
-
it("tells the session watchdog while a tool call or background task is running", async () => {
|
|
3100
|
-
const endInputAt = { count: -1 };
|
|
3101
|
-
const executor = createExecutor({
|
|
3102
|
-
turnEndSettleMs: 20,
|
|
3103
|
-
queryResults: [
|
|
3104
|
-
cliLikeQuery(endInputAt, toolUseAssistantMessage("Bash", { command: "baker canvas run x.canvas.json" }), {
|
|
3105
|
-
type: "user",
|
|
3106
|
-
session_id: "session_bg",
|
|
3107
|
-
message: { role: "user", content: [{ type: "tool_result", tool_use_id: "toolu_stream", content: "ok" }] },
|
|
3108
|
-
}, taskStarted("task_render"), resultMessage(0.1, "session_bg"), taskSettled("task_render"), resultMessage(0.2, "session_bg")),
|
|
3109
|
-
],
|
|
3110
|
-
});
|
|
3111
|
-
const reported = [];
|
|
3112
|
-
const controls = {
|
|
3113
|
-
...activeControls(),
|
|
3114
|
-
setWorkInFlight: (inFlight) => {
|
|
3115
|
-
if (reported.at(-1) !== inFlight)
|
|
3116
|
-
reported.push(inFlight);
|
|
3117
|
-
},
|
|
3118
|
-
};
|
|
3119
|
-
await executor.execute({ threadId: "thread_bg", turnId: "turn_bg", prompt: "Render", history: history([]) }, controls);
|
|
3120
|
-
expect(reported).toEqual([true, false, true, false]);
|
|
3121
|
-
});
|
|
3122
3093
|
it("ends the turn when the last task settles without waking the model into a second result", async () => {
|
|
3123
3094
|
// A settle is not guaranteed to produce another result. If only a result
|
|
3124
3095
|
// could arm the end, this shape would strand in `for await` until the
|
|
@@ -3134,20 +3105,6 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
|
|
|
3134
3105
|
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
|
|
3135
3106
|
expect(endInputAt.count).toBe(3);
|
|
3136
3107
|
});
|
|
3137
|
-
it("bills what a task spent after the last result when it settles without waking the model", async () => {
|
|
3138
|
-
// The last result reported $0.10; the task kept working after it. With no
|
|
3139
|
-
// second result, only the CLI's own running total can say what it cost.
|
|
3140
|
-
const endInputAt = { count: -1 };
|
|
3141
|
-
const query = cliLikeQuery(endInputAt, taskStarted("task_silent"), resultMessage(0.1, "session_bg"), taskSettled("task_silent"));
|
|
3142
|
-
const executor = createExecutor({
|
|
3143
|
-
turnEndSettleMs: 20,
|
|
3144
|
-
queryResults: [
|
|
3145
|
-
{ ...query, usage: () => Promise.resolve({ totalCostUsd: 0.35, modelUsage: {} }) },
|
|
3146
|
-
],
|
|
3147
|
-
});
|
|
3148
|
-
const outcome = await executor.execute({ threadId: "thread_silent", turnId: "turn_silent", prompt: "Fan out one producer", history: history([]) }, activeControls());
|
|
3149
|
-
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.35 });
|
|
3150
|
-
});
|
|
3151
3108
|
it("re-arms the settle window when a passive system event trails the result", async () => {
|
|
3152
3109
|
// Passive trailing events (files persisted, session state) must not cancel
|
|
3153
3110
|
// a scheduled end — only genuine model activity does. A cancel with no
|
|
@@ -3217,6 +3174,35 @@ describe("BridgeJobExecutor turn end with background tasks", () => {
|
|
|
3217
3174
|
expect(outcome).toMatchObject({ kind: "completed", costUsd: 0.1 });
|
|
3218
3175
|
expect(endInputAt.count).toBe(3);
|
|
3219
3176
|
});
|
|
3177
|
+
// The production shape: the model refused, the CLI answered with an error
|
|
3178
|
+
// result and then waited on stdin like after any other result. The error was
|
|
3179
|
+
// no longer thrown, and only a clean result ended input, so the turn sat idle
|
|
3180
|
+
// until the 30-minute session watchdog reported a lost connection.
|
|
3181
|
+
it("ends input after the error result of a refused turn", async () => {
|
|
3182
|
+
const endInputAt = { count: -1 };
|
|
3183
|
+
const refusal = {
|
|
3184
|
+
type: "system",
|
|
3185
|
+
subtype: "model_refusal_no_fallback",
|
|
3186
|
+
original_model: "claude-opus-5-5",
|
|
3187
|
+
api_refusal_category: "reasoning_extraction",
|
|
3188
|
+
session_id: "session_refused",
|
|
3189
|
+
};
|
|
3190
|
+
const erroredResult = {
|
|
3191
|
+
type: "result",
|
|
3192
|
+
subtype: "error_during_execution",
|
|
3193
|
+
is_error: true,
|
|
3194
|
+
errors: ["API Error: Claude Code is unable to respond to this request."],
|
|
3195
|
+
total_cost_usd: 0.01,
|
|
3196
|
+
session_id: "session_refused",
|
|
3197
|
+
};
|
|
3198
|
+
const executor = createExecutor({ queryResults: [cliLikeQuery(endInputAt, refusal, erroredResult)] });
|
|
3199
|
+
const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
|
|
3200
|
+
expect(outcome).toMatchObject({
|
|
3201
|
+
kind: "errored",
|
|
3202
|
+
errors: ["I couldn't help with that request. Try rephrasing what you need."],
|
|
3203
|
+
});
|
|
3204
|
+
expect(endInputAt.count).toBe(2);
|
|
3205
|
+
});
|
|
3220
3206
|
it("does not hold the turn open for ambient skip_transcript tasks", async () => {
|
|
3221
3207
|
const endInputAt = { count: -1 };
|
|
3222
3208
|
const executor = createExecutor({
|
|
@@ -3415,4 +3401,117 @@ describe("BridgeJobExecutor review mode", () => {
|
|
|
3415
3401
|
expect(outcome).toMatchObject({ kind: "completed", structuredOutput: findings });
|
|
3416
3402
|
});
|
|
3417
3403
|
});
|
|
3404
|
+
describe("BridgeJobExecutor model refusals", () => {
|
|
3405
|
+
function refusalNoFallback() {
|
|
3406
|
+
return {
|
|
3407
|
+
type: "system",
|
|
3408
|
+
subtype: "model_refusal_no_fallback",
|
|
3409
|
+
original_model: "claude-opus-5-5",
|
|
3410
|
+
request_id: "req_1",
|
|
3411
|
+
api_refusal_category: "cyber",
|
|
3412
|
+
content: "Claude Code is unable to respond to this request.",
|
|
3413
|
+
uuid: "refusal_notice",
|
|
3414
|
+
session_id: "session_refused",
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
it("ends a refused turn with a product-language error instead of a success", async () => {
|
|
3418
|
+
const executor = createExecutor({
|
|
3419
|
+
queryResults: [scriptedMessages(refusalNoFallback(), resultMessage(0.01, "session_refused"))],
|
|
3420
|
+
});
|
|
3421
|
+
const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
|
|
3422
|
+
expect(outcome).toMatchObject({
|
|
3423
|
+
kind: "errored",
|
|
3424
|
+
errors: ["I couldn't help with that request. Try rephrasing what you need."],
|
|
3425
|
+
});
|
|
3426
|
+
});
|
|
3427
|
+
it("records the refusal category for whoever reads the turn afterwards", async () => {
|
|
3428
|
+
const turnEvents = [];
|
|
3429
|
+
const executor = createExecutor({
|
|
3430
|
+
turnEvents,
|
|
3431
|
+
queryResults: [scriptedMessages(refusalNoFallback(), resultMessage(0.01, "session_refused"))],
|
|
3432
|
+
});
|
|
3433
|
+
await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
|
|
3434
|
+
expect(turnEvents).toContainEqual(expect.objectContaining({ event: "model_refusal", detail: expect.objectContaining({ category: "cyber" }) }));
|
|
3435
|
+
});
|
|
3436
|
+
// The CLI renders the refusal as an assistant error frame written for a
|
|
3437
|
+
// terminal user. The turn already ends on a product-language error, so that
|
|
3438
|
+
// frame never reaches the chat.
|
|
3439
|
+
it("does not relay the CLI's refusal frame to the chat", async () => {
|
|
3440
|
+
const relay = recordingRelay();
|
|
3441
|
+
const refusalFrame = {
|
|
3442
|
+
type: "assistant",
|
|
3443
|
+
uuid: "refusal_frame",
|
|
3444
|
+
message: {
|
|
3445
|
+
role: "assistant",
|
|
3446
|
+
content: [{ type: "text", text: "API Error: Claude Code is unable to respond to this request." }],
|
|
3447
|
+
stop_reason: "refusal",
|
|
3448
|
+
},
|
|
3449
|
+
};
|
|
3450
|
+
const executor = createExecutor({
|
|
3451
|
+
relay,
|
|
3452
|
+
queryResults: [scriptedMessages(refusalFrame, refusalNoFallback(), resultMessage(0.01, "session_refused"))],
|
|
3453
|
+
});
|
|
3454
|
+
await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
|
|
3455
|
+
expect(relay.assistantEvents).toEqual([]);
|
|
3456
|
+
});
|
|
3457
|
+
// A refused subagent is reported the same way, but the main agent gets the
|
|
3458
|
+
// failed task back and carries on — the turn is still its to finish.
|
|
3459
|
+
it("lets the turn complete when the main conversation carries on after a refusal", async () => {
|
|
3460
|
+
const executor = createExecutor({
|
|
3461
|
+
queryResults: [
|
|
3462
|
+
scriptedMessages(refusalNoFallback(), assistantMessage("assistant_after", "Done another way.", "session_refused"), resultMessage(0.01, "session_refused")),
|
|
3463
|
+
],
|
|
3464
|
+
});
|
|
3465
|
+
const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
|
|
3466
|
+
expect(outcome).toMatchObject({ kind: "completed" });
|
|
3467
|
+
});
|
|
3468
|
+
it("keeps the product-language error when the refused turn's result is an error", async () => {
|
|
3469
|
+
const erroredResult = {
|
|
3470
|
+
type: "result",
|
|
3471
|
+
subtype: "error_during_execution",
|
|
3472
|
+
is_error: true,
|
|
3473
|
+
errors: ["API Error: Claude Code is unable to respond to this request."],
|
|
3474
|
+
total_cost_usd: 0.01,
|
|
3475
|
+
session_id: "session_refused",
|
|
3476
|
+
};
|
|
3477
|
+
const executor = createExecutor({ queryResults: [scriptedMessages(refusalNoFallback(), erroredResult)] });
|
|
3478
|
+
const outcome = await executor.execute({ threadId: "thread_refused", turnId: "turn_refused", prompt: "Hi", history: history([]) }, activeControls());
|
|
3479
|
+
expect(outcome).toMatchObject({
|
|
3480
|
+
kind: "errored",
|
|
3481
|
+
errors: ["I couldn't help with that request. Try rephrasing what you need."],
|
|
3482
|
+
});
|
|
3483
|
+
});
|
|
3484
|
+
// A fallback model retried the refused request. What the refused leg already
|
|
3485
|
+
// posted is superseded, so it leaves the chat instead of sitting above the
|
|
3486
|
+
// answer that replaced it.
|
|
3487
|
+
it("retracts the messages a refusal fallback superseded", async () => {
|
|
3488
|
+
const relay = recordingRelay();
|
|
3489
|
+
const fallbackNotice = {
|
|
3490
|
+
type: "system",
|
|
3491
|
+
subtype: "model_refusal_fallback",
|
|
3492
|
+
trigger: "refusal",
|
|
3493
|
+
direction: "retry",
|
|
3494
|
+
scope: "session",
|
|
3495
|
+
original_model: "claude-opus-5-5",
|
|
3496
|
+
fallback_model: "claude-sonnet-5",
|
|
3497
|
+
request_id: "req_1",
|
|
3498
|
+
api_refusal_category: "cyber",
|
|
3499
|
+
retracted_message_uuids: ["refused_partial_1", "refused_partial_2"],
|
|
3500
|
+
content: "Retried on a different model.",
|
|
3501
|
+
uuid: "fallback_notice",
|
|
3502
|
+
session_id: "session_fallback",
|
|
3503
|
+
};
|
|
3504
|
+
const executor = createExecutor({
|
|
3505
|
+
relay,
|
|
3506
|
+
queryResults: [
|
|
3507
|
+
scriptedMessages(assistantMessage("refused_partial_1", "Sure, here is", "session_fallback"), fallbackNotice, assistantMessage("fallback_answer", "Here is the plan.", "session_fallback"), resultMessage(0.01, "session_fallback")),
|
|
3508
|
+
],
|
|
3509
|
+
});
|
|
3510
|
+
const outcome = await executor.execute({ threadId: "thread_fb", turnId: "turn_fb", prompt: "Hi", history: history([]) }, activeControls());
|
|
3511
|
+
expect(relay.retractedEvents).toEqual([
|
|
3512
|
+
{ target: { threadId: "thread_fb", turnId: "turn_fb" }, eventIds: ["refused_partial_1", "refused_partial_2"] },
|
|
3513
|
+
]);
|
|
3514
|
+
expect(outcome).toMatchObject({ kind: "completed" });
|
|
3515
|
+
});
|
|
3516
|
+
});
|
|
3418
3517
|
//# sourceMappingURL=bridge-job.test.js.map
|