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,7 +1,7 @@
import { Schema } from "effect"
import { ContentBlockID, FinishReason, ProtocolID, ProviderMetadata, RouteID, ToolCallID } from "./ids"
import { ModelSchema } from "./options"
import { ToolResultValue } from "./messages"
import { ToolOutput, ToolResultValue } from "./messages"
/**
* Token usage reported by an LLM provider.
@ -163,6 +163,7 @@ export const ToolResult = Schema.Struct({
id: ToolCallID,
name: Schema.String,
result: ToolResultValue,
output: Schema.optional(ToolOutput),
providerExecuted: Schema.optional(Schema.Boolean),
providerMetadata: Schema.optional(ProviderMetadata),
}).annotate({ identifier: "LLM.Event.ToolResult" })
@ -252,7 +253,12 @@ export const LLMEvent = Object.assign(llmEventTagged, {
ToolInputDelta.make({ ...input, id: toolCallID(input.id) }),
toolInputEnd: (input: WithID<ToolInputEnd, ToolCallID>) => ToolInputEnd.make({ ...input, id: toolCallID(input.id) }),
toolCall: (input: WithID<ToolCall, ToolCallID>) => ToolCall.make({ ...input, id: toolCallID(input.id) }),
toolResult: (input: WithID<ToolResult, ToolCallID>) => ToolResult.make({ ...input, id: toolCallID(input.id) }),
toolResult: (input: WithID<ToolResult, ToolCallID>) =>
ToolResult.make({
...input,
id: toolCallID(input.id),
output: input.output === undefined ? undefined : ToolOutput.make(input.output.structured, input.output.content),
}),
toolError: (input: WithID<ToolError, ToolCallID>) => ToolError.make({ ...input, id: toolCallID(input.id) }),
stepFinish: (input: WithUsage<StepFinish>) =>
StepFinish.make({

View file

@ -30,7 +30,7 @@ export type ReasoningEffort = Schema.Schema.Type<typeof ReasoningEffort>
export const TextVerbosity = Schema.Literals(["low", "medium", "high"])
export type TextVerbosity = Schema.Schema.Type<typeof TextVerbosity>
export const MessageRole = Schema.Literals(["user", "assistant", "tool"])
export const MessageRole = Schema.Literals(["system", "user", "assistant", "tool"])
export type MessageRole = Schema.Schema.Type<typeof MessageRole>
export const FinishReason = Schema.Literals(["stop", "length", "tool-calls", "content-filter", "error", "unknown"])

View file

@ -51,6 +51,56 @@ export type ToolResultMediaPart = Schema.Schema.Type<typeof ToolResultMediaPart>
export const ToolResultContentPart = Schema.Union([TextPart, ToolResultMediaPart])
export type ToolResultContentPart = Schema.Schema.Type<typeof ToolResultContentPart>
export class ToolTextContent extends Schema.Class<ToolTextContent>("Tool.TextContent")({
type: Schema.Literal("text"),
text: Schema.String,
}) {}
export const ToolFileSource = Schema.Union([
Schema.Struct({ type: Schema.Literal("data"), data: Schema.String }),
Schema.Struct({ type: Schema.Literal("url"), url: Schema.String }),
Schema.Struct({ type: Schema.Literal("file"), uri: Schema.String }),
]).pipe(Schema.toTaggedUnion("type"))
export type ToolFileSource = Schema.Schema.Type<typeof ToolFileSource>
export class ToolFileContent extends Schema.Class<ToolFileContent>("Tool.FileContent")({
type: Schema.Literal("file"),
source: ToolFileSource,
mime: Schema.String,
name: Schema.optional(Schema.String),
}) {}
/** Ordered, provider-independent content shown to models and UIs after a tool succeeds. */
export const ToolContent = Schema.Union([ToolTextContent, ToolFileContent]).pipe(Schema.toTaggedUnion("type"))
export type ToolContent = Schema.Schema.Type<typeof ToolContent>
export const toolText = (value: ConstructorParameters<typeof ToolTextContent>[0]) => new ToolTextContent(value)
export const toolFile = (value: ConstructorParameters<typeof ToolFileContent>[0]) => new ToolFileContent(value)
const inlineData = (uri: string) => {
if (!uri.startsWith("data:")) return undefined
const match = /^data:[^;,]+;base64,(.*)$/s.exec(uri)
if (!match) throw new Error("Tool file data URI must contain raw base64 bytes")
return match[1]!
}
const legacyInlineData = (value: string) => {
const data = inlineData(value)
if (data !== undefined) return data
if (/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) return value
throw new Error("Legacy tool-result media must contain raw base64 bytes or a base64 data URI")
}
/** Convert a legacy attachment URI without guessing unknown string semantics. */
export const toolFileSourceFromUri = (uri: string): ToolFileSource => {
const data = inlineData(uri)
if (data !== undefined) return { type: "data", data }
const url = URL.parse(uri)
if (url?.protocol === "file:") return { type: "file", uri }
if (url?.protocol === "http:" || url?.protocol === "https:") return { type: "url", url: uri }
throw new Error(`Unsupported tool file URI: ${uri}`)
}
const isToolResultValue = (value: unknown): value is ToolResultValue =>
isRecord(value) &&
(value.type === "text" || value.type === "json" || value.type === "error" || value.type === "content") &&
@ -86,6 +136,80 @@ export const ToolResultValue = Object.assign(
)
export type ToolResultValue = Schema.Schema.Type<typeof ToolResultValue>
export interface ToolOutput {
readonly structured: unknown
readonly content: ReadonlyArray<ToolContent>
}
export const ToolOutput = Object.assign(
Schema.Struct({
structured: Schema.Unknown,
content: Schema.Array(ToolContent),
}).annotate({ identifier: "LLM.ToolOutput" }),
{
make: (structured: unknown, content: ReadonlyArray<ToolContent> = []): ToolOutput => ({
structured,
content: content.map((item) =>
item.type === "text"
? toolText({ type: "text", text: item.text })
: toolFile({ type: "file", source: item.source, mime: item.mime, name: item.name }),
),
}),
fromResultValue: (result: ToolResultValue): ToolOutput | undefined => {
switch (result.type) {
case "json":
return { structured: result.value, content: [] }
case "text":
return { structured: {}, content: [toolText({ type: "text", text: toolResultText(result.value) })] }
case "content":
return {
structured: {},
content: result.value.map((item) =>
item.type === "text"
? toolText({ type: "text", text: item.text })
: toolFile({
type: "file",
source: { type: "data", data: legacyInlineData(item.data) },
mime: item.mediaType,
name: item.filename,
}),
),
}
case "error":
return undefined
}
},
toResultValue: (output: ToolOutput): ToolResultValue => {
if (output.content.length === 0) return { type: "json", value: output.structured }
if (output.content.length === 1 && output.content[0]?.type === "text")
return { type: "text", value: output.content[0].text }
const unsupported = output.content.find((item) => item.type === "file" && item.source.type !== "data")
if (unsupported?.type === "file")
return {
type: "error",
value: `Tool file source "${unsupported.source.type}" must be materialized to inline data before provider conversion`,
}
return {
type: "content",
value: output.content.map((item) => {
if (item.type === "text") return { type: "text", text: item.text }
if (item.source.type !== "data") throw new Error("Unmaterialized tool file source reached provider conversion")
return { type: "media", mediaType: item.mime, data: item.source.data, filename: item.name }
}),
}
},
},
)
const toolResultText = (value: unknown) => {
if (typeof value === "string") return value
try {
return JSON.stringify(value) ?? String(value)
} catch {
return String(value)
}
}
export const ToolCallPart = Object.assign(
Schema.Struct({
type: Schema.Literal("tool-call"),
@ -157,6 +281,7 @@ export class Message extends Schema.Class<Message>("LLM.Message")({
export namespace Message {
export type ContentInput = string | ContentPart | ReadonlyArray<ContentPart>
export type SystemContentInput = string | TextPart | ReadonlyArray<TextPart>
export type Input = Omit<ConstructorParameters<typeof Message>[0], "content"> & {
readonly content: ContentInput
}
@ -175,6 +300,14 @@ export namespace Message {
export const assistant = (content: ContentInput) => make({ role: "assistant", content })
/**
* Add an operator-authored instruction at this chronological point in the
* conversation. This is distinct from the initial `LLMRequest.system`
* prompt. Keep raw retrieved, tool, and web content out of privileged system
* updates; pass that untrusted content through ordinary user/tool channels.
*/
export const system = (content: SystemContentInput) => make({ role: "system", content })
export const tool = (result: ToolResultPart | Parameters<typeof ToolResultPart.make>[0]) =>
make({ role: "tool", content: ["type" in result ? result : ToolResultPart.make(result)] })
}
@ -183,6 +316,7 @@ export class ToolDefinition extends Schema.Class<ToolDefinition>("LLM.ToolDefini
name: Schema.String,
description: Schema.String,
inputSchema: JsonSchema,
outputSchema: Schema.optional(JsonSchema),
cache: Schema.optional(CacheHint),
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
native: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),