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
29 changes: 20 additions & 9 deletions src/adapters/kiro-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@ function omittedToolCatalogNotice(kept: number, omitted: readonly OcxTool[], reg
return `[opencodex] Kiro's outbound catalog budget allows ${kept} of ${kept + omitted.length} client tools this turn. Omitted and unavailable this turn: ${summary}.`;
}

function boundedCatalogPriority(tool: OcxTool): number {
if (tool.loadedFromToolSearch) return 0;
if (tool.toolSearch) return 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve a catalog slot for the search gateway

When a continuation contains at least 48 definitions restored from prior tool_search_output items, every restored definition sorts ahead of the gateway and fills the count limit, so tool_search is omitted and the session can no longer discover additional deferred tools. Since src/responses/parser.ts accumulates restored definitions across the input history, this state can persist on subsequent turns; pin the gateway ahead of restored definitions or reserve one of the 48 slots for it.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

return 2;
}

export function convertKiroToolContext(
parsed: OcxParsedRequest,
registry: KiroToolNameRegistry = createKiroToolNameRegistry(),
Expand All @@ -181,9 +187,7 @@ export function convertKiroToolContext(
// Validate every listed name even when tool_choice:none emulates a tool-free turn.
for (const tool of tools) registry.alias(namespacedToolName(tool.namespace, tool.name));
const effectiveTools = parsed.options.toolChoice === "none" ? [] : tools;
const convertedTools: unknown[] = [];
let omittedAt = effectiveTools.length;
for (const [index, tool] of effectiveTools.entries()) {
const convertedEntries = effectiveTools.map((tool, index) => {
const description = tool.description || `Tool: ${tool.name}`;
// Send the full namespaced wire name (e.g. mcp__chrome-devtools__navigate_page) so Kiro echoes
// it back; the bridge's toolNsMap is keyed by this name and restores the MCP namespace Codex
Expand All @@ -198,19 +202,26 @@ export function convertKiroToolContext(
inputSchema: { json: ensureRootObjectType(sanitizeKiroSchema(tool.parameters ?? {})) },
},
};
// Preserve declaration order and only omit a suffix. Ranking tools would make a catalog change
// silently alter which capability disappears; this deterministic policy is paired with a
// model-visible omission notice so unavailable tools are explicit rather than assumed absent.
return { tool, index, converted };
});
const exceedsBudget = convertedEntries.length > MAX_KIRO_TOOL_COUNT
|| serializedToolCatalogBytes(convertedEntries.map(entry => entry.converted)) > MAX_KIRO_TOOL_CATALOG_BYTES;
const candidates = exceedsBudget
? convertedEntries.toSorted((a, b) => boundedCatalogPriority(a.tool) - boundedCatalogPriority(b.tool) || a.index - b.index)
: convertedEntries;
const convertedTools: unknown[] = [];
let omittedAt = candidates.length;
for (const [index, entry] of candidates.entries()) {
if (
convertedTools.length >= MAX_KIRO_TOOL_COUNT
|| serializedToolCatalogBytes([...convertedTools, converted]) > MAX_KIRO_TOOL_CATALOG_BYTES
|| serializedToolCatalogBytes([...convertedTools, entry.converted]) > MAX_KIRO_TOOL_CATALOG_BYTES
) {
omittedAt = index;
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Continue after a priority tool exceeds the remaining byte budget

When multiple restored tools have large schemas, one may fit while the next exceeds the remaining 96 KiB budget; this break then drops every later candidate, including the small tool_search gateway and ordinary tools that would still fit. An individually oversized restored tool similarly reduces the advertised catalog to zero. Continue scanning after a byte-budget miss and track omitted entries individually rather than treating the first miss as the start of an omitted suffix.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

}
convertedTools.push(converted);
convertedTools.push(entry.converted);
}
const omittedTools = effectiveTools.slice(omittedAt);
const omittedTools = candidates.slice(omittedAt).map(entry => entry.tool);
return {
tools: convertedTools,
systemAdditions: omittedTools.length > 0 ? [omittedToolCatalogNotice(convertedTools.length, omittedTools, registry)] : [],
Expand Down
35 changes: 35 additions & 0 deletions tests/kiro-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,41 @@ describe("kiro adapter — buildRequest", () => {
expect(current.content).toContain("Omitted and unavailable this turn");
});

test("large catalogs prioritize tool-search discoveries and the search gateway", async () => {
const ordinaryTools = Array.from({ length: MAX_KIRO_TOOL_COUNT + 20 }, (_, index) => ({
name: `ordinary_tool_${String(index).padStart(3, "0")}`,
description: `Ordinary tool ${index}`,
parameters: { type: "object" },
}));
const searchGateway = {
name: "tool_search",
description: "Search deferred tools",
parameters: { type: "object" },
toolSearch: true,
};
const loadedTool = {
name: "codex_app__send_message_to_thread",
description: "Send a message to a task",
parameters: { type: "object" },
loadedFromToolSearch: true,
};
const tools = [...ordinaryTools, searchGateway, loadedTool];

const current = JSON.parse((await createKiroAdapter(provider).buildRequest(
parsedWith([{ role: "user", content: "hi" }], tools),
)).body).conversationState.currentMessage.userInputMessage;
const ordinary = current.userInputMessageContext.tools.slice(0, -1);
const names = ordinary.map((tool: { toolSpecification: { name: string } }) => tool.toolSpecification.name);
const omissionNotice = current.content.split("\n\n", 1)[0];

expect(ordinary).toHaveLength(MAX_KIRO_TOOL_COUNT);
expect(names.slice(0, 2)).toEqual([loadedTool.name, searchGateway.name]);
expect(names.slice(2)).toEqual(ordinaryTools.slice(0, MAX_KIRO_TOOL_COUNT - 2).map(tool => tool.name));
expect(omissionNotice).toContain("ordinary_tool_046");
expect(omissionNotice).not.toContain(loadedTool.name);
expect(omissionNotice).not.toContain(searchGateway.name);
});

test("large catalogs retain the declared prefix within Kiro's serialized byte budget", async () => {
// Top-level descriptions stay small, so existing description truncation cannot make this pass.
// The repeated schema descriptions instead make the aggregate converted catalog exceed 96 KiB.
Expand Down
Loading