1- import type { OcxParsedRequest } from "../types" ;
1+ import type { OcxParsedRequest , OcxTool } from "../types" ;
22import { namespacedToolName } from "../types" ;
33import { normalizeKiroModelId } from "../providers/kiro-models" ;
44import { createKiroToolNameRegistry , type KiroToolNameRegistry } from "./kiro-wire" ;
55
66const MAX_KIRO_TOOL_DESCRIPTION_UNVERIFIED = 1024 ;
77const MAX_KIRO_TOOL_DESCRIPTION_GPT_56_SOL = 9_216 ;
8+ // Issue #719's successful Kiro probe used 49 outbound tools at about 108 KiB. Leave one of those
9+ // slots for Kiro's private completion tool and headroom for the enclosing request/message fields.
10+ export const MAX_KIRO_TOOL_COUNT = 48 ;
11+ export const MAX_KIRO_TOOL_CATALOG_BYTES = 96_000 ;
12+ const textEncoder = new TextEncoder ( ) ;
813
914// JSON Schema validation/annotation keywords that Kiro's runtimeservice tool-spec validator
1015// rejects ("ValidationException: Invalid tool use format."). Codex's built-in tools omit these,
@@ -147,6 +152,17 @@ function truncateDescription(description: string, limit: number): string {
147152 return `${ description . slice ( 0 , limit - 1 ) } …` ;
148153}
149154
155+ function serializedToolCatalogBytes ( tools : readonly unknown [ ] ) : number {
156+ return textEncoder . encode ( JSON . stringify ( tools ) ) . byteLength ;
157+ }
158+
159+ function omittedToolCatalogNotice ( kept : number , omitted : readonly OcxTool [ ] , registry : KiroToolNameRegistry ) : string {
160+ const names = omitted . slice ( 0 , 12 ) . map ( tool => registry . alias ( namespacedToolName ( tool . namespace , tool . name ) ) ) ;
161+ const remainder = omitted . length - names . length ;
162+ const summary = `${ names . join ( ", " ) } ${ remainder > 0 ? `, and ${ remainder } more` : "" } ` ;
163+ return `[opencodex] Kiro's outbound catalog budget allows ${ kept } of ${ kept + omitted . length } client tools this turn. Omitted and unavailable this turn: ${ summary } .` ;
164+ }
165+
150166export function convertKiroToolContext (
151167 parsed : OcxParsedRequest ,
152168 registry : KiroToolNameRegistry = createKiroToolNameRegistry ( ) ,
@@ -156,24 +172,39 @@ export function convertKiroToolContext(
156172 // Validate every listed name even when tool_choice:none emulates a tool-free turn.
157173 for ( const tool of tools ) registry . alias ( namespacedToolName ( tool . namespace , tool . name ) ) ;
158174 const effectiveTools = parsed . options . toolChoice === "none" ? [ ] : tools ;
175+ const convertedTools : unknown [ ] = [ ] ;
176+ let omittedAt = effectiveTools . length ;
177+ for ( const [ index , tool ] of effectiveTools . entries ( ) ) {
178+ const description = tool . description || `Tool: ${ tool . name } ` ;
179+ // Send the full namespaced wire name (e.g. mcp__chrome-devtools__navigate_page) so Kiro echoes
180+ // it back; the bridge's toolNsMap is keyed by this name and restores the MCP namespace Codex
181+ // routes by. Kiro's runtimeservice rejects names with spaces or >64 chars, so normalize to a
182+ // safe form and remember the mapping; the response parser restores the original wire name.
183+ const wireName = namespacedToolName ( tool . namespace , tool . name ) ;
184+ const toolName = registry . alias ( wireName ) ;
185+ const converted = {
186+ toolSpecification : {
187+ name : toolName ,
188+ description : truncateDescription ( description , descriptionLimit ) ,
189+ inputSchema : { json : ensureRootObjectType ( sanitizeKiroSchema ( tool . parameters ?? { } ) ) } ,
190+ } ,
191+ } ;
192+ // Preserve declaration order and only omit a suffix. Ranking tools would make a catalog change
193+ // silently alter which capability disappears; this deterministic policy is paired with a
194+ // model-visible omission notice so unavailable tools are explicit rather than assumed absent.
195+ if (
196+ convertedTools . length >= MAX_KIRO_TOOL_COUNT
197+ || serializedToolCatalogBytes ( [ ...convertedTools , converted ] ) > MAX_KIRO_TOOL_CATALOG_BYTES
198+ ) {
199+ omittedAt = index ;
200+ break ;
201+ }
202+ convertedTools . push ( converted ) ;
203+ }
204+ const omittedTools = effectiveTools . slice ( omittedAt ) ;
159205 return {
160- tools : effectiveTools . map ( t => {
161- const description = t . description || `Tool: ${ t . name } ` ;
162- // Send the full namespaced wire name (e.g. mcp__chrome-devtools__navigate_page) so Kiro echoes
163- // it back; the bridge's toolNsMap is keyed by this name and restores the MCP namespace Codex
164- // routes by. Kiro's runtimeservice rejects names with spaces or >64 chars, so normalize to a
165- // safe form and remember the mapping; the response parser restores the original wire name.
166- const wireName = namespacedToolName ( t . namespace , t . name ) ;
167- const toolName = registry . alias ( wireName ) ;
168- return {
169- toolSpecification : {
170- name : toolName ,
171- description : truncateDescription ( description , descriptionLimit ) ,
172- inputSchema : { json : ensureRootObjectType ( sanitizeKiroSchema ( t . parameters ?? { } ) ) } ,
173- } ,
174- } ;
175- } ) ,
176- systemAdditions : [ ] ,
206+ tools : convertedTools ,
207+ systemAdditions : omittedTools . length > 0 ? [ omittedToolCatalogNotice ( convertedTools . length , omittedTools , registry ) ] : [ ] ,
177208 nameMap : registry . nameMap ,
178209 registry,
179210 } ;
0 commit comments