feat(core): add embedded v2 session runtime and tool foundation (#30632)

This commit is contained in:
Kit Langton 2026-06-03 23:02:17 -04:00 committed by GitHub
commit 76ee87ead8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
215 changed files with 31398 additions and 3332 deletions

View file

@ -1,6 +1,6 @@
import { Effect, JsonSchema, Schema } from "effect"
import type { ToolCallPart, ToolDefinition as ToolDefinitionClass } from "./schema"
import { ToolDefinition, ToolFailure } from "./schema"
import type { ToolCallPart, ToolContent, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema"
import { ToolDefinition, ToolFailure, ToolOutput, toolText } from "./schema"
/**
* Schema constraint for tool parameters / success values: no decoding or
@ -18,6 +18,16 @@ export type ToolExecute<Parameters extends ToolSchema<any>, Success extends Tool
context?: ToolExecuteContext,
) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>
export interface ToolModelOutputInput<Parameters, Output> {
readonly callID: ToolCallPart["id"]
readonly parameters: Parameters
readonly output: Output
}
export type ToolToModelOutput<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = (
input: ToolModelOutputInput<Schema.Schema.Type<Parameters>, Success["Encoded"]>,
) => ReadonlyArray<ToolContent>
/**
* A type-safe LLM tool. Each tool bundles its own description, parameter
* Schema and success Schema. The execute handler is optional: omit it when you
@ -28,22 +38,27 @@ export type ToolExecute<Parameters extends ToolSchema<any>, Success extends Tool
* the stream.
*
* Internally each tool also carries memoized codecs and a precomputed
* `ToolDefinition` so the runtime doesn't rebuild them per invocation.
* `ToolDefinition` so callers do not rebuild them per invocation.
*/
export interface Tool<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> {
readonly description: string
readonly parameters: Parameters
readonly success: Success
readonly execute?: ToolExecute<Parameters, Success>
readonly toModelOutput?: ToolToModelOutput<Parameters, Success>
/** @internal */
readonly _decode: (input: unknown) => Effect.Effect<Schema.Schema.Type<Parameters>, Schema.SchemaError>
/** @internal */
readonly _encode: (value: Schema.Schema.Type<Success>) => Effect.Effect<unknown, Schema.SchemaError>
/** @internal */
readonly _project: (parameters: Schema.Schema.Type<Parameters>, callID: ToolCallPart["id"], output: unknown) => ToolOutputType
/** @internal */
readonly _legacyResult: boolean
/** @internal */
readonly _definition: ToolDefinitionClass
}
export type AnyTool = Tool<ToolSchema<any>, ToolSchema<any>>
export type AnyTool = Tool<any, any>
export type ExecutableTool<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>> = Tool<
Parameters,
@ -52,7 +67,7 @@ export type ExecutableTool<Parameters extends ToolSchema<any>, Success extends T
readonly execute: ToolExecute<Parameters, Success>
}
export type AnyExecutableTool = ExecutableTool<ToolSchema<any>, ToolSchema<any>>
export type AnyExecutableTool = ExecutableTool<any, any>
export type ExecutableTools = Record<string, AnyExecutableTool>
@ -61,12 +76,15 @@ type TypedToolConfig = {
readonly parameters: ToolSchema<any>
readonly success: ToolSchema<any>
readonly execute?: ToolExecute<ToolSchema<any>, ToolSchema<any>>
readonly toModelOutput?: ToolToModelOutput<ToolSchema<any>, ToolSchema<any>>
}
type DynamicToolConfig = {
readonly description: string
readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema
readonly execute?: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent>
}
/**
@ -97,30 +115,36 @@ type DynamicToolConfig = {
* })
* ```
*
* In both modes the produced tool flows through `toDefinitions(...)` and the
* runtime identically.
* In both modes the produced tool flows through `toDefinitions(...)`
* identically.
*/
export function make<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(config: {
readonly description: string
readonly parameters: Parameters
readonly success: Success
readonly execute: ToolExecute<Parameters, Success>
readonly toModelOutput?: ToolToModelOutput<Parameters, Success>
}): ExecutableTool<Parameters, Success>
export function make<Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(config: {
readonly description: string
readonly parameters: Parameters
readonly success: Success
readonly execute?: undefined
readonly toModelOutput?: ToolToModelOutput<Parameters, Success>
}): Tool<Parameters, Success>
export function make(config: {
readonly description: string
readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema
readonly execute: (params: unknown, context?: ToolExecuteContext) => Effect.Effect<unknown, ToolFailure>
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent>
}): AnyExecutableTool
export function make(config: {
readonly description: string
readonly jsonSchema: JsonSchema.JsonSchema
readonly outputSchema?: JsonSchema.JsonSchema
readonly execute?: undefined
readonly toModelOutput?: (input: ToolModelOutputInput<unknown, unknown>) => ReadonlyArray<ToolContent>
}): AnyTool
export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
if ("jsonSchema" in config) {
@ -129,12 +153,16 @@ export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
parameters: Schema.Unknown as ToolSchema<unknown>,
success: Schema.Unknown as ToolSchema<unknown>,
execute: config.execute,
toModelOutput: config.toModelOutput,
_decode: Effect.succeed,
_encode: Effect.succeed,
_project: (parameters, callID, output) => project(config.toModelOutput, parameters, callID, output),
_legacyResult: config.toModelOutput === undefined,
_definition: new ToolDefinition({
name: "",
description: config.description,
inputSchema: config.jsonSchema,
outputSchema: config.outputSchema,
}),
}
}
@ -143,18 +171,20 @@ export function make(config: TypedToolConfig | DynamicToolConfig): AnyTool {
parameters: config.parameters,
success: config.success,
execute: config.execute,
toModelOutput: config.toModelOutput,
_decode: Schema.decodeUnknownEffect(config.parameters),
_encode: Schema.encodeEffect(config.success),
_project: (parameters, callID, output) => project(config.toModelOutput, parameters, callID, output),
_legacyResult: false,
_definition: new ToolDefinition({
name: "",
description: config.description,
inputSchema: toJsonSchema(config.parameters),
outputSchema: toJsonSchema(config.success),
}),
}
}
export const tool = make
/**
* A record of named tools. The record key becomes the tool name on the wire.
*/
@ -162,8 +192,7 @@ export type Tools = Record<string, AnyTool>
/**
* Convert a tools record into the `ToolDefinition[]` shape that
* `LLMRequest.tools` expects. The runtime calls this internally; consumers
* that build `LLMRequest` themselves can use it too.
* `LLMRequest.tools` expects.
*
* Tool names come from the record keys, so the per-tool cached
* `_definition` is rebuilt with the correct name here. The JSON Schema body
@ -176,6 +205,7 @@ export const toDefinitions = (tools: Tools): ReadonlyArray<ToolDefinitionClass>
name,
description: item._definition.description,
inputSchema: item._definition.inputSchema,
outputSchema: item._definition.outputSchema,
}),
)
@ -185,6 +215,18 @@ const toJsonSchema = (schema: Schema.Top): JsonSchema.JsonSchema => {
return { ...document.schema, $defs: document.definitions }
}
const project = (
toModelOutput: ((input: ToolModelOutputInput<any, any>) => ReadonlyArray<ToolContent>) | undefined,
parameters: unknown,
callID: ToolCallPart["id"],
output: unknown,
): ToolOutputType =>
ToolOutput.make(
output,
toModelOutput?.({ callID, parameters, output }) ??
(typeof output === "string" ? [toolText({ type: "text", text: output })] : []),
)
export { ToolFailure }
export * as Tool from "./tool"