Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/interaction-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@corbits/interaction-tools",
"private": true,
"description": "The ask_user tool: an @intx/agent bundle that poses an interview question in-thread as a question block and parks the turn on a message_response gate until the user answers",
"version": "0.0.4",
"version": "0.0.5",
"license": "LGPL-2.1-or-later",
"type": "module",
"exports": {
Expand Down
103 changes: 95 additions & 8 deletions packages/interaction-tools/src/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,27 +65,114 @@ test("a 404 from the route surfaces as NoOwnChannelError", async () => {
const fetchImpl = (async () =>
new Response(
JSON.stringify({
error: { code: "not_found", message: "no channel" },
error: {
code: "not_found",
userMessage: "no channel",
refId: "ref_test",
},
}),
{ status: 404 },
)) as unknown as typeof fetch;

await expect(
postQuestion(testConfig(fetchImpl), {
try {
await postQuestion(testConfig(fetchImpl), {
question: "Q?",
options: ["a", "b"],
}),
).rejects.toBeInstanceOf(NoOwnChannelError);
});
throw new Error("expected NoOwnChannelError");
} catch (error) {
expect(error).toBeInstanceOf(NoOwnChannelError);
expect((error as Error).message).toBe("no channel");
}
});

test("a non-ok, non-404 response throws a plain error", async () => {
test("a non-ok, non-404 response surfaces the envelope userMessage", async () => {
const fetchImpl = (async () =>
new Response("boom", { status: 500 })) as unknown as typeof fetch;
new Response(
JSON.stringify({
error: {
code: "internal_error",
userMessage: "the hub could not post the question",
refId: "ref_test",
},
}),
{ status: 500 },
)) as unknown as typeof fetch;

await expect(
postQuestion(testConfig(fetchImpl), {
question: "Q?",
options: ["a", "b"],
}),
).rejects.toThrow();
).rejects.toThrow("the hub could not post the question");
});

test("reads userMessage even when a legacy message field is also present", async () => {
const fetchImpl = (async () =>
new Response(
JSON.stringify({
error: {
code: "internal_error",
message: "legacy",
userMessage: "canonical",
refId: "ref_test",
},
}),
{ status: 500 },
)) as unknown as typeof fetch;

try {
await postQuestion(testConfig(fetchImpl), {
question: "Q?",
options: ["a", "b"],
});
throw new Error("expected postQuestion to throw");
} catch (error) {
expect((error as Error).message).toBe(
"Posting the question failed: canonical",
);
}
});

test("a 404 envelope with only error.message falls back, never the legacy field", async () => {
const fetchImpl = (async () =>
new Response(
JSON.stringify({
error: { code: "not_found", message: "legacy" },
}),
{ status: 404 },
)) as unknown as typeof fetch;

try {
await postQuestion(testConfig(fetchImpl), {
question: "Q?",
options: ["a", "b"],
});
throw new Error("expected NoOwnChannelError");
} catch (error) {
expect(error).toBeInstanceOf(NoOwnChannelError);
expect((error as Error).message).toBe(
"The caller has no channel of its own to post into",
);
}
});

test("a non-ok, non-envelope response falls back to status text", async () => {
const fetchImpl = (async () =>
new Response("boom", {
status: 500,
statusText: "Internal Server Error",
})) as unknown as typeof fetch;

try {
await postQuestion(testConfig(fetchImpl), {
question: "Q?",
options: ["a", "b"],
});
throw new Error("expected postQuestion to throw");
} catch (error) {
expect((error as Error).message).toBe(
"Posting the question failed: 500 Internal Server Error",
);
}
});
18 changes: 14 additions & 4 deletions packages/interaction-tools/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,23 @@ function authHeaders(config: AskUserClientConfig): Record<string, string> {
};
}

/** Pulls `error.userMessage` out of the canonical hub envelope
* (`{error: {code, userMessage, refId}}`), if `body` matches that shape —
* same shape `@corbits/agent-directory-tools`' client reads. */
function errorMessageFrom(body: unknown): string | undefined {
if (body === null || typeof body !== "object" || !("error" in body)) {
return undefined;
}
const error = (body as { error: unknown }).error;
if (error === null || typeof error !== "object" || !("message" in error)) {
if (
error === null ||
typeof error !== "object" ||
!("userMessage" in error)
) {
return undefined;
}
const message = (error as { message: unknown }).message;
return typeof message === "string" ? message : undefined;
const userMessage = (error as { userMessage: unknown }).userMessage;
return typeof userMessage === "string" ? userMessage : undefined;
}

async function readErrorMessage(
Expand Down Expand Up @@ -112,7 +119,10 @@ export async function postQuestion(
}
if (!response.ok) {
throw new Error(
`Posting the question failed: ${response.status} ${response.statusText}`,
`Posting the question failed: ${await readErrorMessage(
response,
`${response.status} ${response.statusText}`,
)}`,
);
}
const body: unknown = await response.json();
Expand Down
10 changes: 8 additions & 2 deletions packages/interaction-tools/src/tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,13 @@ test("ask_user rejects fewer than 2 options before ever posting", async () => {
test("ask_user surfaces a no-own-channel failure as a blocked call, not a throw", async () => {
const fetchImpl = (async () =>
new Response(
JSON.stringify({ error: { code: "not_found", message: "no channel" } }),
JSON.stringify({
error: {
code: "not_found",
userMessage: "no channel found",
refId: "ref_test",
},
}),
{ status: 404 },
)) as unknown as typeof fetch;

Expand All @@ -123,7 +129,7 @@ test("ask_user surfaces a no-own-channel failure as a blocked call, not a throw"
if (decision.type !== "block") {
throw new Error(`expected a block decision, got ${decision.type}`);
}
expect(decision.reason).toContain("no channel");
expect(decision.reason).toBe("no channel found");
});

test("a call for another tool name is allowed through unsuspended", async () => {
Expand Down
2 changes: 1 addition & 1 deletion workflows/assistant/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const ASSISTANT_TOOL_PACKAGE_PINS: readonly ToolPackagePin[] = [
{ name: "@corbits/catalog-tools", version: "0.0.2" },
{ name: "@corbits/skills-tools", version: "0.0.6" },
{ name: "@corbits/mcp-tools", version: "0.0.11" },
{ name: "@corbits/interaction-tools", version: "0.0.4" },
{ name: "@corbits/interaction-tools", version: "0.0.5" },
{ name: "@corbits/manus-tools", version: "0.0.11" },
{ name: "@corbits/workflow-authoring-tools", version: "0.0.4" },
];
Expand Down
Loading