Compare commits

...
Author SHA1 Message Date
Aiden Cline 2342683aa5 feat(core): add pinned Code Mode tools 2026-07-24 16:14:15 -05:00
9 changed files with 140 additions and 25 deletions
+20 -13
View File
@@ -6,6 +6,7 @@ export const Entry = Schema.Struct({
path: Schema.String,
description: Schema.String,
signature: Schema.String,
pinned: Schema.optionalKey(Schema.Literal(true)),
})
export type Entry = typeof Entry.Type
@@ -31,8 +32,8 @@ const DESCRIPTION_LIMIT = 120
const CHARACTERS_PER_TOKEN = 4
const INLINE_BUDGET = 2_000
// Keep every namespace searchable, then select full listings one per namespace per round,
// considering shorter listings first until the inline budget is exhausted.
// Keep every namespace searchable and every pinned listing visible, then select additional
// listings one per namespace per round until the remaining inline budget is exhausted.
export function summarize(entries: ReadonlyArray<Entry>, budget = INLINE_BUDGET): Summary {
const namespaces = [...Map.groupBy(entries, (entry) => entry.path.split(".", 1)[0] ?? entry.path)]
.sort(([left], [right]) => {
@@ -45,44 +46,46 @@ export function summarize(entries: ReadonlyArray<Entry>, budget = INLINE_BUDGET)
.map((entry) => {
const firstLine = entry.description.split("\n", 1)[0]?.trim() ?? ""
const description =
firstLine.length > DESCRIPTION_LIMIT
? firstLine.slice(0, DESCRIPTION_LIMIT - 3) + "..."
: firstLine
firstLine.length > DESCRIPTION_LIMIT ? firstLine.slice(0, DESCRIPTION_LIMIT - 3) + "..." : firstLine
const suffix = description.length === 0 ? "" : ` // ${description}`
return { path: entry.path, line: ` - ${entry.signature}${suffix}` }
return { path: entry.path, line: ` - ${entry.signature}${suffix}`, pinned: entry.pinned === true }
})
.toSorted((left, right) => {
if (left.path < right.path) return -1
if (left.path > right.path) return 1
return 0
})
const pinned = listings.filter((listing) => listing.pinned)
return {
name,
listings,
selectionOrder: rankListings(listings),
selectedListings: new Set<typeof Listing.Type>(),
selectionOrder: rankListings(listings.filter((listing) => !listing.pinned)),
selectedListings: new Set<typeof Listing.Type>(pinned),
pinnedCost: pinned.reduce((total, listing) => total + listingCost(listing), 0),
}
})
const active = new Set(namespaces)
let remaining = budget
let remaining = Math.max(0, budget - namespaces.reduce((total, namespace) => total + namespace.pinnedCost, 0))
while (active.size > 0) {
for (const namespace of active) {
const candidate = namespace.selectionOrder[namespace.selectedListings.size]
const candidate = namespace.selectionOrder.shift()
if (!candidate || candidate.cost > remaining) {
active.delete(namespace)
continue
}
namespace.selectedListings.add(candidate.listing)
remaining -= candidate.cost
if (namespace.selectedListings.size === namespace.selectionOrder.length) active.delete(namespace)
if (namespace.selectionOrder.length === 0) active.delete(namespace)
}
}
const namespaceSummaries = namespaces.map((namespace) => ({
name: namespace.name,
count: namespace.listings.length,
entries: namespace.listings.filter((listing) => namespace.selectedListings.has(listing)),
entries: namespace.listings
.filter((listing) => namespace.selectedListings.has(listing))
.map((listing) => ({ path: listing.path, line: listing.line })),
}))
return {
total: entries.length,
@@ -91,9 +94,13 @@ export function summarize(entries: ReadonlyArray<Entry>, budget = INLINE_BUDGET)
}
}
function listingCost(listing: typeof Listing.Type) {
return Math.round(listing.line.length / CHARACTERS_PER_TOKEN)
}
function rankListings(listings: ReadonlyArray<typeof Listing.Type>) {
return listings
.map((listing) => ({ listing, cost: Math.round(listing.line.length / CHARACTERS_PER_TOKEN) }))
.map((listing) => ({ listing, cost: listingCost(listing) }))
.toSorted((left, right) => {
if (left.cost !== right.cost) return left.cost - right.cost
if (left.listing.path < right.listing.path) return -1
+13 -3
View File
@@ -129,7 +129,14 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
}
export const catalog = (registrations: ReadonlyMap<string, Registration>) => {
return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable"))).catalog()
const pinned = new Set(
Array.from(registrations.values()).flatMap((registration) =>
registration.pinned === true ? [registrationPath(registration)] : [],
),
)
return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable")))
.catalog()
.map((entry) => (pinned.has(entry.path) ? { ...entry, pinned: true as const } : entry))
}
function runtime(
@@ -140,8 +147,7 @@ function runtime(
const tools: Record<string, Tool.Tool<never>> = {}
for (const [name, registration] of registrations) {
const child = toLLMDefinition(name, registration.tool)
const path =
registration.namespace === undefined ? registration.name : `${registration.namespace}.${registration.name}`
const path = registrationPath(registration)
tools[path] = Tool.make({
description: child.description,
input: child.inputSchema,
@@ -152,6 +158,10 @@ function runtime(
return CodeMode.make<typeof tools>({ tools, ...hooks })
}
function registrationPath(registration: Registration) {
return registration.namespace === undefined ? registration.name : `${registration.namespace}.${registration.name}`
}
// Tool inputs arrive as parsed JSON, so the JSON value cast is a boundary fact.
function displayInput(input: unknown): Record<string, typeof Schema.Json.Type> | undefined {
if (input === null || input === undefined) return
+7
View File
@@ -245,6 +245,13 @@ const registryLayer = Layer.effect(
}),
)
const codemode = options?.codemode ?? true
if (!codemode && options?.pinned === true)
return yield* Effect.fail(
new Tool.RegistrationError({
name: entries[0]?.key ?? "",
message: "Pinned tools must use Code Mode",
}),
)
const reserved = codemode ? undefined : entries.find((entry) => entry.key === "execute")
if (reserved)
return yield* Effect.fail(
+51 -8
View File
@@ -10,14 +10,17 @@ describe("CodeMode", () => {
Effect.gen(function* () {
const codeMode = yield* CodeMode.Service
yield* codeMode.register(
Tool.registrationEntries({
echo: Tool.make({
description: "Echo text",
input: Schema.Struct({ text: Schema.String }),
output: Schema.String,
execute: ({ text }) => Effect.succeed({ output: text }),
}),
}),
Tool.registrationEntries(
{
echo: Tool.make({
description: "Echo text",
input: Schema.Struct({ text: Schema.String }),
output: Schema.String,
execute: ({ text }) => Effect.succeed({ output: text }),
}),
},
{ pinned: true },
),
)
const materialized = yield* codeMode.materialize()
@@ -27,6 +30,46 @@ describe("CodeMode", () => {
path: "echo",
description: "Echo text",
signature: "tools.echo(input: {\n text: string,\n}): Promise<string>",
pinned: true,
},
])
}).pipe(Effect.scoped, Effect.provide(AppNodeBuilder.build(CodeMode.node))),
)
it.effect("omits denied pinned registrations from the catalog", () =>
Effect.gen(function* () {
const codeMode = yield* CodeMode.Service
yield* codeMode.register(
Tool.registrationEntries({
visible: Tool.make({
description: "Visible tool",
input: Schema.Struct({}),
output: Schema.String,
execute: () => Effect.succeed({ output: "visible" }),
}),
}),
)
yield* codeMode.register(
Tool.registrationEntries(
{
hidden: Tool.make({
description: "Hidden tool",
input: Schema.Struct({}),
output: Schema.String,
execute: () => Effect.succeed({ output: "hidden" }),
}),
},
{ permission: "hidden", pinned: true },
),
)
const materialized = yield* codeMode.materialize([{ action: "hidden", resource: "*", effect: "deny" }])
expect(materialized.tool).toBeDefined()
expect(materialized.catalog).toEqual([
{
path: "visible",
description: "Visible tool",
signature: "tools.visible(input: {}): Promise<string>",
},
])
}).pipe(Effect.scoped, Effect.provide(AppNodeBuilder.build(CodeMode.node))),
@@ -57,6 +57,23 @@ describe("CodeModeCatalog.summarize", () => {
expect(description).toHaveLength(120)
expect(description).toEndWith("...")
})
test("keeps pinned signatures when no unpinned listing fits", () => {
const pinned = { ...entry("files.read", "Read a file"), pinned: true as const }
const catalog = CodeModeCatalog.summarize([entry("files.write", "Write a file"), pinned], 0)
expect(catalog).toEqual({
total: 2,
shown: 1,
namespaces: [
{
name: "files",
count: 2,
entries: [{ path: pinned.path, line: ` - ${pinned.signature} // Read a file` }],
},
],
})
})
})
describe("CodeModeInstructions.render", () => {
@@ -121,6 +121,17 @@ describe("ToolRegistry", () => {
}),
)
it.effect("rejects pinned tools outside Code Mode", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
const error = yield* service.register({ echo: make() }, { codemode: false, pinned: true }).pipe(Effect.flip)
expect(error).toBeInstanceOf(Tool.RegistrationError)
expect(error.message).toBe("Pinned tools must use Code Mode")
expect((yield* service.snapshot()).definitions).toEqual([])
}),
)
it.effect("canonicalizes effective definitions and keeps Code Mode last", () =>
Effect.gen(function* () {
const service = yield* ToolRegistry.Service
+4 -1
View File
@@ -326,12 +326,15 @@ Unsupported characters in tool names are normalized to underscores. Namespace
segments must begin with a letter, contain at most 64 letters, digits,
underscores, or hyphens, and are joined with dots. Pass the optional third
argument to `tools.add` to configure the registration with
`{ namespace, codemode }`:
`{ namespace, codemode, pinned }`:
- `namespace` prefixes and groups the exposed tool name.
- `codemode` defaults to `true` and makes the tool available through the
`execute` CodeMode tool. Set `codemode: false` to expose it directly to the
provider.
- `pinned: true` keeps the tool's complete signature visible when the Code Mode
catalog is compact. Pinned tools must use Code Mode and cannot set
`codemode: false`.
The executor receives a second context argument containing `sessionID`,
`agent`, `messageID`, `callID`, and `progress`. A tool with `output`
@@ -129,6 +129,8 @@ export interface RegisterOptions {
readonly namespace?: string
/** Defaults to true. False exposes the tool directly to the provider. */
readonly codemode?: boolean
/** Keeps the complete signature visible when the Code Mode catalog is compact. */
readonly pinned?: boolean
/** Permission action used for whole-tool visibility filtering. */
readonly permission?: string
}
@@ -138,6 +140,7 @@ export interface Registration {
readonly name: string
readonly namespace?: string
readonly permission: string
readonly pinned?: boolean
}
export const validateName = (name: string) =>
@@ -159,6 +162,7 @@ export const registrationEntries = (
namespace: options?.namespace,
tool,
permission: options?.permission ?? key,
...(options?.pinned === true ? { pinned: true } : {}),
}
})
+13
View File
@@ -125,3 +125,16 @@ test("raw JSON schemas are render-only and omitted output means model-only", asy
})
expect(await Effect.runPromise(Tool.decodeInput(tool.input, { value: 1 }))).toEqual({ value: 1 })
})
test("registration metadata omits disabled pinning", () => {
const tool = Tool.make({
description: "Echo text",
input: Schema.Struct({ text: Schema.String }),
output: Schema.String,
execute: ({ text }) => Effect.succeed({ output: text }),
})
const registration = Tool.registrationEntries({ echo: tool })
expect(Tool.registrationEntries({ echo: tool }, { pinned: false })).toEqual(registration)
expect(Tool.registrationEntries({ echo: tool }, { pinned: true })).toEqual([{ ...registration[0], pinned: true }])
})