Skip to content

Commit be73f46

Browse files
authored
refactor(codemode): namespace public types (anomalyco#35425)
1 parent f14eafe commit be73f46

9 files changed

Lines changed: 98 additions & 146 deletions

File tree

packages/codemode/README.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const result =
6060
`)
6161
```
6262

63-
`result` is always an `ExecuteResult`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption.
63+
`result` is always a `CodeMode.Result`. Program, validation, limit, and tool failures are returned as diagnostics rather than failing the Effect. Host interruption remains interruption.
6464

6565
Successful result values are JSON-safe data. A program that returns `undefined`, including by reaching the end without `return`, produces `null`; nested `undefined` values are normalized to `null` as well.
6666

@@ -83,6 +83,8 @@ const tool = Tool.make({
8383

8484
The description and schemas are part of the model-visible tool contract. Keep descriptions concrete and put authorization in `run` or in the service it calls.
8585

86+
Public tool types are grouped under the same namespace: `Tool.Definition`, `Tool.Options`, `Tool.SchemaType`, and `Tool.JsonSchema`.
87+
8688
### `CodeMode.execute`
8789

8890
Use `CodeMode.execute` for a single execution:
@@ -113,30 +115,32 @@ const runtime = CodeMode.make({
113115

114116
runtime.catalog() // structured tool descriptions
115117
runtime.instructions() // model-facing syntax and tool guide
116-
runtime.execute(source) // ExecuteResult
118+
runtime.execute(source) // CodeMode.Result
117119
```
118120

119-
`CodeMode.Input` and `CodeMode.Result` are Effect schemas for the execution request and result. Hosts can combine them with `runtime.instructions()` and `runtime.execute()` when constructing a framework-specific agent tool.
121+
`CodeMode.Input`, `CodeMode.Result`, `CodeMode.Success`, `CodeMode.Failure`, `CodeMode.Diagnostic`, and `CodeMode.DiagnosticKind` are both Effect schemas and their inferred TypeScript types. Hosts can combine `CodeMode.Input` and `CodeMode.Result` with `runtime.instructions()` and `runtime.execute()` when constructing a framework-specific agent tool.
122+
123+
All other CodeMode types use the same namespace: `CodeMode.Options`, `CodeMode.ExecuteOptions`, `CodeMode.Runtime`, `CodeMode.ExecutionLimits`, `CodeMode.DiscoveryOptions`, `CodeMode.DataValue`, `CodeMode.ToolDescription`, and the `CodeMode.ToolCall*` observation types.
120124

121125
### Results
122126

123127
```ts
124-
type ExecuteResult = ExecuteSuccess | ExecuteFailure
128+
type Result = Success | Failure
125129

126-
interface ExecuteSuccess {
130+
interface Success {
127131
readonly ok: true
128-
readonly value: Schema.Json
132+
readonly value: CodeMode.DataValue
129133
readonly logs?: ReadonlyArray<string>
130134
readonly truncated?: boolean
131-
readonly toolCalls: ReadonlyArray<ToolCall>
135+
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
132136
}
133137

134-
interface ExecuteFailure {
138+
interface Failure {
135139
readonly ok: false
136-
readonly error: Diagnostic
140+
readonly error: CodeMode.Diagnostic
137141
readonly logs?: ReadonlyArray<string>
138142
readonly truncated?: boolean
139-
readonly toolCalls: ReadonlyArray<ToolCall>
143+
readonly toolCalls: ReadonlyArray<CodeMode.ToolCall>
140144
}
141145
```
142146

@@ -300,7 +304,7 @@ import { toolError } from "@opencode-ai/codemode"
300304
run: ({ id }) => (authorized(id) ? loadOrder(id) : Effect.fail(toolError("Order is unavailable")))
301305
```
302306

303-
Only the supplied message is model-visible. The optional cause is never returned in `ExecuteResult`; hosts should perform any required internal logging before crossing this boundary.
307+
Only the supplied message is model-visible. The optional cause is never returned in `CodeMode.Result`; hosts should perform any required internal logging before crossing this boundary.
304308

305309
## Authority Boundary
306310

@@ -332,7 +336,7 @@ The public contract is guided by these equivalences:
332336
- A tool implementation is not invoked unless its input has decoded successfully.
333337
- A tool result is not visible to the program unless its output has decoded and crossed the plain-data boundary successfully.
334338
- Unknown host failures do not become model-visible diagnostics; `ToolError` is the explicit safe-message channel.
335-
- Host interruption remains interruption rather than an `ExecuteFailure`.
339+
- Host interruption remains interruption rather than a `CodeMode.Failure`.
336340

337341
## Non-Goals
338342

packages/codemode/codemode.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ wave; both packages typecheck clean.
222222
result is exposed as-is. Discrimination via `Schema.isSchema`. New helpers exported from
223223
`tool.ts`: `inputTypeScript`/`outputTypeScript`/`decodeInput`/`decodeOutput`/
224224
`jsonSchemaToTypeScript`; `tool-runtime.ts` consumes them (no direct `Schema.*` use there
225-
anymore). Types `JsonSchema`/`ToolSchema` exported from the index. Note: an empty
225+
anymore). Types `Tool.JsonSchema`/`Tool.SchemaType` exported from the index. Note: an empty
226226
`Schema.Struct({})` renders as `{ } | Array<unknown>` (effect's JSON Schema emission) -
227227
cosmetic, fixed in Wave 4.
228228
- **`output.*` API deleted**: `OutputItem`(+Schema), result `output` fields, the `output`
@@ -237,7 +237,7 @@ wave; both packages typecheck clean.
237237
so failures are typed and observable). `message` is the model-safe failure message
238238
(`ToolError`/`ToolRuntimeError` message, else "Tool execution failed"). Interrupted calls
239239
fire no end event (timeout kills the whole execution anyway).
240-
- **Limits collapse**: public `ExecutionLimits` = `{ timeoutMs?, maxToolCalls?,
240+
- **Limits collapse**: public `CodeMode.ExecutionLimits` = `{ timeoutMs?, maxToolCalls?,
241241
maxOutputBytes? }` (defaults 10_000 / 100 / 32_000). This wave kept the other knobs as
242242
internal defaults reachable through an `@internal` `InternalExecutionLimits` type; Fix 5
243243
later deleted that type and the internal limit system entirely.
@@ -313,7 +313,7 @@ real MCP config. Package still 101 tests / 0 fail; opencode adapter suites still
313313
packages typecheck clean.
314314

315315
- **Budgeted catalog** (`discoveryPlan` in `tool-runtime.ts`): the all-or-nothing
316-
inline/search modes are gone - `DiscoveryMode` deleted, `DiscoveryOptions` is just
316+
inline/search modes are gone - `DiscoveryMode` deleted, `CodeMode.DiscoveryOptions` is just
317317
`{ maxInlineCatalogBytes? }` (default 16,000 UTF-8 bytes; later converted to
318318
`maxInlineCatalogTokens`, default 4,000 estimated tokens - see Post-wave fixes). Port of
319319
the old opencode
@@ -542,7 +542,7 @@ budget; namespaces must always be present):
542542

543543
- `src/token.ts` added: copy of `@opencode-ai/core/util/token` (`round(chars / 4)`), so
544544
the package stays dependency-free; keep in sync if the core heuristic changes.
545-
- `DiscoveryOptions.maxInlineCatalogBytes` -> `maxInlineCatalogTokens` (default 4,000
545+
- `CodeMode.DiscoveryOptions.maxInlineCatalogBytes` -> `maxInlineCatalogTokens` (default 4,000
546546
estimated tokens ~ the old 16,000 bytes at 4 chars/token - behavior parity, not a size
547547
reduction). `discoveryPlan` charges `estimate(catalogLine(tool))` per line; cheapest-first
548548
- stop-on-first-miss unchanged at the time (stop-on-first-miss replaced by round-robin in
@@ -558,7 +558,7 @@ budget; namespaces must always be present):
558558
**Fix 5 - internal limits removed** (user direction: only the three PUBLIC limits survive as
559559
configurable knobs; the internal limit system dies):
560560

561-
- `ExecutionLimits` (`timeoutMs` 10_000 / `maxToolCalls` 100 / `maxOutputBytes` 32_000 at
561+
- `CodeMode.ExecutionLimits` (`timeoutMs` 10_000 / `maxToolCalls` 100 / `maxOutputBytes` 32_000 at
562562
the time; Fix 6 later removed the first two defaults. Same validation: safe integers,
563563
timeoutMs >= 1, others >= 0, RangeError otherwise) is now
564564
the ENTIRE limit surface - exactly the shape section 2's original locked spec named.
@@ -602,7 +602,7 @@ configurable knobs; the internal limit system dies):
602602
enumeration operation-budget, codemode maxDataBytes/maxSourceBytes/maxOperations/
603603
maxConcurrency-RangeError assertions, and the adapter's runaway-loop-via-operation-limit
604604
test - superseded by the package timeout regression test); rewrote the helpers that used
605-
`InternalExecutionLimits` as a convenience to plain `ExecutionLimits`
605+
`InternalExecutionLimits` as a convenience to plain `CodeMode.ExecutionLimits`
606606
(promise/enumeration/stdlib run helpers). Package suite: 154 pass / 0 fail; adapter
607607
suites: 34 + 16.
608608

@@ -849,7 +849,7 @@ section 4 outer-truncation item the OPPOSITE way from "kill the outer one"):
849849
that relied on the old default now asserts the oversized result reaches the shared
850850
wrapper un-truncated. Suites: 210 + 50, tsgo clean both.
851851

852-
**Docs polish** (post-API-review): stale `DiscoveryOptions` JSDoc fixed (claimed default
852+
**Docs polish** (post-API-review): stale `CodeMode.DiscoveryOptions` JSDoc fixed (claimed default
853853
4,000 and alphabetical cheapest-first - now 2,000 and round-robin, matching Fix 8/9 reality)
854854
and the README's incorrect "`effect` as a peer dependency" line corrected (`effect` is a
855855
regular dependency; hosts depend on it themselves because the API surface is Effect-typed).

packages/codemode/src/codemode.ts

Lines changed: 51 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ import { ToolError } from "./tool-error.js"
1919
import { isSandboxValue, SandboxDate, SandboxMap, SandboxPromise, SandboxRegExp, SandboxSet } from "./values.js"
2020

2121
/** A tool call admitted during an execution. */
22-
export type { ToolCall, ToolCallStarted, ToolDescription } from "./tool-runtime.js"
23-
export { ToolError, toolError } from "./tool-error.js"
22+
export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescription } from "./tool-runtime.js"
2423

2524
/** Resource budgets enforced independently during each CodeMode program execution. */
2625
export type ExecutionLimits = {
@@ -74,50 +73,20 @@ export type ExecuteOptions<Tools extends Record<string, unknown> = {}> = {
7473
onToolCallEnd?: (call: ToolRuntime.ToolCallEnded) => Effect.Effect<void, never, Services<Tools>>
7574
}
7675

77-
/** A normalized program diagnostic safe to return across an agent tool boundary. */
78-
export type Diagnostic = {
79-
readonly kind: DiagnosticKind
80-
readonly message: string
81-
readonly location?: { readonly line: number; readonly column: number }
82-
readonly suggestions?: ReadonlyArray<string>
83-
}
84-
8576
/** A JSON value that can cross the confined interpreter boundary. */
8677
export type DataValue = Schema.Json
8778

88-
/** Successful execution after the result has crossed the plain-data boundary. */
89-
export type ExecuteSuccess = {
90-
readonly ok: true
91-
readonly value: DataValue
92-
readonly logs?: ReadonlyArray<string>
93-
/** Present when the value or logs were truncated to fit `maxOutputBytes`. */
94-
readonly truncated?: boolean
95-
readonly toolCalls: ReadonlyArray<ToolCall>
96-
}
97-
98-
/** Failed execution with calls admitted before the diagnostic was produced. */
99-
export type ExecuteFailure = {
100-
readonly ok: false
101-
readonly error: Diagnostic
102-
readonly logs?: ReadonlyArray<string>
103-
/** Present when the logs were truncated to fit `maxOutputBytes`. */
104-
readonly truncated?: boolean
105-
readonly toolCalls: ReadonlyArray<ToolCall>
106-
}
107-
108-
/** Result of executing a CodeMode program. Program failures are data, not Effect failures. */
109-
export type ExecuteResult = ExecuteSuccess | ExecuteFailure
110-
11179
/** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */
112-
export type CodeModeOptions<Tools extends Record<string, unknown> = {}> = Omit<ExecuteOptions<Tools>, "code"> & {
80+
export type Options<Tools extends Record<string, unknown> = {}> = Omit<ExecuteOptions<Tools>, "code"> & {
11381
/** Progressive-disclosure configuration for the agent-facing tool catalog. */
11482
readonly discovery?: DiscoveryOptions
11583
}
11684

117-
/** Schema for a CodeMode execution request. */
118-
const Input = Schema.Struct({ code: Schema.String })
85+
/** Schema for a host tool input containing CodeMode source. */
86+
export const Input = Schema.Struct({ code: Schema.String })
87+
export type Input = typeof Input.Type
11988

120-
const DiagnosticKindSchema = Schema.Literals([
89+
export const DiagnosticKind = Schema.Literals([
12190
"ParseError",
12291
"UnsupportedSyntax",
12392
"UnknownTool",
@@ -129,38 +98,52 @@ const DiagnosticKindSchema = Schema.Literals([
12998
"ToolFailure",
13099
"ExecutionFailure",
131100
])
101+
/** Stable categories produced by program, schema, tool, and limit failures. */
102+
export type DiagnosticKind = typeof DiagnosticKind.Type
103+
104+
export const Diagnostic = Schema.Struct({
105+
kind: DiagnosticKind,
106+
message: Schema.String,
107+
location: Schema.optionalKey(Schema.Struct({ line: Schema.Number, column: Schema.Number })),
108+
suggestions: Schema.optionalKey(Schema.Array(Schema.String)),
109+
})
110+
/** A normalized program diagnostic safe to return across an agent tool boundary. */
111+
export type Diagnostic = typeof Diagnostic.Type
112+
113+
const ToolCallSchema = Schema.Struct({ name: Schema.String })
114+
export const Success = Schema.Struct({
115+
ok: Schema.Literal(true),
116+
value: Schema.Json,
117+
logs: Schema.optionalKey(Schema.Array(Schema.String)),
118+
truncated: Schema.optionalKey(Schema.Boolean),
119+
toolCalls: Schema.Array(ToolCallSchema),
120+
})
121+
/** Successful execution after the result has crossed the plain-data boundary. */
122+
export type Success = typeof Success.Type
123+
124+
export const Failure = Schema.Struct({
125+
ok: Schema.Literal(false),
126+
error: Diagnostic,
127+
logs: Schema.optionalKey(Schema.Array(Schema.String)),
128+
truncated: Schema.optionalKey(Schema.Boolean),
129+
toolCalls: Schema.Array(ToolCallSchema),
130+
})
131+
/** Failed execution with calls admitted before the diagnostic was produced. */
132+
export type Failure = typeof Failure.Type
132133

133134
/** Schema for the structured success or diagnostic returned by CodeMode execution. */
134-
const Result = Schema.Union([
135-
Schema.Struct({
136-
ok: Schema.Literal(true),
137-
value: Schema.Json,
138-
logs: Schema.optionalKey(Schema.Array(Schema.String)),
139-
truncated: Schema.optionalKey(Schema.Boolean),
140-
toolCalls: Schema.Array(Schema.Struct({ name: Schema.String })),
141-
}),
142-
Schema.Struct({
143-
ok: Schema.Literal(false),
144-
error: Schema.Struct({
145-
kind: DiagnosticKindSchema,
146-
message: Schema.String,
147-
location: Schema.optionalKey(Schema.Struct({ line: Schema.Number, column: Schema.Number })),
148-
suggestions: Schema.optionalKey(Schema.Array(Schema.String)),
149-
}),
150-
logs: Schema.optionalKey(Schema.Array(Schema.String)),
151-
truncated: Schema.optionalKey(Schema.Boolean),
152-
toolCalls: Schema.Array(Schema.Struct({ name: Schema.String })),
153-
}),
154-
])
135+
export const Result = Schema.Union([Success, Failure])
136+
/** Result of executing a CodeMode program. Program failures are data, not Effect failures. */
137+
export type Result = typeof Result.Type
155138

156139
/** Reusable confined runtime over one explicit tool tree. */
157-
export type CodeModeRuntime<R = never> = {
140+
export type Runtime<R = never> = {
158141
/** Lists schema-described tool paths provided by the host. */
159142
readonly catalog: () => ReadonlyArray<ToolDescription>
160143
/** Builds model-facing syntax guidance and visible tool signatures. */
161144
readonly instructions: () => string
162145
/** Executes a program using this runtime's configured host tools. */
163-
readonly execute: (code: string) => Effect.Effect<ExecuteResult, never, R>
146+
readonly execute: (code: string) => Effect.Effect<Result, never, R>
164147
}
165148

166149
type SourcePosition = {
@@ -275,19 +258,6 @@ const errorBrandName = (value: unknown): string | undefined =>
275258
? ((value as Record<PropertyKey, unknown>)[ErrorBrand] as string | undefined)
276259
: undefined
277260

278-
/** Stable categories produced by program, schema, tool, and limit failures. */
279-
export type DiagnosticKind =
280-
| "ParseError"
281-
| "UnsupportedSyntax"
282-
| "UnknownTool"
283-
| "InvalidToolInput"
284-
| "InvalidToolOutput"
285-
| "InvalidDataValue"
286-
| "ToolCallLimitExceeded"
287-
| "TimeoutExceeded"
288-
| "ToolFailure"
289-
| "ExecutionFailure"
290-
291261
const arrayMethods = new Set([
292262
"map",
293263
"filter",
@@ -3943,7 +3913,7 @@ const executeWithLimits = <const Tools extends Record<string, unknown>>(
39433913
options: ExecuteOptions<Tools>,
39443914
limits: ResolvedExecutionLimits,
39453915
searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"],
3946-
): Effect.Effect<ExecuteResult, never, Services<Tools>> => {
3916+
): Effect.Effect<Result, never, Services<Tools>> => {
39473917
const hooks = {
39483918
...(options.onToolCallStart === undefined ? {} : { onToolCallStart: options.onToolCallStart }),
39493919
...(options.onToolCallEnd === undefined ? {} : { onToolCallEnd: options.onToolCallEnd }),
@@ -3975,7 +3945,7 @@ const executeWithLimits = <const Tools extends Record<string, unknown>>(
39753945
value: result,
39763946
...logged(),
39773947
toolCalls: tools.calls,
3978-
} satisfies ExecuteResult
3948+
} satisfies Result
39793949
}).pipe((program) => {
39803950
const timeoutMs = limits.timeoutMs
39813951
if (timeoutMs === undefined) return program
@@ -3988,7 +3958,7 @@ const executeWithLimits = <const Tools extends Record<string, unknown>>(
39883958
error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` },
39893959
...logged(),
39903960
toolCalls: tools.calls,
3991-
} satisfies ExecuteResult),
3961+
} satisfies Result),
39923962
}),
39933963
)
39943964
})
@@ -4002,7 +3972,7 @@ const executeWithLimits = <const Tools extends Record<string, unknown>>(
40023972
error: normalizeError(Cause.squash(cause)),
40033973
...logged(),
40043974
toolCalls: tools.calls,
4005-
} satisfies ExecuteResult),
3975+
} satisfies Result),
40063976
),
40073977
Effect.map((result) => (limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes))),
40083978
)
@@ -4026,7 +3996,7 @@ const utf8Truncate = (value: string, maxBytes: number): string => {
40263996
* fails the execution; `truncated: true` marks affected results. Only runs when the host set
40273997
* `maxOutputBytes` - with the limit absent, output passes through unbounded.
40283998
*/
4029-
const boundOutput = (result: ExecuteResult, maxOutputBytes: number): ExecuteResult => {
3999+
const boundOutput = (result: Result, maxOutputBytes: number): Result => {
40304000
let truncated = false
40314001

40324002
let value: DataValue = null
@@ -4068,7 +4038,7 @@ const boundOutput = (result: ExecuteResult, maxOutputBytes: number): ExecuteResu
40684038

40694039
export const execute = <const Tools extends Record<string, unknown>>(
40704040
options: ExecuteOptions<Tools>,
4071-
): Effect.Effect<ExecuteResult, never, Services<Tools>> => {
4041+
): Effect.Effect<Result, never, Services<Tools>> => {
40724042
const tools = (options.tools ?? {}) as HostTools<Services<Tools>>
40734043
ToolRuntime.assertValidTools(tools)
40744044
return executeWithLimits(options, resolveExecutionLimits(options.limits), ToolRuntime.searchIndex(tools))
@@ -4086,8 +4056,8 @@ export const execute = <const Tools extends Record<string, unknown>>(
40864056
* ```
40874057
*/
40884058
export const make = <const Tools extends Record<string, unknown> = {}>(
4089-
options: CodeModeOptions<Tools> = {} as CodeModeOptions<Tools>,
4090-
): CodeModeRuntime<Services<Tools>> => {
4059+
options: Options<Tools> = {} as Options<Tools>,
4060+
): Runtime<Services<Tools>> => {
40914061
const tools = (options.tools ?? {}) as HostTools<Services<Tools>>
40924062
ToolRuntime.assertValidTools(tools)
40934063
const limits = resolveExecutionLimits(options.limits)
@@ -4102,6 +4072,3 @@ export const make = <const Tools extends Record<string, unknown> = {}>(
41024072
execute: executeProgram,
41034073
}
41044074
}
4105-
4106-
/** Constructors for one-shot and reusable CodeMode execution. */
4107-
export const CodeMode = { Input, Result, make, execute }

packages/codemode/src/index.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,4 @@
1-
export { ToolError, CodeMode, toolError } from "./codemode.js"
2-
export { Tool } from "./tool.js"
1+
export * as CodeMode from "./codemode.js"
2+
export * as Tool from "./tool-api.js"
33
export * as OpenAPI from "./openapi/index.js"
4-
export type { Definition as ToolDefinition, JsonSchema, ToolSchema } from "./tool.js"
5-
export type { ToolCallEnded, ToolCallHooks } from "./tool-runtime.js"
6-
export type {
7-
CodeModeOptions,
8-
CodeModeRuntime,
9-
DataValue,
10-
Diagnostic,
11-
DiagnosticKind,
12-
DiscoveryOptions,
13-
ExecuteFailure,
14-
ExecuteOptions,
15-
ExecuteResult,
16-
ExecuteSuccess,
17-
ExecutionLimits,
18-
ToolCall,
19-
ToolCallStarted,
20-
ToolDescription,
21-
} from "./codemode.js"
4+
export { ToolError, toolError } from "./tool-error.js"

0 commit comments

Comments
 (0)