mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-30 13:36:18 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7a0b717e7 | ||
|
|
04e0bd3b3c | ||
|
|
a79639e7a9 | ||
|
|
5d4c4d0ede | ||
|
|
b0075f63b4 |
@@ -7,6 +7,7 @@ export const Entry = Schema.Struct({
|
||||
description: Schema.String,
|
||||
signature: Schema.String,
|
||||
pinned: Schema.optionalKey(Schema.Boolean),
|
||||
namespaceInstructions: Schema.optionalKey(Schema.String),
|
||||
})
|
||||
export type Entry = typeof Entry.Type
|
||||
|
||||
@@ -19,6 +20,7 @@ const Namespace = Schema.Struct({
|
||||
name: Schema.String,
|
||||
count: Schema.Number,
|
||||
entries: Schema.Array(Listing),
|
||||
instructions: Schema.optionalKey(Schema.String),
|
||||
})
|
||||
|
||||
export const Summary = Schema.Struct({
|
||||
@@ -42,6 +44,9 @@ export function summarize(entries: ReadonlyArray<Entry>, budget = INLINE_BUDGET)
|
||||
return 0
|
||||
})
|
||||
.map(([name, namespaceEntries]) => {
|
||||
const instructions = namespaceEntries.find(
|
||||
(entry) => entry.namespaceInstructions !== undefined,
|
||||
)?.namespaceInstructions
|
||||
const listings = namespaceEntries
|
||||
.map((entry) => {
|
||||
const firstLine = entry.description.split("\n", 1)[0]?.trim() ?? ""
|
||||
@@ -64,6 +69,7 @@ export function summarize(entries: ReadonlyArray<Entry>, budget = INLINE_BUDGET)
|
||||
)
|
||||
return {
|
||||
name,
|
||||
...(instructions === undefined ? {} : { instructions }),
|
||||
listings,
|
||||
selectionOrder: ranked.filter((candidate) => !pinned.has(candidate.listing)),
|
||||
selectedListings: pinned,
|
||||
@@ -75,8 +81,18 @@ export function summarize(entries: ReadonlyArray<Entry>, budget = INLINE_BUDGET)
|
||||
let remaining =
|
||||
budget -
|
||||
namespaces
|
||||
.flatMap((namespace) => namespace.listings.filter((listing) => namespace.selectedListings.has(listing)))
|
||||
.reduce((total, listing) => total + Math.round(listing.line.length / CHARACTERS_PER_TOKEN), 0)
|
||||
.flatMap((namespace) => {
|
||||
const listings = namespace.listings
|
||||
.filter((listing) => namespace.selectedListings.has(listing))
|
||||
.map((listing) => listing.line)
|
||||
if (namespace.instructions === undefined) return listings
|
||||
const instructions = namespace.instructions
|
||||
.split("\n")
|
||||
.map((line) => ` ${line}`)
|
||||
.join("\n")
|
||||
return [instructions, ...listings]
|
||||
})
|
||||
.reduce((total, text) => total + Math.round(text.length / CHARACTERS_PER_TOKEN), 0)
|
||||
while (active.size > 0) {
|
||||
for (const namespace of active) {
|
||||
const candidate = namespace.selectionOrder[namespace.selectionIndex]
|
||||
@@ -95,6 +111,7 @@ export function summarize(entries: ReadonlyArray<Entry>, budget = INLINE_BUDGET)
|
||||
name: namespace.name,
|
||||
count: namespace.listings.length,
|
||||
entries: namespace.listings.filter((listing) => namespace.selectedListings.has(listing)),
|
||||
...(namespace.instructions === undefined ? {} : { instructions: namespace.instructions }),
|
||||
}))
|
||||
return {
|
||||
total: entries.length,
|
||||
|
||||
@@ -30,7 +30,11 @@ export function render(catalog: CodeModeCatalog.Summary) {
|
||||
: namespace.entries.length === 0
|
||||
? `${count}, none shown`
|
||||
: `${count}, ${namespace.entries.length} shown`
|
||||
return [`- ${namespace.name} (${label})`, ...namespace.entries.map((entry) => entry.line)]
|
||||
return [
|
||||
`- ${namespace.name} (${label})`,
|
||||
...(namespace.instructions?.split("\n").map((line) => ` ${line}`) ?? []),
|
||||
...namespace.entries.map((entry) => entry.line),
|
||||
]
|
||||
})
|
||||
|
||||
return `${prompt(catalog.shown < catalog.total)}
|
||||
@@ -47,6 +51,19 @@ ${render(current)}`
|
||||
const currentComplete = current.shown === current.total
|
||||
if (previousComplete !== currentComplete) return replacement
|
||||
|
||||
const guidance = Instructions.diffByKey(
|
||||
previous.namespaces,
|
||||
current.namespaces,
|
||||
(namespace) => namespace.name,
|
||||
(before, after) => before.instructions !== after.instructions,
|
||||
)
|
||||
if (
|
||||
guidance.changed.length > 0 ||
|
||||
guidance.added.some((namespace) => namespace.instructions !== undefined) ||
|
||||
guidance.removed.some((namespace) => namespace.instructions !== undefined)
|
||||
)
|
||||
return replacement
|
||||
|
||||
const diff = Instructions.diffByKey(
|
||||
previous.namespaces.flatMap((namespace) => namespace.entries),
|
||||
current.namespaces.flatMap((namespace) => namespace.entries),
|
||||
|
||||
@@ -134,7 +134,7 @@ export const create = (
|
||||
} satisfies Info
|
||||
}
|
||||
|
||||
export const catalog = (registrations: ReadonlyMap<string, Info>) => {
|
||||
export const catalog = (registrations: ReadonlyMap<string, Info>, instructions: ReadonlyMap<string, string>) => {
|
||||
const pinned = new Set(
|
||||
Array.from(registrations.values())
|
||||
.filter((registration) => registration.options?.pinned === true)
|
||||
@@ -142,7 +142,14 @@ export const catalog = (registrations: ReadonlyMap<string, Info>) => {
|
||||
)
|
||||
return runtime(registrations, () => Effect.fail(toolError("Execute context is unavailable")))
|
||||
.catalog()
|
||||
.map((entry) => ({ ...entry, pinned: pinned.has(entry.path) }))
|
||||
.map((entry) => {
|
||||
const guidance = instructions.get(entry.path.split(".", 1)[0] ?? entry.path)
|
||||
return {
|
||||
...entry,
|
||||
pinned: pinned.has(entry.path),
|
||||
...(guidance === undefined ? {} : { namespaceInstructions: guidance }),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function runtime(
|
||||
|
||||
@@ -145,6 +145,20 @@ const layer = Layer.effect(
|
||||
validateNamespace,
|
||||
{ discard: true },
|
||||
)
|
||||
const invalid = tools.find((tool) => {
|
||||
const direct = tool.options?.codemode === false
|
||||
return tool.options?.namespaceInstructions !== undefined && (tool.options.namespace === undefined || direct)
|
||||
})
|
||||
if (invalid)
|
||||
return yield* Effect.fail(
|
||||
new RegistrationError({
|
||||
name: invalid.name,
|
||||
message:
|
||||
invalid.options?.namespace === undefined
|
||||
? `Namespace instructions require a tool namespace: "${invalid.name}"`
|
||||
: `Namespace instructions require a Code Mode tool: "${invalid.name}"`,
|
||||
}),
|
||||
)
|
||||
const entries = normalizedEntries(tools)
|
||||
yield* Effect.forEach(entries, (entry) => validateName(normalizedName(entry.tool)), { discard: true })
|
||||
const collision = entries.find(
|
||||
@@ -182,6 +196,24 @@ const layer = Layer.effect(
|
||||
yield* Effect.uninterruptible(
|
||||
lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const conflict = [
|
||||
...Map.groupBy(
|
||||
[
|
||||
...Array.from(local.values()).flatMap((registrations) => registrations.map((item) => item.tool)),
|
||||
...tools,
|
||||
].filter((tool) => tool.options?.namespaceInstructions !== undefined),
|
||||
(tool) => tool.options?.namespace?.split(".", 1)[0] ?? "",
|
||||
),
|
||||
].find(
|
||||
([, registrations]) => new Set(registrations.map((tool) => tool.options?.namespaceInstructions)).size > 1,
|
||||
)
|
||||
if (conflict)
|
||||
return yield* Effect.fail(
|
||||
new RegistrationError({
|
||||
name: conflict[0],
|
||||
message: `Conflicting instructions for tool namespace: "${conflict[0]}"`,
|
||||
}),
|
||||
)
|
||||
const token = {}
|
||||
for (const entry of entries)
|
||||
local.set(entry.key, [...(local.get(entry.key) ?? []), { token, tool: entry.tool }])
|
||||
@@ -207,10 +239,16 @@ const layer = Layer.effect(
|
||||
lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const active = new Map<string, Tool.Info>()
|
||||
const guidance = new Map<string, string>()
|
||||
const rules = permissions ?? []
|
||||
for (const [name, entries] of local) {
|
||||
const tool = entries.at(-1)?.tool
|
||||
if (!tool) continue
|
||||
if (tool.options?.namespace !== undefined && tool.options.namespaceInstructions !== undefined)
|
||||
guidance.set(
|
||||
tool.options.namespace.split(".", 1)[0] ?? tool.options.namespace,
|
||||
tool.options.namespaceInstructions,
|
||||
)
|
||||
if (whollyDisabled(tool.options?.permission ?? name, rules)) continue
|
||||
active.set(name, tool)
|
||||
}
|
||||
@@ -221,7 +259,7 @@ const layer = Layer.effect(
|
||||
const codemodeTool = codemodeEnabled
|
||||
? CodeModeTool.create(codemode, (name, tool, input, context) => executeTool(tool, name, input, context))
|
||||
: undefined
|
||||
const codeModeCatalog = codemodeEnabled ? CodeModeTool.catalog(codemode) : undefined
|
||||
const codeModeCatalog = codemodeEnabled ? CodeModeTool.catalog(codemode, guidance) : undefined
|
||||
return {
|
||||
...(codeModeCatalog === undefined ? {} : { codeModeCatalog }),
|
||||
definitions: [
|
||||
|
||||
@@ -40,4 +40,35 @@ describe("CodeMode", () => {
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.effect("includes namespace instructions in the materialized catalog", () =>
|
||||
Effect.gen(function* () {
|
||||
const tools = yield* Tool.Service
|
||||
yield* tools.transform((draft) =>
|
||||
draft.add({
|
||||
name: "create",
|
||||
description: "Create a session",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.String,
|
||||
options: {
|
||||
namespace: "sessions",
|
||||
namespaceInstructions: "Create independent sessions only when explicitly requested.",
|
||||
},
|
||||
execute: () => Effect.succeed({ output: "created" }),
|
||||
}),
|
||||
)
|
||||
|
||||
expect((yield* tools.snapshot()).codeModeCatalog?.[0]).toMatchObject({
|
||||
path: "sessions.create",
|
||||
namespaceInstructions: "Create independent sessions only when explicitly requested.",
|
||||
})
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(
|
||||
AppNodeBuilder.build(Tool.node, [
|
||||
[Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })],
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -47,6 +47,32 @@ describe("CodeModeCatalog.summarize", () => {
|
||||
expect(catalog.namespaces.every((namespace) => namespace.entries.length === 0)).toBe(true)
|
||||
})
|
||||
|
||||
test("retains namespace instructions when no tool listing fits", () => {
|
||||
const catalog = CodeModeCatalog.summarize(
|
||||
[{ ...lookup, namespaceInstructions: "Use order tools only for verified customers." }],
|
||||
0,
|
||||
)
|
||||
|
||||
expect(catalog.namespaces).toEqual([
|
||||
{
|
||||
name: "orders",
|
||||
count: 1,
|
||||
entries: [],
|
||||
instructions: "Use order tools only for verified customers.",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("charges namespace instructions against the tool listing budget", () => {
|
||||
const guidance = "Ask before using these tools."
|
||||
const guided = { ...lookup, namespaceInstructions: guidance }
|
||||
const line = ` - ${lookup.signature} // Look up an order by ID`
|
||||
const budget = Math.round(line.length / 4) + Math.round(` ${guidance}`.length / 4)
|
||||
|
||||
expect(CodeModeCatalog.summarize([guided], budget).shown).toBe(1)
|
||||
expect(CodeModeCatalog.summarize([guided], budget - 1).shown).toBe(0)
|
||||
})
|
||||
|
||||
test("always retains pinned tools beyond the inline budget", () => {
|
||||
const pinned = [entry("alpha.first", "First", undefined, true), entry("beta.second", "Second", undefined, true)]
|
||||
const catalog = CodeModeCatalog.summarize([...pinned, entry("alpha.unpinned", "Unpinned")], 0)
|
||||
@@ -108,6 +134,26 @@ describe("CodeModeInstructions.render", () => {
|
||||
expect(partial).not.toContain("tools.orders.lookup(input:")
|
||||
})
|
||||
|
||||
test("renders multiline namespace instructions once before tool listings", () => {
|
||||
const guidance = "Use order tools only for verified customers.\nNever create orders without approval."
|
||||
const instructions = render([
|
||||
{ ...lookup, namespaceInstructions: guidance },
|
||||
{ ...entry("orders.cancel", "Cancel an order"), namespaceInstructions: guidance },
|
||||
])
|
||||
|
||||
expect(instructions).toContain(
|
||||
"- orders (2 tools)\n Use order tools only for verified customers.\n Never create orders without approval.",
|
||||
)
|
||||
expect(instructions.split("Use order tools only for verified customers.")).toHaveLength(2)
|
||||
})
|
||||
|
||||
test("renders namespace instructions when no tool listing fits", () => {
|
||||
const instructions = render([{ ...lookup, namespaceInstructions: "Verify the customer before searching." }], 0)
|
||||
|
||||
expect(instructions).toContain("- orders (1 tool, none shown)\n Verify the customer before searching.")
|
||||
expect(instructions).not.toContain("tools.orders.lookup(input:")
|
||||
})
|
||||
|
||||
test("budgets signatures round-robin so every namespace remains visible", () => {
|
||||
const cheapAlpha = entry("alpha.cheap", "Cheap")
|
||||
const cheapBeta = entry("beta.cheap", "Cheap")
|
||||
@@ -197,4 +243,31 @@ describe("CodeModeInstructions.update", () => {
|
||||
expect(text).not.toContain("tools.alpha.tool10(input:")
|
||||
expect(text).not.toContain("## Available tools")
|
||||
})
|
||||
|
||||
test("replaces complete catalogs when namespace instructions change", () => {
|
||||
const previous = { ...lookup, namespaceInstructions: "Ask before looking up orders." }
|
||||
const current = { ...lookup, namespaceInstructions: "Only look up approved orders." }
|
||||
const text = update([previous], [current])
|
||||
|
||||
expect(text).toContain("This catalog supersedes the previous Code Mode tool catalog.")
|
||||
expect(text).toContain("- orders (1 tool)\n Only look up approved orders.")
|
||||
expect(text).not.toContain("Ask before looking up orders.")
|
||||
})
|
||||
|
||||
test("replaces partial catalogs when namespace instructions change", () => {
|
||||
const previous = { ...lookup, namespaceInstructions: "Ask before looking up orders." }
|
||||
const current = { ...lookup, namespaceInstructions: "Only look up approved orders." }
|
||||
const text = update([previous], [current], 0)
|
||||
|
||||
expect(text).toContain("This catalog supersedes the previous Code Mode tool catalog.")
|
||||
expect(text).toContain("- orders (1 tool, none shown)\n Only look up approved orders.")
|
||||
expect(text).not.toContain("tools.orders.lookup(input:")
|
||||
})
|
||||
|
||||
test("replaces catalogs when an instructed namespace is added or removed", () => {
|
||||
const guided = { ...lookup, namespaceInstructions: "Ask before looking up orders." }
|
||||
|
||||
expect(update([echo], [echo, guided])).toContain("- orders (1 tool)\n Ask before looking up orders.")
|
||||
expect(update([echo, guided], [echo])).toContain("This catalog supersedes the previous Code Mode tool catalog.")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -69,6 +69,23 @@ describe("CodeModeInstructions", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("persists namespace instruction changes as durable catalog replacements", () =>
|
||||
Effect.gen(function* () {
|
||||
const initialized = yield* readInitial(
|
||||
CodeModeInstructions.make([{ ...echo, namespaceInstructions: "Ask before using note tools." }]),
|
||||
)
|
||||
expect(initialized.text).toContain("- notes (1 tool)\n Ask before using note tools.")
|
||||
|
||||
const changed = yield* readUpdate(
|
||||
CodeModeInstructions.make([{ ...echo, namespaceInstructions: "Use note tools only when requested." }]),
|
||||
initialized,
|
||||
)
|
||||
expect(changed.changed).toBe(true)
|
||||
expect(changed.text).toContain("This catalog supersedes the previous Code Mode tool catalog.")
|
||||
expect(changed.text).toContain("- notes (1 tool)\n Use note tools only when requested.")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("stores a canonical sorted snapshot so registration order does not churn history", () => {
|
||||
const alpha = {
|
||||
name: "alpha",
|
||||
|
||||
@@ -82,6 +82,120 @@ describe("Tool", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects namespace instructions without a namespace", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
const error = yield* transform(service, { echo: make() }, { namespaceInstructions: "Ask first." }).pipe(
|
||||
Effect.flip,
|
||||
)
|
||||
|
||||
expect(error).toBeInstanceOf(Tool.RegistrationError)
|
||||
expect(error.message).toBe('Namespace instructions require a tool namespace: "echo"')
|
||||
expect((yield* service.snapshot()).codeModeCatalog).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects namespace instructions on direct-only tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
const options = { namespace: "sessions", codemode: false as const }
|
||||
Reflect.set(options, "namespaceInstructions", "Ask first.")
|
||||
const error = yield* transform(service, { echo: make() }, options).pipe(Effect.flip)
|
||||
|
||||
expect(error).toBeInstanceOf(Tool.RegistrationError)
|
||||
expect(error.message).toBe('Namespace instructions require a Code Mode tool: "echo"')
|
||||
expect((yield* service.snapshot()).codeModeCatalog).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects conflicting namespace instructions before installing a registration batch", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
const error = yield* service
|
||||
.transform((draft) => {
|
||||
draft.add({
|
||||
...make(),
|
||||
name: "first",
|
||||
options: { namespace: "sessions", namespaceInstructions: "Ask first." },
|
||||
})
|
||||
draft.add({
|
||||
...make(),
|
||||
name: "second",
|
||||
options: { namespace: "sessions", namespaceInstructions: "Never ask first." },
|
||||
})
|
||||
})
|
||||
.pipe(Effect.flip)
|
||||
|
||||
expect(error).toBeInstanceOf(Tool.RegistrationError)
|
||||
expect(error.message).toBe('Conflicting instructions for tool namespace: "sessions"')
|
||||
expect((yield* service.snapshot()).codeModeCatalog).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects conflicting namespace instructions across registrations", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
yield* transform(service, { first: make() }, { namespace: "sessions", namespaceInstructions: "Ask first." })
|
||||
const error = yield* transform(
|
||||
service,
|
||||
{ second: make() },
|
||||
{ namespace: "sessions.admin", namespaceInstructions: "Never ask first." },
|
||||
).pipe(Effect.flip)
|
||||
|
||||
expect(error.message).toBe('Conflicting instructions for tool namespace: "sessions"')
|
||||
expect((yield* service.snapshot()).codeModeCatalog?.map((tool) => tool.path)).toEqual(["sessions.first"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("accepts matching namespace instructions across registrations", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
const options = { namespace: "sessions", namespaceInstructions: "Ask first." }
|
||||
yield* transform(service, { first: make() }, options)
|
||||
yield* transform(service, { second: make() }, options)
|
||||
|
||||
expect((yield* service.snapshot()).codeModeCatalog?.map((tool) => tool.path)).toEqual([
|
||||
"sessions.first",
|
||||
"sessions.second",
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects instructions conflicting with a shadowed registration", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
yield* transform(service, { first: make() }, { namespace: "sessions", namespaceInstructions: "Ask first." })
|
||||
yield* transform(service, { first: make() }, { namespace: "sessions" })
|
||||
const error = yield* transform(
|
||||
service,
|
||||
{ second: make() },
|
||||
{ namespace: "sessions", namespaceInstructions: "Never ask first." },
|
||||
).pipe(Effect.flip)
|
||||
|
||||
expect(error.message).toBe('Conflicting instructions for tool namespace: "sessions"')
|
||||
expect((yield* service.snapshot()).codeModeCatalog?.map((tool) => tool.path)).toEqual(["sessions.first"])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("keeps namespace instructions when their declaring tool is denied", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
yield* service.transform((draft) => {
|
||||
draft.add({
|
||||
...make(),
|
||||
name: "create",
|
||||
options: { namespace: "sessions", namespaceInstructions: "Ask before using session tools." },
|
||||
})
|
||||
draft.add({ ...make(), name: "get", options: { namespace: "sessions" } })
|
||||
})
|
||||
|
||||
const catalog = (yield* service.snapshot([{ action: "sessions_create", resource: "*", effect: "deny" }]))
|
||||
.codeModeCatalog
|
||||
expect(catalog?.map((tool) => tool.path)).toEqual(["sessions.get"])
|
||||
expect(catalog?.[0]?.namespaceInstructions).toBe("Ask before using session tools.")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("rejects invalid and colliding normalized names", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
|
||||
@@ -29,10 +29,12 @@ export type Options = BaseOptions &
|
||||
| {
|
||||
readonly codemode?: true
|
||||
readonly pinned?: boolean
|
||||
readonly namespaceInstructions?: string
|
||||
}
|
||||
| {
|
||||
readonly codemode: boolean
|
||||
readonly pinned?: never
|
||||
readonly namespaceInstructions?: never
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -828,7 +828,11 @@ effect: (ctx) =>
|
||||
description: "Create a greeting",
|
||||
input: Schema.Struct({ name: Schema.String }),
|
||||
output: Schema.Struct({ greeting: Schema.String }),
|
||||
options: { namespace: "acme", codemode: true },
|
||||
options: {
|
||||
namespace: "acme",
|
||||
codemode: true,
|
||||
namespaceInstructions: "Use Acme tools only when the user requests an Acme operation.",
|
||||
},
|
||||
execute: ({ name }, context) =>
|
||||
Effect.gen(function* () {
|
||||
yield* context.progress({ status: "greeting" })
|
||||
@@ -839,6 +843,10 @@ effect: (ctx) =>
|
||||
}),
|
||||
```
|
||||
|
||||
Set `namespaceInstructions` on a namespaced Code Mode tool to add guidance once under its top-level namespace in the tool
|
||||
catalog. The instructions remain visible when individual tools are hidden, participate in durable instruction updates,
|
||||
and must match any instructions registered by other tools in the same top-level namespace.
|
||||
|
||||
Schemas: [`Tool.Content`](/api#schema-Tool.Content), [`Tool.TextContent`](/api#schema-Tool.TextContent),
|
||||
[`Tool.FileContent`](/api#schema-Tool.FileContent).
|
||||
|
||||
|
||||
@@ -796,7 +796,11 @@ await ctx.tool.transform((draft) => {
|
||||
required: ["name"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
options: { namespace: "acme", codemode: true },
|
||||
options: {
|
||||
namespace: "acme",
|
||||
codemode: true,
|
||||
namespaceInstructions: "Use Acme tools only when the user requests an Acme operation.",
|
||||
},
|
||||
execute: async (input, tool) => {
|
||||
await tool.progress({ status: "greeting" })
|
||||
return { content: `Hello ${(input as { name: string }).name}!` }
|
||||
@@ -805,6 +809,10 @@ await ctx.tool.transform((draft) => {
|
||||
})
|
||||
```
|
||||
|
||||
Set `namespaceInstructions` on a namespaced Code Mode tool to add guidance once under its top-level namespace in the tool
|
||||
catalog. The instructions remain visible when individual tools are hidden, participate in durable instruction updates,
|
||||
and must match any instructions registered by other tools in the same top-level namespace.
|
||||
|
||||
#### Reference
|
||||
|
||||
Schemas: [`Tool.Content`](/api#schema-Tool.Content), [`Tool.TextContent`](/api#schema-Tool.TextContent),
|
||||
|
||||
Reference in New Issue
Block a user