codemode.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { Effect, Schema } from "effect"
  2. import { executeWithLimits } from "./interpreter/runtime.js"
  3. import { type HostTools, type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js"
  4. import type { Definition } from "./tool.js"
  5. /** A tool call admitted during an execution. */
  6. export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescription } from "./tool-runtime.js"
  7. /** Resource budgets enforced independently during each CodeMode program execution. */
  8. export type ExecutionLimits = {
  9. /** Maximum wall-clock execution time in milliseconds. No default: absent means no timeout. */
  10. readonly timeoutMs?: number
  11. /** Maximum number of tool calls admitted by the runtime. No default: absent means unlimited. */
  12. readonly maxToolCalls?: number
  13. /** Maximum UTF-8 bytes of model-facing output. No default: absent means no truncation. */
  14. readonly maxOutputBytes?: number
  15. }
  16. /** Controls how much of the tool catalog is inlined in agent instructions. */
  17. export type DiscoveryOptions = {
  18. /** Approximate token budget (chars/4, default 2000) for full catalog entries. */
  19. readonly catalogBudget?: number
  20. }
  21. type ToolTree<R = never> = {
  22. readonly [name: string]: Definition<R> | ToolTree<R>
  23. }
  24. export type ResolvedExecutionLimits = {
  25. readonly timeoutMs: number | undefined
  26. readonly maxToolCalls: number | undefined
  27. readonly maxOutputBytes: number | undefined
  28. }
  29. /** Options for one CodeMode execution. */
  30. export type ExecuteOptions<Tools extends Record<string, unknown> = {}> = {
  31. /** Source for one program in the supported JavaScript subset. */
  32. code: string
  33. /** Explicit tool tree exposed to the program as `tools`. */
  34. tools?: Tools & ToolTree<Services<Tools>>
  35. /** Per-execution overrides for the default resource limits. */
  36. limits?: ExecutionLimits
  37. /** Observes decoded tool input immediately before tool execution. */
  38. onToolCallStart?: (call: ToolRuntime.ToolCallStarted) => Effect.Effect<void, never, Services<Tools>>
  39. /** Observes each admitted tool call as it settles, with outcome and duration. */
  40. onToolCallEnd?: (call: ToolRuntime.ToolCallEnded) => Effect.Effect<void, never, Services<Tools>>
  41. }
  42. /** A JSON value that can cross the confined interpreter boundary. */
  43. export type DataValue = Schema.Json
  44. /** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */
  45. export type Options<Tools extends Record<string, unknown> = {}> = Omit<ExecuteOptions<Tools>, "code"> & {
  46. /** Progressive-disclosure configuration for the agent-facing tool catalog. */
  47. readonly discovery?: DiscoveryOptions
  48. }
  49. /** Schema for a host tool input containing CodeMode source. */
  50. export const Input = Schema.Struct({ code: Schema.String })
  51. export type Input = typeof Input.Type
  52. export const DiagnosticKind = Schema.Literals([
  53. "ParseError",
  54. "UnsupportedSyntax",
  55. "UnknownTool",
  56. "InvalidToolInput",
  57. "InvalidToolOutput",
  58. "InvalidDataValue",
  59. "ToolCallLimitExceeded",
  60. "TimeoutExceeded",
  61. "ToolFailure",
  62. "ExecutionFailure",
  63. ])
  64. /** Stable categories produced by program, schema, tool, and limit failures. */
  65. export type DiagnosticKind = typeof DiagnosticKind.Type
  66. export const Diagnostic = Schema.Struct({
  67. kind: DiagnosticKind,
  68. message: Schema.String,
  69. location: Schema.optionalKey(Schema.Struct({ line: Schema.Number, column: Schema.Number })),
  70. suggestions: Schema.optionalKey(Schema.Array(Schema.String)),
  71. })
  72. /** A normalized program diagnostic safe to return across an agent tool boundary. */
  73. export type Diagnostic = typeof Diagnostic.Type
  74. const ToolCallSchema = Schema.Struct({ name: Schema.String })
  75. export const Success = Schema.Struct({
  76. ok: Schema.Literal(true),
  77. value: Schema.Json,
  78. logs: Schema.optionalKey(Schema.Array(Schema.String)),
  79. truncated: Schema.optionalKey(Schema.Boolean),
  80. toolCalls: Schema.Array(ToolCallSchema),
  81. })
  82. /** Successful execution after the result has crossed the plain-data boundary. */
  83. export type Success = typeof Success.Type
  84. export const Failure = Schema.Struct({
  85. ok: Schema.Literal(false),
  86. error: Diagnostic,
  87. logs: Schema.optionalKey(Schema.Array(Schema.String)),
  88. truncated: Schema.optionalKey(Schema.Boolean),
  89. toolCalls: Schema.Array(ToolCallSchema),
  90. })
  91. /** Failed execution with calls admitted before the diagnostic was produced. */
  92. export type Failure = typeof Failure.Type
  93. /** Schema for the structured success or diagnostic returned by CodeMode execution. */
  94. export const Result = Schema.Union([Success, Failure])
  95. /** Result of executing a CodeMode program. Program failures are data, not Effect failures. */
  96. export type Result = typeof Result.Type
  97. /** Reusable confined runtime over one explicit tool tree. */
  98. export type Runtime<R = never> = {
  99. readonly catalog: () => ReadonlyArray<ToolDescription>
  100. readonly instructions: () => string
  101. readonly execute: (code: string) => Effect.Effect<Result, never, R>
  102. }
  103. const validateLimit = <Value extends number | undefined>(
  104. name: keyof ExecutionLimits,
  105. value: Value,
  106. minimum: number,
  107. ): Value => {
  108. if (value !== undefined && (!Number.isSafeInteger(value) || value < minimum)) {
  109. throw new RangeError(`${name} must be a safe integer greater than or equal to ${minimum}.`)
  110. }
  111. return value
  112. }
  113. const resolveExecutionLimits = (limits?: ExecutionLimits): ResolvedExecutionLimits => ({
  114. timeoutMs: validateLimit("timeoutMs", limits?.timeoutMs, 1),
  115. maxToolCalls: validateLimit("maxToolCalls", limits?.maxToolCalls, 0),
  116. maxOutputBytes: validateLimit("maxOutputBytes", limits?.maxOutputBytes, 0),
  117. })
  118. /** Executes one Effect-native CodeMode program without constructing a reusable runtime. */
  119. export const execute = <const Tools extends Record<string, unknown>>(
  120. options: ExecuteOptions<Tools>,
  121. ): Effect.Effect<Result, never, Services<Tools>> => {
  122. const tools = (options.tools ?? {}) as HostTools<Services<Tools>>
  123. ToolRuntime.assertValidTools(tools)
  124. return executeWithLimits(options, resolveExecutionLimits(options.limits), ToolRuntime.searchIndex(tools))
  125. }
  126. /** Creates an Effect-native runtime over explicit, schema-described tools. */
  127. export const make = <const Tools extends Record<string, unknown> = {}>(
  128. options: Options<Tools> = {} as Options<Tools>,
  129. ): Runtime<Services<Tools>> => {
  130. const tools = (options.tools ?? {}) as HostTools<Services<Tools>>
  131. ToolRuntime.assertValidTools(tools)
  132. const limits = resolveExecutionLimits(options.limits)
  133. const prepared = ToolRuntime.prepare(tools, options.discovery?.catalogBudget)
  134. return {
  135. catalog: () => prepared.catalog,
  136. instructions: () => prepared.instructions,
  137. execute: (code) => executeWithLimits<Tools>({ ...options, code }, limits, prepared.searchIndex),
  138. }
  139. }