refactor(core): replace deferred tool option with codemode (#36560)
This commit is contained in:
parent
a4b91d33d9
commit
64ca9b8d77
27 changed files with 88 additions and 251 deletions
|
|
@ -4,7 +4,7 @@
|
||||||
- Do not add a speculative generic permission or approval policy. A host omits tools it does not expose and enforces domain authorization inside each provided tool.
|
- Do not add a speculative generic permission or approval policy. A host omits tools it does not expose and enforces domain authorization inside each provided tool.
|
||||||
- Keep Code Mode unaware of host session, channel, and conversation models. The hosting application supplies trusted execution scope around it.
|
- Keep Code Mode unaware of host session, channel, and conversation models. The hosting application supplies trusted execution scope around it.
|
||||||
- Tool schemas are the model-facing Interface. Keep arguments minimal and natural to the operation; never add unrelated IDs as ambient capability tokens.
|
- Tool schemas are the model-facing Interface. Keep arguments minimal and natural to the operation; never add unrelated IDs as ambient capability tokens.
|
||||||
- When interpreter behavior or support changes, update `interpreter-support.md` and direct tests in the same PR. Update `codemode.md` when the package design, integration status, or rationale changes.
|
- When interpreter behavior or support changes, update `interpreter-support.md` and direct tests in the same PR.
|
||||||
|
|
||||||
## OpenAPI
|
## OpenAPI
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
# CodeMode Design and Status
|
|
||||||
|
|
||||||
This is the living design and status document for `@opencode-ai/codemode` and its existing V2 OpenCode adapter.
|
|
||||||
It records current behavior, intentional boundaries, durable rationale, and material remaining work.
|
|
||||||
|
|
||||||
Completed implementation history, branch names, test counts, and closed findings belong in git, not here. Remove
|
|
||||||
completed work instead of preserving checked-off chronology.
|
|
||||||
|
|
||||||
Detailed package API documentation lives in [README.md](./README.md), and the checkable language/runtime matrix lives
|
|
||||||
in [interpreter-support.md](./interpreter-support.md). OpenAPI-specific follow-ups live in
|
|
||||||
[src/openapi/TODO.md](./src/openapi/TODO.md).
|
|
||||||
|
|
||||||
## How CodeMode Works
|
|
||||||
|
|
||||||
### Purpose
|
|
||||||
|
|
||||||
CodeMode gives a model one `execute` tool backed by a confined JavaScript interpreter. Inside the program, the model
|
|
||||||
can call an explicit tree of schema-described tools, sequence dependent work, run independent calls concurrently,
|
|
||||||
and filter or aggregate results before returning them to the agent loop.
|
|
||||||
|
|
||||||
The goals are:
|
|
||||||
|
|
||||||
- Reduce model context consumed by large tool catalogs.
|
|
||||||
- Avoid an agent round-trip between every dependent tool call.
|
|
||||||
- Keep large intermediate results inside the program instead of sending them through model context.
|
|
||||||
- Give generated code only the authority explicitly supplied by the host.
|
|
||||||
|
|
||||||
CodeMode is an orchestration language, not a general JavaScript runtime or an application authorization system.
|
|
||||||
|
|
||||||
### Runtime
|
|
||||||
|
|
||||||
The generic runtime lives in `packages/codemode` and is host-neutral:
|
|
||||||
|
|
||||||
1. The host builds a tree of `Tool.make(...)` definitions and calls `CodeMode.make(...)` or `CodeMode.execute(...)`.
|
|
||||||
2. CodeMode generates model instructions, a budgeted inline catalog, and the global `search(...)` built-in.
|
|
||||||
3. TypeScript syntax is transpiled away, Acorn parses the resulting JavaScript, and an owned tree-walking interpreter
|
|
||||||
executes it without `eval`.
|
|
||||||
4. Tool inputs and outputs cross schema and plain-data boundaries before they become visible on either side.
|
|
||||||
5. Execution returns `CodeMode.Result`. Expected program and tool failures are diagnostic data; host interruption
|
|
||||||
remains Effect interruption.
|
|
||||||
|
|
||||||
Effect Schemas validate and transform tool inputs and outputs. JSON Schemas render model-facing signatures but do not
|
|
||||||
validate values; adapter-provided values still cross the plain-data boundary. A tool without an output schema is
|
|
||||||
advertised as `Promise<unknown>`.
|
|
||||||
|
|
||||||
### Discovery and model workflow
|
|
||||||
|
|
||||||
The model sees a token-budgeted catalog. Every namespace remains visible, and complete signatures are selected
|
|
||||||
round-robin across namespaces so one large namespace cannot starve the others. The global `search(...)` built-in is
|
|
||||||
always callable - synchronously, counted as an admitted tool call - and is advertised when the inline catalog is
|
|
||||||
partial.
|
|
||||||
|
|
||||||
The intended workflow is:
|
|
||||||
|
|
||||||
1. Pick an exact signature from the inline catalog, or return `search(...)` results and use a selected path in the
|
|
||||||
next execution.
|
|
||||||
2. Call the exact returned path without guessing or normalizing segments.
|
|
||||||
3. Narrow `Promise<unknown>` results before reading fields.
|
|
||||||
4. Start independent calls together and await them with `Promise.all`.
|
|
||||||
5. Filter and aggregate inside the program, then return only the data needed by the model.
|
|
||||||
|
|
||||||
Search returns directly usable JavaScript paths, descriptions, and complete TypeScript signatures. It supports exact
|
|
||||||
path lookup, namespace browsing, deterministic ranking, and pagination.
|
|
||||||
|
|
||||||
### Tool execution
|
|
||||||
|
|
||||||
Every sandbox promise starts eagerly on a run-once fiber owned by the whole CodeMode execution, including tool calls,
|
|
||||||
async functions, chained `.then`/`.catch`/`.finally` reactions, `new Promise(executor)` constructions, and the
|
|
||||||
`Promise.all`/`allSettled`/`race`/`any`/`resolve`/`reject` statics. Nested functions therefore cannot end the lifetime
|
|
||||||
of work they started.
|
|
||||||
Independent aggregate batches overlap, and rejection is observed at the eventual `await` or chained rejection handler.
|
|
||||||
`Promise.race` and `Promise.any` use native non-cancelling settlement semantics: the deciding member wins while losers
|
|
||||||
continue running, and an all-rejected `Promise.any` rejects with an `AggregateError`. `new Promise(...)` hands the
|
|
||||||
executor first-class resolve/reject callables that may escape and settle the promise later, exactly once.
|
|
||||||
Reaction ordering matches what V8 makes observable - handlers and await continuations are deferred and run in attach
|
|
||||||
order, and a combinator settles one reaction turn after its deciding member - without promising exact microtask-count
|
|
||||||
parity beyond that. At normal completion CodeMode interrupts everything still running - race losers,
|
|
||||||
fail-fast `Promise.all` stragglers, and fire-and-forget calls alike: the program has returned, so no future await can
|
|
||||||
exist, and work whose completion matters must be awaited by the program. Waiting for any class of leftover instead
|
|
||||||
would let it hold the execution open indefinitely.
|
|
||||||
Rejections that settled un-awaited before the return become `Success.warnings` diagnostics. A fatal program failure or
|
|
||||||
host interruption closes the execution promise scope and interrupts its active fibers instead. A timeout does the
|
|
||||||
same, except that a value the program already returned is preserved alongside a `TimeoutExceeded` warning rather than
|
|
||||||
discarded. CodeMode does not limit tool-call concurrency.
|
|
||||||
|
|
||||||
The public execution-policy knobs are `timeoutMs`, `maxToolCalls`, and `maxOutputBytes`. The package supplies no
|
|
||||||
defaults because budgets are host policy. The interpreter also enforces a fixed internal data nesting depth.
|
|
||||||
`maxOutputBytes` bounds retained payload bytes, not the complete rendered message;
|
|
||||||
warning diagnostics have an equal separate budget so a large value cannot starve them, and fixed truncation notices and
|
|
||||||
host-added framing are intentionally outside the budgets.
|
|
||||||
|
|
||||||
### Data, files, and failures
|
|
||||||
|
|
||||||
Program results and tool arguments are JSON-like data. Dates become ISO strings at host boundaries; RegExp, Map, and
|
|
||||||
Set values become `{}` as they do under JSON serialization. Promise and runtime reference values cannot cross the
|
|
||||||
boundary.
|
|
||||||
|
|
||||||
Unknown host failures and invalid outputs are sanitized. `ToolError` is the explicit channel for a safe message that a
|
|
||||||
tool wants the model to see. Diagnostic categories distinguish parsing, unsupported syntax, unknown tools, invalid
|
|
||||||
data, tool failures, limits, timeouts, execution failures, and warning truncation.
|
|
||||||
|
|
||||||
Files and other attachment content stay outside the interpreter. A host may collect them while child tools execute and
|
|
||||||
attach them to the outer result, but the program receives only the structured tool output.
|
|
||||||
|
|
||||||
### V2 OpenCode adapter
|
|
||||||
|
|
||||||
CodeMode is integrated into V2 through `packages/core/src/tool/registry.ts` and
|
|
||||||
`packages/core/src/tool/execute.ts`:
|
|
||||||
|
|
||||||
- Core has one canonical `Tool` representation. Location-scoped producers register direct or deferred tools through
|
|
||||||
`Tools.Service`.
|
|
||||||
- Each model step snapshots effective registrations, applies catalog visibility filtering, and exposes direct tools
|
|
||||||
normally.
|
|
||||||
- When visible deferred tools exist, Core reserves and materializes one `execute` tool. Grouped deferred tools become
|
|
||||||
CodeMode namespaces instead of flattened model-facing names.
|
|
||||||
- Nested calls execute the registered `Tool` values captured for the model request; later registrations affect later
|
|
||||||
requests.
|
|
||||||
- Authorization and side-effect ordering remain responsibilities of the leaf tool. Catalog visibility is not execution
|
|
||||||
authorization.
|
|
||||||
- Structured child output enters the interpreter. File parts are collected host-side and attached to the outer result.
|
|
||||||
- Nested call statuses are returned as final `execute` metadata for the TUI.
|
|
||||||
- `execute` is the one model-facing tool invocation. Nested calls reuse its invocation context and do not independently
|
|
||||||
run registry hooks or model-output bounding; this keeps complete intermediate structured values available for
|
|
||||||
in-program filtering. The outer `execute` settlement is the single model-output bounding boundary.
|
|
||||||
- Core supplies no CodeMode timeout or tool-call limit. User cancellation interrupts the outer invocation and its
|
|
||||||
supervised children; the outer settlement applies Core's normal output-retention policy.
|
|
||||||
|
|
||||||
MCP tools use this canonical path: they register as grouped tools and are deferred while CodeMode is enabled. Existing
|
|
||||||
output schemas are preserved in generated signatures. Direct Core tools remain direct and are not ambient globals
|
|
||||||
inside CodeMode.
|
|
||||||
|
|
||||||
## Intentionally Unsupported
|
|
||||||
|
|
||||||
These are product boundaries rather than DSL backlog:
|
|
||||||
|
|
||||||
- Ambient filesystem, process, environment, network, credential, or application access. External work must go through
|
|
||||||
supplied tools.
|
|
||||||
- Modules, imports, dynamic imports, `eval`, arbitrary host globals, npm packages, and prototype mutation.
|
|
||||||
- Generic permission prompts, authorization policy, durable pause/resume, replay, storage, or exactly-once external
|
|
||||||
side effects. Hosts and tools own those concerns.
|
|
||||||
- Heuristic parsing of text tool results as JSON. A result should not silently change type based on its contents.
|
|
||||||
|
|
||||||
The OpenAPI adapter may gain more transports and encodings, but it must continue skipping operations it cannot
|
|
||||||
represent accurately rather than guessing semantics.
|
|
||||||
|
|
||||||
## Decisions and Rationale
|
|
||||||
|
|
||||||
| Decision | Rationale |
|
|
||||||
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| Keep an owned tree-walking interpreter. | The product need is bounded tool orchestration, not arbitrary JavaScript. Owning the language surface keeps authority and behavior explicit. |
|
|
||||||
| Treat schemas as the model-facing interface. | Signatures drive correct calls; Effect Schema also provides the runtime validation boundary, while JSON Schema supports adapter interoperability. |
|
|
||||||
| Keep authority host-owned. | CodeMode can only confine programs to supplied tools. The host chooses those tools, and each tool enforces its own authorization and side-effect policy. |
|
|
||||||
| Use progressive catalog disclosure plus search. | Large tool sets should not consume the prompt, but every namespace must remain discoverable and speculative search calls should remain valid. |
|
|
||||||
| Start promises eagerly and supervise them for the execution. | This preserves normal call-time parallelism and run-once settlement while allowing pending work to be interrupted when the program returns. |
|
|
||||||
| Keep files outside the sandbox value space. | Models should compose structured data without routing binary payloads through generated code or context. |
|
|
||||||
| Treat `execute` as the model-facing invocation boundary. | Nested calls are implementation details of one orchestration program. Reusing the outer context and bounding only the final result preserves complete intermediate data without inventing durable child-call identities. |
|
|
||||||
| Return expected failures as data. | Models need actionable diagnostics without exposing private host causes; host interruption and defects must still propagate correctly. |
|
|
||||||
| Leave execution-limit defaults to hosts. | Appropriate budgets depend on the surrounding product and its own cancellation, retention, and output-bounding policies. |
|
|
||||||
| Skip unsupported OpenAPI operations. | Incorrect parameter encoding, authentication, or transport behavior is worse than a precise `skipped` reason. |
|
|
||||||
|
|
||||||
## Remaining Work
|
|
||||||
|
|
||||||
The [interpreter support checklist](./interpreter-support.md) owns concrete DSL, standard-library, semantic-correctness,
|
|
||||||
diagnostic, and data-boundary work. OpenAPI adapter work remains in [src/openapi/TODO.md](./src/openapi/TODO.md).
|
|
||||||
|
|
@ -52,9 +52,6 @@ export const Flag = {
|
||||||
get OPENCODE_EXPERIMENTAL_REFERENCES() {
|
get OPENCODE_EXPERIMENTAL_REFERENCES() {
|
||||||
return enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES")
|
return enabledByExperimental("OPENCODE_EXPERIMENTAL_REFERENCES")
|
||||||
},
|
},
|
||||||
get CODEMODE_ENABLED() {
|
|
||||||
return process.env["CODEMODE_ENABLED"] === undefined || truthy("CODEMODE_ENABLED")
|
|
||||||
},
|
|
||||||
get OPENCODE_TUI_CONFIG() {
|
get OPENCODE_TUI_CONFIG() {
|
||||||
return process.env["OPENCODE_TUI_CONFIG"]
|
return process.env["OPENCODE_TUI_CONFIG"]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
export * as McpGuidance from "./guidance"
|
export * as McpGuidance from "./guidance"
|
||||||
|
|
||||||
import { makeLocationNode } from "../effect/app-node"
|
import { makeLocationNode } from "../effect/app-node"
|
||||||
import { Flag } from "../flag/flag"
|
|
||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
|
|
@ -18,11 +17,7 @@ type Summary = typeof Summary.Type
|
||||||
const entries = (servers: ReadonlyArray<Summary>) =>
|
const entries = (servers: ReadonlyArray<Summary>) =>
|
||||||
servers.flatMap((server) => [
|
servers.flatMap((server) => [
|
||||||
` <server name="${server.server}">`,
|
` <server name="${server.server}">`,
|
||||||
...(Flag.CODEMODE_ENABLED
|
` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.group(server.server))}]\`.`,
|
||||||
? [
|
|
||||||
` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.group(server.server))}]\`.`,
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...server.instructions.split("\n").map((line) => ` ${line}`),
|
...server.instructions.split("\n").map((line) => ` ${line}`),
|
||||||
" </server>",
|
" </server>",
|
||||||
])
|
])
|
||||||
|
|
@ -81,7 +76,7 @@ export const layer = Layer.effect(
|
||||||
removed: () => "MCP server instructions are no longer available.",
|
removed: () => "MCP server instructions are no longer available.",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (Flag.CODEMODE_ENABLED && PermissionV2.evaluate("execute", "*", agent.permissions).effect === "deny")
|
if (PermissionV2.evaluate("execute", "*", agent.permissions).effect === "deny")
|
||||||
return source(Instructions.removed)
|
return source(Instructions.removed)
|
||||||
const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
|
const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
|
||||||
concurrency: "unbounded",
|
concurrency: "unbounded",
|
||||||
|
|
@ -90,12 +85,9 @@ export const layer = Layer.effect(
|
||||||
const visible = instructions
|
const visible = instructions
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
const owned = tools.filter((tool) => tool.server === item.server)
|
const owned = tools.filter((tool) => tool.server === item.server)
|
||||||
return (
|
return owned.some(
|
||||||
(!Flag.CODEMODE_ENABLED && owned.length === 0) ||
|
(tool) =>
|
||||||
owned.some(
|
PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
|
||||||
(tool) =>
|
|
||||||
PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.map((item) => ({ server: item.server, instructions: item.instructions }))
|
.map((item) => ({ server: item.server, instructions: item.instructions }))
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ Leaves own resolution, permission, and side-effect ordering. Translate only expe
|
||||||
## Registration
|
## Registration
|
||||||
|
|
||||||
Built-ins and plugin tools register through `Tools.Service.register({ [name]: tool })`. Registrations may provide a
|
Built-ins and plugin tools register through `Tools.Service.register({ [name]: tool })`. Registrations may provide a
|
||||||
group, which flattens direct model names to `<group>_<tool>`, and may be deferred from direct model exposure.
|
group, which flattens direct model names to `<group>_<tool>`, and default into CodeMode (`codemode` defaults true;
|
||||||
|
`codemode: false` keeps the tool on the provider's native tool list).
|
||||||
|
|
||||||
Registrations are scoped:
|
Registrations are scoped:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,7 @@ export const Plugin = {
|
||||||
}),
|
}),
|
||||||
"edit",
|
"edit",
|
||||||
),
|
),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -58,16 +58,16 @@ export const create = (registrations: ReadonlyMap<string, Registration>) => {
|
||||||
})
|
})
|
||||||
if (registration.group === undefined) {
|
if (registration.group === undefined) {
|
||||||
const path = registration.name
|
const path = registration.name
|
||||||
if (Object.hasOwn(tools, path)) throw new TypeError(`Deferred tool namespace conflict: ${path}`)
|
if (Object.hasOwn(tools, path)) throw new TypeError(`CodeMode tool namespace conflict: ${path}`)
|
||||||
tools[path] = value
|
tools[path] = value
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const path = registration.name
|
const path = registration.name
|
||||||
const namespace = registration.group
|
const namespace = registration.group
|
||||||
const group = tools[namespace]
|
const group = tools[namespace]
|
||||||
if (group && Tool.isDefinition(group)) throw new TypeError(`Deferred tool namespace conflict: ${namespace}`)
|
if (group && Tool.isDefinition(group)) throw new TypeError(`CodeMode tool namespace conflict: ${namespace}`)
|
||||||
if (group) {
|
if (group) {
|
||||||
if (Object.hasOwn(group, path)) throw new TypeError(`Deferred tool namespace conflict: ${namespace}.${path}`)
|
if (Object.hasOwn(group, path)) throw new TypeError(`CodeMode tool namespace conflict: ${namespace}.${path}`)
|
||||||
group[path] = value
|
group[path] = value
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ export const Plugin = {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ export const Plugin = {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { McpEvent } from "@opencode-ai/schema/mcp-event"
|
||||||
import { Effect, Exit, type JsonSchema, Layer, Scope, Semaphore, Stream } from "effect"
|
import { Effect, Exit, type JsonSchema, Layer, Scope, Semaphore, Stream } from "effect"
|
||||||
import { makeLocationNode } from "../effect/app-node"
|
import { makeLocationNode } from "../effect/app-node"
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { Flag } from "../flag/flag"
|
|
||||||
import { MCP } from "../mcp"
|
import { MCP } from "../mcp"
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
|
|
@ -107,7 +107,7 @@ export const layer = Layer.effectDiscard(
|
||||||
const next = yield* Scope.fork(scope)
|
const next = yield* Scope.fork(scope)
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(
|
||||||
groups,
|
groups,
|
||||||
([group, record]) => tools.register(record, { group, deferred: Flag.CODEMODE_ENABLED }),
|
([group, record]) => tools.register(record, { group }),
|
||||||
{
|
{
|
||||||
discard: true,
|
discard: true,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@ export const Plugin = {
|
||||||
}),
|
}),
|
||||||
"edit",
|
"edit",
|
||||||
),
|
),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ export const Plugin = {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,7 @@ export const Plugin = {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ export * as ToolRegistry from "./registry"
|
||||||
import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@opencode-ai/llm"
|
import { ToolOutput, type ToolCall, type ToolDefinition, type ToolResultValue } from "@opencode-ai/llm"
|
||||||
import { Context, Effect, Layer, Scope } from "effect"
|
import { Context, Effect, Layer, Scope } from "effect"
|
||||||
import type { AgentV2 } from "../agent"
|
import type { AgentV2 } from "../agent"
|
||||||
import { Flag } from "../flag/flag"
|
|
||||||
import { PermissionV2 } from "../permission"
|
import { PermissionV2 } from "../permission"
|
||||||
import { SessionMessage } from "../session/message"
|
import { SessionMessage } from "../session/message"
|
||||||
import { SessionSchema } from "../session/schema"
|
import { SessionSchema } from "../session/schema"
|
||||||
|
|
@ -56,7 +55,7 @@ const registryLayer = Layer.effect(
|
||||||
readonly tool: AnyTool
|
readonly tool: AnyTool
|
||||||
readonly name: string
|
readonly name: string
|
||||||
readonly group?: string
|
readonly group?: string
|
||||||
readonly deferred: boolean
|
readonly codemode: boolean
|
||||||
}
|
}
|
||||||
const local = new Map<string, Array<{ readonly token: object; readonly registration: Registration }>>()
|
const local = new Map<string, Array<{ readonly token: object; readonly registration: Registration }>>()
|
||||||
|
|
||||||
|
|
@ -132,7 +131,8 @@ const registryLayer = Layer.effect(
|
||||||
register: Effect.fn("ToolRegistry.register")(function* (tools, options) {
|
register: Effect.fn("ToolRegistry.register")(function* (tools, options) {
|
||||||
const entries = registrationEntries(tools, options?.group)
|
const entries = registrationEntries(tools, options?.group)
|
||||||
if (entries.length === 0) return
|
if (entries.length === 0) return
|
||||||
const reserved = options?.deferred ? undefined : entries.find((entry) => entry.key === "execute")
|
const codemode = options?.codemode ?? true
|
||||||
|
const reserved = codemode ? undefined : entries.find((entry) => entry.key === "execute")
|
||||||
if (reserved)
|
if (reserved)
|
||||||
return yield* Effect.fail(
|
return yield* Effect.fail(
|
||||||
new RegistrationError({ name: reserved.key, message: 'Tool name "execute" is reserved for CodeMode' }),
|
new RegistrationError({ name: reserved.key, message: 'Tool name "execute" is reserved for CodeMode' }),
|
||||||
|
|
@ -149,7 +149,7 @@ const registryLayer = Layer.effect(
|
||||||
tool: entry.tool,
|
tool: entry.tool,
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
group: entry.group,
|
group: entry.group,
|
||||||
deferred: options?.deferred ?? false,
|
codemode,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
@ -168,18 +168,17 @@ const registryLayer = Layer.effect(
|
||||||
}),
|
}),
|
||||||
materialize: Effect.fn("ToolRegistry.materialize")(function* (permissions) {
|
materialize: Effect.fn("ToolRegistry.materialize")(function* (permissions) {
|
||||||
const direct = new Map<string, Registration>()
|
const direct = new Map<string, Registration>()
|
||||||
const deferred = new Map<string, Registration>()
|
const codemode = new Map<string, Registration>()
|
||||||
const rules = permissions ?? []
|
const rules = permissions ?? []
|
||||||
for (const [name, entries] of local) {
|
for (const [name, entries] of local) {
|
||||||
const registration = entries.at(-1)?.registration
|
const registration = entries.at(-1)?.registration
|
||||||
if (!registration) continue
|
if (!registration) continue
|
||||||
if (registration.deferred && !Flag.CODEMODE_ENABLED) continue
|
|
||||||
if (whollyDisabled(permission(registration.tool, name), rules)) continue
|
if (whollyDisabled(permission(registration.tool, name), rules)) continue
|
||||||
if (registration.deferred) deferred.set(name, registration)
|
if (registration.codemode) codemode.set(name, registration)
|
||||||
else direct.set(name, registration)
|
else direct.set(name, registration)
|
||||||
}
|
}
|
||||||
const execute =
|
const execute =
|
||||||
deferred.size > 0 && !whollyDisabled("execute", rules) ? ExecuteTool.create(deferred) : undefined
|
codemode.size > 0 && !whollyDisabled("execute", rules) ? ExecuteTool.create(codemode) : undefined
|
||||||
return {
|
return {
|
||||||
definitions: [
|
definitions: [
|
||||||
...Array.from(direct, ([name, registration]) => definition(name, registration.tool)),
|
...Array.from(direct, ([name, registration]) => definition(name, registration.tool)),
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,7 @@ export const Plugin = {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ export const Plugin = {
|
||||||
}).pipe(Effect.mapError((error) => unableToLoad(input.id, error)))
|
}).pipe(Effect.mapError((error) => unableToLoad(input.id, error)))
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,7 @@ export const Plugin = {
|
||||||
return { sessionID: child.id, status: "completed" as const, output: result?.info.output ?? NO_TEXT }
|
return { sessionID: child.id, status: "completed" as const, output: result?.info.output ?? NO_TEXT }
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,7 @@ export const Plugin = {
|
||||||
}
|
}
|
||||||
}).pipe(Effect.mapError((error) => new ToolFailure({ message: `Unable to fetch ${input.url}`, error }))),
|
}).pipe(Effect.mapError((error) => new ToolFailure({ message: `Unable to fetch ${input.url}`, error }))),
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,7 @@ export const Plugin = {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ export const Plugin = {
|
||||||
}),
|
}),
|
||||||
"edit",
|
"edit",
|
||||||
),
|
),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -260,6 +260,7 @@ describe("PluginV2", () => {
|
||||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||||
execute: () => Effect.succeed({ ok: true }),
|
execute: () => Effect.succeed({ ok: true }),
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie),
|
.pipe(Effect.orDie),
|
||||||
|
|
@ -273,7 +274,7 @@ describe("PluginV2", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("groups tool names and defers registrations from direct exposure", () =>
|
it.effect("groups tool names and routes codemode registrations through execute", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugins = yield* PluginV2.Service
|
const plugins = yield* PluginV2.Service
|
||||||
const registry = yield* ToolRegistry.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
|
|
@ -289,9 +290,9 @@ describe("PluginV2", () => {
|
||||||
effect: (ctx) =>
|
effect: (ctx) =>
|
||||||
ctx.tool
|
ctx.tool
|
||||||
.transform((draft) => {
|
.transform((draft) => {
|
||||||
draft.add("plain", tool("Plain"))
|
draft.add("plain", tool("Plain"), { codemode: false })
|
||||||
draft.add("look/up", tool("Lookup"), { group: "context 7" })
|
draft.add("look/up", tool("Lookup"), { group: "context 7", codemode: false })
|
||||||
draft.add("search", tool("Search"), { group: "context 7", deferred: true })
|
draft.add("search", tool("Search"), { group: "context 7" })
|
||||||
})
|
})
|
||||||
.pipe(Effect.orDie),
|
.pipe(Effect.orDie),
|
||||||
})
|
})
|
||||||
|
|
@ -330,6 +331,7 @@ describe("PluginV2", () => {
|
||||||
output: Schema.Struct({ text: Schema.String }),
|
output: Schema.Struct({ text: Schema.String }),
|
||||||
execute: ({ text }) => Effect.sync(() => executed.push({ text })).pipe(Effect.as({ text })),
|
execute: ({ text }) => Effect.sync(() => executed.push({ text })).pipe(Effect.as({ text })),
|
||||||
}),
|
}),
|
||||||
|
{ codemode: false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,7 @@ describe("fromPromise", () => {
|
||||||
await ctx.tool.transform((tools) => {
|
await ctx.tool.transform((tools) => {
|
||||||
tools.add({
|
tools.add({
|
||||||
name: "hello",
|
name: "hello",
|
||||||
|
options: { codemode: false },
|
||||||
description: "Hello",
|
description: "Hello",
|
||||||
input: Schema.Struct({ name: Schema.String }),
|
input: Schema.Struct({ name: Schema.String }),
|
||||||
output: Schema.String,
|
output: Schema.String,
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ describe("ToolRegistry", () => {
|
||||||
bash: make(),
|
bash: make(),
|
||||||
edit: make("edit"),
|
edit: make("edit"),
|
||||||
write: make("edit"),
|
write: make("edit"),
|
||||||
})
|
}, { codemode: false })
|
||||||
const names = (permissions: PermissionV2.Ruleset) =>
|
const names = (permissions: PermissionV2.Ruleset) =>
|
||||||
toolDefinitions(service, permissions).pipe(Effect.map((definitions) => definitions.map((tool) => tool.name)))
|
toolDefinitions(service, permissions).pipe(Effect.map((definitions) => definitions.map((tool) => tool.name)))
|
||||||
|
|
||||||
|
|
@ -95,8 +95,8 @@ describe("ToolRegistry", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
const shared = make()
|
const shared = make()
|
||||||
yield* service.register({ first: shared })
|
yield* service.register({ first: shared }, { codemode: false })
|
||||||
yield* service.register({ second: Tool.withPermission(shared, "edit") })
|
yield* service.register({ second: Tool.withPermission(shared, "edit") }, { codemode: false })
|
||||||
Tool.withPermission(shared, "question")
|
Tool.withPermission(shared, "question")
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|
@ -110,7 +110,7 @@ describe("ToolRegistry", () => {
|
||||||
it.effect("reuses model definitions across requests", () =>
|
it.effect("reuses model definitions across requests", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
yield* service.register({ echo: make() })
|
yield* service.register({ echo: make() }, { codemode: false })
|
||||||
const first = yield* toolDefinitions(service)
|
const first = yield* toolDefinitions(service)
|
||||||
const second = yield* toolDefinitions(service)
|
const second = yield* toolDefinitions(service)
|
||||||
|
|
||||||
|
|
@ -122,7 +122,7 @@ describe("ToolRegistry", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
const scope = yield* Scope.make()
|
const scope = yield* Scope.make()
|
||||||
yield* service.register({ echo: make() }).pipe(Scope.provide(scope))
|
yield* service.register({ echo: make() }, { codemode: false }).pipe(Scope.provide(scope))
|
||||||
expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo"])
|
expect((yield* toolDefinitions(service)).map((tool) => tool.name)).toEqual(["echo"])
|
||||||
yield* Scope.close(scope, Exit.void)
|
yield* Scope.close(scope, Exit.void)
|
||||||
expect(yield* toolDefinitions(service)).toEqual([])
|
expect(yield* toolDefinitions(service)).toEqual([])
|
||||||
|
|
@ -135,7 +135,7 @@ describe("ToolRegistry", () => {
|
||||||
const scope = yield* Scope.make()
|
const scope = yield* Scope.make()
|
||||||
const registered = yield* Deferred.make<void>()
|
const registered = yield* Deferred.make<void>()
|
||||||
const fiber = yield* service
|
const fiber = yield* service
|
||||||
.register({ echo: make() })
|
.register({ echo: make() }, { codemode: false })
|
||||||
.pipe(
|
.pipe(
|
||||||
Effect.andThen(Deferred.succeed(registered, undefined)),
|
Effect.andThen(Deferred.succeed(registered, undefined)),
|
||||||
Effect.andThen(Effect.never),
|
Effect.andThen(Effect.never),
|
||||||
|
|
@ -161,7 +161,7 @@ describe("ToolRegistry", () => {
|
||||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||||
execute: () => Effect.fail(new Tool.Failure({ message: "Denied" })),
|
execute: () => Effect.fail(new Tool.Failure({ message: "Denied" })),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
expect(
|
expect(
|
||||||
yield* executeTool(service, {
|
yield* executeTool(service, {
|
||||||
sessionID,
|
sessionID,
|
||||||
|
|
@ -184,7 +184,7 @@ describe("ToolRegistry", () => {
|
||||||
output: Schema.Struct({}),
|
output: Schema.Struct({}),
|
||||||
execute: () => Effect.die("unexpected executor defect"),
|
execute: () => Effect.die("unexpected executor defect"),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
expect(
|
expect(
|
||||||
yield* service.materialize().pipe(
|
yield* service.materialize().pipe(
|
||||||
Effect.flatMap((materialized) =>
|
Effect.flatMap((materialized) =>
|
||||||
|
|
@ -203,7 +203,7 @@ describe("ToolRegistry", () => {
|
||||||
it.effect("propagates retention failures through settlement", () =>
|
it.effect("propagates retention failures through settlement", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
yield* service.register({ echo: make() })
|
yield* service.register({ echo: make() }, { codemode: false })
|
||||||
const materialized = yield* service.materialize()
|
const materialized = yield* service.materialize()
|
||||||
const exit = yield* materialized.settle(call("echo", "call-retention-failure")).pipe(Effect.exit)
|
const exit = yield* materialized.settle(call("echo", "call-retention-failure")).pipe(Effect.exit)
|
||||||
|
|
||||||
|
|
@ -234,7 +234,7 @@ describe("ToolRegistry", () => {
|
||||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||||
execute: (_, context) => Effect.sync(() => contexts.push(context)).pipe(Effect.as({ ok: true })),
|
execute: (_, context) => Effect.sync(() => contexts.push(context)).pipe(Effect.as({ ok: true })),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
yield* executeTool(service, {
|
yield* executeTool(service, {
|
||||||
sessionID,
|
sessionID,
|
||||||
...identity,
|
...identity,
|
||||||
|
|
@ -248,7 +248,7 @@ describe("ToolRegistry", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
bounds.length = 0
|
bounds.length = 0
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
yield* service.register({ bounded: make() })
|
yield* service.register({ bounded: make() }, { codemode: false })
|
||||||
expect(
|
expect(
|
||||||
yield* settleTool(service, {
|
yield* settleTool(service, {
|
||||||
sessionID,
|
sessionID,
|
||||||
|
|
@ -282,7 +282,7 @@ describe("ToolRegistry", () => {
|
||||||
execute: ({ value }) => Effect.sync(() => executed.push(value)).pipe(Effect.as({ value })),
|
execute: ({ value }) => Effect.sync(() => executed.push(value)).pipe(Effect.as({ value })),
|
||||||
toModelOutput: ({ output }) => [{ type: "text", text: String(output.value) }],
|
toModelOutput: ({ output }) => [{ type: "text", text: String(output.value) }],
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
yield* executeTool(service, {
|
yield* executeTool(service, {
|
||||||
|
|
@ -319,7 +319,7 @@ describe("ToolRegistry", () => {
|
||||||
}),
|
}),
|
||||||
execute: () => Effect.succeed({ value: "invalid" }),
|
execute: () => Effect.succeed({ value: "invalid" }),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
expect(
|
expect(
|
||||||
yield* executeTool(service, {
|
yield* executeTool(service, {
|
||||||
sessionID,
|
sessionID,
|
||||||
|
|
@ -334,10 +334,10 @@ describe("ToolRegistry", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
const scope = yield* Scope.make()
|
const scope = yield* Scope.make()
|
||||||
yield* service.register({ echo: constant("advertised") }).pipe(Scope.provide(scope))
|
yield* service.register({ echo: constant("advertised") }, { codemode: false }).pipe(Scope.provide(scope))
|
||||||
const request = yield* service.materialize()
|
const request = yield* service.materialize()
|
||||||
yield* Scope.close(scope, Exit.void)
|
yield* Scope.close(scope, Exit.void)
|
||||||
yield* service.register({ echo: constant("replacement") })
|
yield* service.register({ echo: constant("replacement") }, { codemode: false })
|
||||||
|
|
||||||
expect((yield* request.settle(call("echo"))).result).toEqual({ type: "text", value: "advertised" })
|
expect((yield* request.settle(call("echo"))).result).toEqual({ type: "text", value: "advertised" })
|
||||||
expect(yield* executeTool(service, call("echo"))).toEqual({ type: "text", value: "replacement" })
|
expect(yield* executeTool(service, call("echo"))).toEqual({ type: "text", value: "replacement" })
|
||||||
|
|
@ -347,9 +347,9 @@ describe("ToolRegistry", () => {
|
||||||
it.effect("reveals the previous registration after an overlay closes", () =>
|
it.effect("reveals the previous registration after an overlay closes", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
yield* service.register({ echo: constant("base") })
|
yield* service.register({ echo: constant("base") }, { codemode: false })
|
||||||
const overlay = yield* Scope.make()
|
const overlay = yield* Scope.make()
|
||||||
yield* service.register({ echo: constant("overlay") }).pipe(Scope.provide(overlay))
|
yield* service.register({ echo: constant("overlay") }, { codemode: false }).pipe(Scope.provide(overlay))
|
||||||
|
|
||||||
expect(yield* executeTool(service, call("echo"))).toEqual({ type: "text", value: "overlay" })
|
expect(yield* executeTool(service, call("echo"))).toEqual({ type: "text", value: "overlay" })
|
||||||
yield* Scope.close(overlay, Exit.void)
|
yield* Scope.close(overlay, Exit.void)
|
||||||
|
|
@ -357,37 +357,31 @@ describe("ToolRegistry", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.effect("executes deferred tools advertised in a model request", () =>
|
it.effect("executes codemode tools advertised in a model request", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const service = yield* ToolRegistry.Service
|
const service = yield* ToolRegistry.Service
|
||||||
const executed: string[] = []
|
const executed: string[] = []
|
||||||
const scope = yield* Scope.make()
|
const scope = yield* Scope.make()
|
||||||
yield* service
|
yield* service
|
||||||
.register(
|
.register({
|
||||||
{
|
|
||||||
echo: Tool.make({
|
|
||||||
description: "Echo text",
|
|
||||||
input: Schema.Struct({ text: Schema.String }),
|
|
||||||
output: Schema.Struct({ text: Schema.String }),
|
|
||||||
execute: ({ text }) => Effect.sync(() => executed.push(`old:${text}`)).pipe(Effect.as({ text })),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{ deferred: true },
|
|
||||||
)
|
|
||||||
.pipe(Scope.provide(scope))
|
|
||||||
const materialized = yield* service.materialize()
|
|
||||||
yield* Scope.close(scope, Exit.void)
|
|
||||||
yield* service.register(
|
|
||||||
{
|
|
||||||
echo: Tool.make({
|
echo: Tool.make({
|
||||||
description: "Echo text",
|
description: "Echo text",
|
||||||
input: Schema.Struct({ text: Schema.String }),
|
input: Schema.Struct({ text: Schema.String }),
|
||||||
output: Schema.Struct({ text: Schema.String }),
|
output: Schema.Struct({ text: Schema.String }),
|
||||||
execute: ({ text }) => Effect.sync(() => executed.push(`new:${text}`)).pipe(Effect.as({ text })),
|
execute: ({ text }) => Effect.sync(() => executed.push(`old:${text}`)).pipe(Effect.as({ text })),
|
||||||
}),
|
}),
|
||||||
},
|
})
|
||||||
{ deferred: true },
|
.pipe(Scope.provide(scope))
|
||||||
)
|
const materialized = yield* service.materialize()
|
||||||
|
yield* Scope.close(scope, Exit.void)
|
||||||
|
yield* service.register({
|
||||||
|
echo: Tool.make({
|
||||||
|
description: "Echo text",
|
||||||
|
input: Schema.Struct({ text: Schema.String }),
|
||||||
|
output: Schema.Struct({ text: Schema.String }),
|
||||||
|
execute: ({ text }) => Effect.sync(() => executed.push(`new:${text}`)).pipe(Effect.as({ text })),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
const settlement = yield* materialized.settle({
|
const settlement = yield* materialized.settle({
|
||||||
...call("execute"),
|
...call("execute"),
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,7 @@ const echo = Layer.effectDiscard(
|
||||||
output: Schema.Any,
|
output: Schema.Any,
|
||||||
execute: () => Effect.succeed({ big: 1n }),
|
execute: () => Effect.succeed({ big: 1n }),
|
||||||
}),
|
}),
|
||||||
}),
|
}, { codemode: false }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] })
|
const echoNode = makeLocationNode({ name: "test/session-runner-tools", layer: echo, deps: [ToolRegistry.node] })
|
||||||
|
|
@ -782,7 +782,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
return { answer: query.toUpperCase() }
|
return { answer: query.toUpperCase() }
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
yield* admit(session, "Use application context")
|
yield* admit(session, "Use application context")
|
||||||
responses = [reply.tool("call-location", "location_context", { query: "hello" }), []]
|
responses = [reply.tool("call-location", "location_context", { query: "hello" }), []]
|
||||||
|
|
||||||
|
|
@ -827,7 +827,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
output: Schema.Struct({ value: Schema.String }),
|
output: Schema.Struct({ value: Schema.String }),
|
||||||
execute: () => Effect.sync(() => executions.push("advertised")).pipe(Effect.as({ value: "advertised" })),
|
execute: () => Effect.sync(() => executions.push("advertised")).pipe(Effect.as({ value: "advertised" })),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
.pipe(Scope.provide(scope))
|
.pipe(Scope.provide(scope))
|
||||||
yield* admit(session, "Use the reloaded tool")
|
yield* admit(session, "Use the reloaded tool")
|
||||||
responses = [
|
responses = [
|
||||||
|
|
@ -852,7 +852,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
output: Schema.Struct({ value: Schema.String }),
|
output: Schema.Struct({ value: Schema.String }),
|
||||||
execute: () => Effect.sync(() => executions.push("replacement")).pipe(Effect.as({ value: "replacement" })),
|
execute: () => Effect.sync(() => executions.push("replacement")).pipe(Effect.as({ value: "replacement" })),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
yield* Deferred.succeed(streamGate, undefined)
|
yield* Deferred.succeed(streamGate, undefined)
|
||||||
yield* Fiber.join(run)
|
yield* Fiber.join(run)
|
||||||
|
|
||||||
|
|
@ -3189,7 +3189,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
Effect.mapError(() => new Tool.Failure({ message: "Permission blocked" })),
|
Effect.mapError(() => new Tool.Failure({ message: "Permission blocked" })),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
yield* admit(session, "Call blocked")
|
yield* admit(session, "Call blocked")
|
||||||
|
|
||||||
responses = [reply.tool("call-blocked", "blocked", {}), reply.stop()]
|
responses = [reply.tool("call-blocked", "blocked", {}), reply.stop()]
|
||||||
|
|
@ -3221,7 +3221,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
output: Schema.Struct({}),
|
output: Schema.Struct({}),
|
||||||
execute: () => Effect.die(new PermissionV2.DeclinedError()),
|
execute: () => Effect.die(new PermissionV2.DeclinedError()),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
yield* admit(session, "Call declined")
|
yield* admit(session, "Call declined")
|
||||||
|
|
||||||
response = reply.tool("call-declined", "declined", {})
|
response = reply.tool("call-declined", "declined", {})
|
||||||
|
|
@ -3261,7 +3261,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
Effect.mapError(() => new Tool.Failure({ message: "Use another tool" })),
|
Effect.mapError(() => new Tool.Failure({ message: "Use another tool" })),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
yield* admit(session, "Call corrected")
|
yield* admit(session, "Call corrected")
|
||||||
|
|
||||||
responses = [reply.tool("call-corrected", "corrected", {}), reply.stop()]
|
responses = [reply.tool("call-corrected", "corrected", {}), reply.stop()]
|
||||||
|
|
@ -3321,7 +3321,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const session = yield* setup
|
const session = yield* setup
|
||||||
const registry = yield* ToolRegistry.Service
|
const registry = yield* ToolRegistry.Service
|
||||||
yield* registry.register({ permissionfail: permissionFail })
|
yield* registry.register({ permissionfail: permissionFail }, { codemode: false })
|
||||||
yield* admit(session, "Reject permission")
|
yield* admit(session, "Reject permission")
|
||||||
responses = [
|
responses = [
|
||||||
reply.tool("call-permission", "permissionfail", {}),
|
reply.tool("call-permission", "permissionfail", {}),
|
||||||
|
|
@ -3366,7 +3366,7 @@ describe("SessionRunnerLLM", () => {
|
||||||
output: Schema.Struct({}),
|
output: Schema.Struct({}),
|
||||||
execute: () => Effect.die(new QuestionTool.CancelledError()),
|
execute: () => Effect.die(new QuestionTool.CancelledError()),
|
||||||
}),
|
}),
|
||||||
})
|
}, { codemode: false })
|
||||||
yield* admit(session, "Ask then stop")
|
yield* admit(session, "Ask then stop")
|
||||||
|
|
||||||
responses = [reply.tool("call-question", "question", {}), []]
|
responses = [reply.tool("call-question", "question", {}), []]
|
||||||
|
|
|
||||||
7
packages/docs/build/plugins.mdx
vendored
7
packages/docs/build/plugins.mdx
vendored
|
|
@ -315,11 +315,12 @@ export default Plugin.define({
|
||||||
Unsupported characters in tool and group names are normalized to underscores.
|
Unsupported characters in tool and group names are normalized to underscores.
|
||||||
The resulting exposed key must begin with a letter and contain at most 64
|
The resulting exposed key must begin with a letter and contain at most 64
|
||||||
letters, digits, underscores, or hyphens. Set `options` on the declaration to
|
letters, digits, underscores, or hyphens. Set `options` on the declaration to
|
||||||
configure registration with `{ group, deferred }`:
|
configure registration with `{ group, codemode }`:
|
||||||
|
|
||||||
- `group` prefixes and groups the exposed tool name.
|
- `group` prefixes and groups the exposed tool name.
|
||||||
- `deferred: true` makes the tool available through the deferred `execute`
|
- `codemode` defaults to `true` and makes the tool available through the
|
||||||
tool instead of exposing it directly.
|
`execute` CodeMode tool. Set `codemode: false` to expose it directly to the
|
||||||
|
provider.
|
||||||
|
|
||||||
The executor receives a second context argument containing `sessionID`,
|
The executor receives a second context argument containing `sessionID`,
|
||||||
`agent`, `assistantMessageID`, and `toolCallID`.
|
`agent`, `assistantMessageID`, and `toolCallID`.
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,8 @@ export interface ToolExecuteAfterEvent {
|
||||||
|
|
||||||
export interface RegisterOptions {
|
export interface RegisterOptions {
|
||||||
readonly group?: string
|
readonly group?: string
|
||||||
readonly deferred?: boolean
|
/** Defaults to true. False exposes the tool directly to the provider. */
|
||||||
|
readonly codemode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolDraft {
|
export interface ToolDraft {
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,8 @@ export interface ToolExecuteAfterEvent {
|
||||||
|
|
||||||
export interface RegisterOptions {
|
export interface RegisterOptions {
|
||||||
readonly group?: string
|
readonly group?: string
|
||||||
readonly deferred?: boolean
|
/** Defaults to true. False exposes the tool directly to the provider. */
|
||||||
|
readonly codemode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolDraft {
|
export interface ToolDraft {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue