@yolk-sdk/agent 0.1.0-canary.21 → 0.1.0-canary.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +36 -8
  2. package/dist/loop/run.d.mts.map +1 -1
  3. package/dist/loop/run.mjs +38 -19
  4. package/dist/loop/run.mjs.map +1 -1
  5. package/dist/protocol/content.d.mts +2 -1
  6. package/dist/protocol/content.d.mts.map +1 -1
  7. package/dist/protocol/content.mjs +8 -1
  8. package/dist/protocol/content.mjs.map +1 -1
  9. package/dist/protocol/index.d.mts +3 -3
  10. package/dist/protocol/index.d.mts.map +1 -1
  11. package/dist/protocol/index.mjs +3 -3
  12. package/dist/protocol/message.d.mts +20 -1
  13. package/dist/protocol/message.d.mts.map +1 -1
  14. package/dist/protocol/message.mjs +59 -1
  15. package/dist/protocol/message.mjs.map +1 -1
  16. package/dist/providers/anthropic/claude-provider.d.mts +6 -0
  17. package/dist/providers/anthropic/claude-provider.d.mts.map +1 -1
  18. package/dist/providers/anthropic/claude-provider.mjs +47 -24
  19. package/dist/providers/anthropic/claude-provider.mjs.map +1 -1
  20. package/dist/providers/openai/codex-provider.d.mts.map +1 -1
  21. package/dist/providers/openai/codex-provider.mjs +4 -2
  22. package/dist/providers/openai/codex-provider.mjs.map +1 -1
  23. package/dist/providers/openai/provider.d.mts.map +1 -1
  24. package/dist/providers/openai/provider.mjs +4 -2
  25. package/dist/providers/openai/provider.mjs.map +1 -1
  26. package/dist/providers/transcript.d.mts +9 -0
  27. package/dist/providers/transcript.d.mts.map +1 -0
  28. package/dist/providers/transcript.mjs +17 -0
  29. package/dist/providers/transcript.mjs.map +1 -0
  30. package/package.json +1 -1
  31. package/src/loop/run.ts +56 -17
  32. package/src/protocol/content.ts +11 -0
  33. package/src/protocol/index.ts +10 -1
  34. package/src/protocol/message.ts +127 -0
  35. package/src/providers/anthropic/claude-provider.ts +78 -34
  36. package/src/providers/openai/codex-provider.ts +4 -1
  37. package/src/providers/openai/provider.ts +4 -2
  38. package/src/providers/transcript.ts +24 -0
  39. package/src/tools/README.md +3 -2
@@ -1,5 +1,6 @@
1
1
  import { classifyProviderFailure, providerErrorInfo, providerFailureCause, providerFailureRetryable } from "../provider-error.mjs";
2
- import { AgentInputUsage, AgentOutputUsage, AgentUsage, ToolCall, assistantContent, assistantHostToolCalls, attachmentSourceDataUrl, attachmentSourceText, isTextDocumentMimeType, messageContextText, prependMessageContextToContent } from "@yolk-sdk/agent/protocol";
2
+ import { validateProviderTranscript } from "../transcript.mjs";
3
+ import { AgentInputUsage, AgentOutputUsage, AgentUsage, ToolCall, assistantContent, assistantHostToolCalls, attachmentSourceText, attachmentSourceUrl, isTextDocumentMimeType, messageContextText, prependMessageContextToContent } from "@yolk-sdk/agent/protocol";
3
4
  import { Config, Context, Effect, Layer, Option, Redacted, Stream } from "effect";
4
5
  import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
5
6
  import * as Schema from "effect/Schema";
@@ -71,7 +72,7 @@ const contentPartToUserPart = (part) => {
71
72
  type: "text",
72
73
  text: part.text
73
74
  });
74
- case "Image": return Option.match(attachmentSourceDataUrl(part.source, part.mimeType), {
75
+ case "Image": return Option.match(attachmentSourceUrl(part.source, part.mimeType), {
75
76
  onNone: () => Effect.fail(unsupportedContentError("Unresolved image source")),
76
77
  onSome: (url) => Effect.succeed({
77
78
  type: "image_url",
@@ -138,6 +139,7 @@ const toOpenAiTool = (tool) => ({
138
139
  }
139
140
  });
140
141
  const toOpenAiRequestBody = (request, config) => Effect.gen(function* () {
142
+ yield* validateProviderTranscript(request.messages);
141
143
  const messages = [{
142
144
  role: "system",
143
145
  content: request.systemPrompt
@@ -1 +1 @@
1
- {"version":3,"file":"provider.mjs","names":["LLMTextDelta"],"sources":["../../../src/providers/openai/provider.ts"],"sourcesContent":["import { Config, Context, Effect, Layer, Option, Redacted, Stream } from 'effect'\nimport {\n FetchHttpClient,\n HttpClient,\n HttpClientRequest,\n type HttpClientError,\n type HttpClientResponse\n} from 'effect/unstable/http'\nimport * as Schema from 'effect/Schema'\nimport {\n ToolCall,\n AgentInputUsage,\n AgentOutputUsage,\n AgentUsage,\n attachmentSourceDataUrl,\n attachmentSourceText,\n assistantContent,\n assistantHostToolCalls,\n isTextDocumentMimeType,\n messageContextText,\n prependMessageContextToContent,\n type AgentMessage,\n type Content,\n type ContentPart,\n type ToolDef\n} from '@yolk-sdk/agent/protocol'\nimport {\n LLMError,\n LLMDone,\n LLMProvider,\n LLMTextDelta,\n LLMToolCall,\n LLMUsage,\n type LLMEvent,\n type LLMRequest\n} from '@yolk-sdk/agent/loop'\nimport {\n classifyProviderFailure,\n providerErrorInfo,\n providerFailureCause,\n providerFailureRetryable\n} from '../provider-error.ts'\n\nexport type OpenAiProviderConfig = {\n readonly chatCompletionsUrl?: string\n readonly maxCompletionTokens?: number\n readonly extraHeaders?: Readonly<Record<string, string>>\n readonly apiKey: Redacted.Redacted<string>\n}\n\ntype OpenAiTextContentPart = {\n readonly type: 'text'\n readonly text: string\n}\n\ntype OpenAiImageContentPart = {\n readonly type: 'image_url'\n readonly image_url: {\n readonly url: string\n }\n}\n\ntype OpenAiUserContent = string | ReadonlyArray<OpenAiTextContentPart | OpenAiImageContentPart>\n\ntype OpenAiToolCall = {\n readonly id: string\n readonly type: 'function'\n readonly function: {\n readonly name: string\n readonly arguments: string\n }\n}\n\ntype OpenAiMessage =\n | { readonly role: 'system'; readonly content: string }\n | { readonly role: 'user'; readonly content: OpenAiUserContent }\n | {\n readonly role: 'assistant'\n readonly content: string | null\n readonly tool_calls?: ReadonlyArray<OpenAiToolCall>\n }\n | { readonly role: 'tool'; readonly tool_call_id: string; readonly content: string }\n\ntype OpenAiTool = {\n readonly type: 'function'\n readonly function: {\n readonly name: string\n readonly description: string\n readonly parameters: unknown\n }\n}\n\ntype OpenAiRequestBody = {\n readonly model: string\n readonly messages: ReadonlyArray<OpenAiMessage>\n readonly max_completion_tokens: number\n readonly tools?: ReadonlyArray<OpenAiTool>\n readonly parallel_tool_calls?: true\n}\n\nclass OpenAiFunctionResponse extends Schema.Class<OpenAiFunctionResponse>('OpenAiFunctionResponse')(\n {\n name: Schema.String,\n arguments: Schema.String\n }\n) {}\n\nclass OpenAiToolCallResponse extends Schema.Class<OpenAiToolCallResponse>('OpenAiToolCallResponse')(\n {\n id: Schema.String,\n type: Schema.Literals(['function']),\n function: OpenAiFunctionResponse\n }\n) {}\n\nclass OpenAiMessageResponse extends Schema.Class<OpenAiMessageResponse>('OpenAiMessageResponse')({\n content: Schema.NullOr(Schema.String),\n tool_calls: Schema.optional(Schema.Array(OpenAiToolCallResponse))\n}) {}\n\nclass OpenAiChoiceResponse extends Schema.Class<OpenAiChoiceResponse>('OpenAiChoiceResponse')({\n message: OpenAiMessageResponse\n}) {}\n\nclass OpenAiPromptTokensDetails extends Schema.Class<OpenAiPromptTokensDetails>(\n 'OpenAiPromptTokensDetails'\n)({\n cached_tokens: Schema.optional(Schema.Number)\n}) {}\n\nclass OpenAiCompletionTokensDetails extends Schema.Class<OpenAiCompletionTokensDetails>(\n 'OpenAiCompletionTokensDetails'\n)({\n reasoning_tokens: Schema.optional(Schema.Number)\n}) {}\n\nclass OpenAiUsageResponse extends Schema.Class<OpenAiUsageResponse>('OpenAiUsageResponse')({\n prompt_tokens: Schema.Number,\n completion_tokens: Schema.Number,\n prompt_tokens_details: Schema.optional(OpenAiPromptTokensDetails),\n completion_tokens_details: Schema.optional(OpenAiCompletionTokensDetails)\n}) {}\n\nclass OpenAiChatCompletionResponse extends Schema.Class<OpenAiChatCompletionResponse>(\n 'OpenAiChatCompletionResponse'\n)({\n choices: Schema.Array(OpenAiChoiceResponse),\n usage: Schema.optional(OpenAiUsageResponse)\n}) {}\n\nclass OpenAiConfig extends Context.Service<OpenAiConfig, OpenAiProviderConfig>()(\n '@app/OpenAiConfig'\n) {}\n\nconst OpenAiConfigLayer = Layer.effect(\n OpenAiConfig,\n Effect.gen(function* () {\n const apiKey = yield* Config.redacted('OPENAI_API_KEY')\n return { apiKey }\n }).pipe(\n Effect.mapError(\n () =>\n new LLMError({\n cause: 'provider_error',\n message: 'OPENAI_API_KEY not found',\n retryable: false\n })\n )\n )\n)\n\nconst unknownToMessage = (error: unknown) =>\n error instanceof Error ? error.message : String(error)\n\nconst encodeJsonString = (value: unknown, message: string) =>\n Schema.encodeUnknownEffect(Schema.UnknownFromJsonString)(value).pipe(\n Effect.mapError(\n error =>\n new LLMError({\n cause: 'provider_error',\n message: `${message}: ${unknownToMessage(error)}`,\n retryable: false\n })\n )\n )\n\nconst decodeJsonString = (raw: string, message: string) =>\n Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)(raw).pipe(\n Effect.mapError(\n error =>\n new LLMError({\n cause: 'invalid_response',\n message: `${message}: ${unknownToMessage(error)}`,\n retryable: false\n })\n )\n )\n\nconst unsupportedContentError = (contentType: string) =>\n new LLMError({\n cause: 'provider_error',\n message: `${contentType} content is not supported by the OpenAI provider yet`,\n retryable: false\n })\n\nconst textDocumentToOpenAiPart = (part: Extract<ContentPart, { readonly _tag: 'Document' }>) =>\n attachmentSourceText(part.source).pipe(\n Effect.mapError(() => unsupportedContentError('Invalid document text')),\n Effect.flatMap(\n Option.match({\n onNone: () => Effect.fail(unsupportedContentError('Unresolved document source')),\n onSome: text => {\n const block: OpenAiTextContentPart = {\n type: 'text',\n text: `Document: ${part.title ?? part.filename}\\n\\n${text}`\n }\n\n return Effect.succeed(block)\n }\n })\n )\n )\n\nconst contentPartToUserPart = (\n part: ContentPart\n): Effect.Effect<OpenAiTextContentPart | OpenAiImageContentPart, LLMError> => {\n switch (part._tag) {\n case 'Text':\n return Effect.succeed({ type: 'text', text: part.text })\n case 'Image':\n return Option.match(attachmentSourceDataUrl(part.source, part.mimeType), {\n onNone: () => Effect.fail(unsupportedContentError('Unresolved image source')),\n onSome: url => Effect.succeed({ type: 'image_url', image_url: { url } })\n })\n case 'Document':\n return isTextDocumentMimeType(part.mimeType)\n ? textDocumentToOpenAiPart(part)\n : Effect.fail(unsupportedContentError('Document'))\n case 'Audio':\n return Effect.fail(unsupportedContentError('Audio'))\n }\n}\n\nconst contentToUserContent = (content: Content): Effect.Effect<OpenAiUserContent, LLMError> =>\n typeof content === 'string'\n ? Effect.succeed(content)\n : Effect.forEach(content, contentPartToUserPart)\n\nconst contentPartToText = (part: ContentPart, owner: string): Effect.Effect<string, LLMError> => {\n switch (part._tag) {\n case 'Text':\n return Effect.succeed(part.text)\n case 'Image':\n return Effect.fail(unsupportedContentError(`${owner} image`))\n case 'Document':\n return Effect.fail(unsupportedContentError(`${owner} document`))\n case 'Audio':\n return Effect.fail(unsupportedContentError(`${owner} audio`))\n }\n}\n\nconst contentToText = (content: Content, owner: string): Effect.Effect<string, LLMError> =>\n typeof content === 'string'\n ? Effect.succeed(content)\n : Effect.forEach(content, part => contentPartToText(part, owner)).pipe(\n Effect.map(textParts => textParts.join('\\n'))\n )\n\nconst serializeToolArguments = (params: unknown) =>\n encodeJsonString(params, 'Could not serialize OpenAI tool arguments')\n\nconst toolCallToOpenAiToolCall = (call: ToolCall): Effect.Effect<OpenAiToolCall, LLMError> =>\n Effect.gen(function* () {\n return {\n id: call.id,\n type: 'function',\n function: {\n name: call.name,\n arguments: yield* serializeToolArguments(call.params)\n }\n }\n })\n\nconst toOpenAiMessage = (message: AgentMessage): Effect.Effect<OpenAiMessage, LLMError> =>\n Effect.gen(function* () {\n switch (message._tag) {\n case 'User':\n return {\n role: 'user',\n content: yield* contentToUserContent(\n prependMessageContextToContent(message.content, messageContextText(message))\n )\n }\n case 'Assistant': {\n const content = prependMessageContextToContent(\n assistantContent(message),\n messageContextText(message)\n )\n const toolCalls = yield* Effect.forEach(\n assistantHostToolCalls(message),\n toolCallToOpenAiToolCall\n )\n\n if (toolCalls.length > 0) {\n return {\n role: 'assistant',\n content: yield* contentToText(content, 'Assistant'),\n tool_calls: toolCalls\n }\n }\n\n return {\n role: 'assistant',\n content: yield* contentToText(content, 'Assistant')\n }\n }\n case 'ToolResult':\n return {\n role: 'tool',\n tool_call_id: message.toolCallId,\n content: yield* contentToText(\n prependMessageContextToContent(message.content, messageContextText(message)),\n 'Tool result'\n )\n }\n }\n })\n\nconst toOpenAiTool = (tool: ToolDef): OpenAiTool => ({\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters\n }\n})\n\nexport const toOpenAiRequestBody = (\n request: LLMRequest,\n config?: { readonly maxCompletionTokens?: number }\n): Effect.Effect<OpenAiRequestBody, LLMError> =>\n Effect.gen(function* () {\n const systemMessage: OpenAiMessage = { role: 'system', content: request.systemPrompt }\n const requestMessages = yield* Effect.forEach(request.messages, toOpenAiMessage)\n const messages = [systemMessage, ...requestMessages]\n\n const body = {\n model: request.model,\n messages,\n max_completion_tokens: config?.maxCompletionTokens ?? 4096\n }\n\n if (request.tools.length === 0) {\n return body\n }\n\n return {\n ...body,\n tools: request.tools.map(toOpenAiTool),\n parallel_tool_calls: true\n }\n })\n\nconst parseToolArguments = (raw: string) =>\n decodeJsonString(raw, 'Invalid OpenAI tool arguments JSON')\n\nconst toLlmEvents = (\n message: OpenAiMessageResponse\n): Effect.Effect<ReadonlyArray<LLMEvent>, LLMError> =>\n Effect.gen(function* () {\n const content = message.content ?? ''\n const textEvents = content.length > 0 ? [LLMTextDelta.make({ text: content })] : []\n const toolCallEvents = yield* Effect.forEach(message.tool_calls ?? [], call =>\n parseToolArguments(call.function.arguments).pipe(\n Effect.map(params =>\n LLMToolCall.make({\n call: ToolCall.make({\n id: call.id,\n name: call.function.name,\n params\n })\n })\n )\n )\n )\n\n if (toolCallEvents.length > 0) {\n return [...textEvents, ...toolCallEvents, LLMDone.make({ stopReason: 'tool_use' })]\n }\n\n return [...textEvents, LLMDone.make({ stopReason: 'stop' })]\n })\n\nconst toAgentUsage = (usage: OpenAiUsageResponse) =>\n AgentUsage.make({\n input: AgentInputUsage.make({\n total: usage.prompt_tokens,\n uncached: usage.prompt_tokens - (usage.prompt_tokens_details?.cached_tokens ?? 0),\n cacheRead: usage.prompt_tokens_details?.cached_tokens\n }),\n output: AgentOutputUsage.make({\n total: usage.completion_tokens,\n reasoning: usage.completion_tokens_details?.reasoning_tokens,\n text: usage.completion_tokens - (usage.completion_tokens_details?.reasoning_tokens ?? 0)\n })\n })\n\nconst toHttpClientLlmError =\n (message: string, retryable: boolean) => (error: HttpClientError.HttpClientError) =>\n new LLMError({\n cause: 'provider_error',\n message: `${message}: ${error.message}`,\n retryable,\n provider: providerErrorInfo({\n provider: 'openai',\n kind: retryable ? 'network' : 'unknown'\n })\n })\n\nconst parseOpenAiResponseJson = (\n response: HttpClientResponse.HttpClientResponse\n): Effect.Effect<unknown, LLMError> =>\n response.json.pipe(\n Effect.mapError(\n error =>\n new LLMError({\n cause: 'invalid_response',\n message: `Could not parse OpenAI response JSON: ${error.message}`,\n retryable: false\n })\n )\n )\n\nconst sendOpenAiRequest = (\n config: OpenAiProviderConfig,\n request: LLMRequest,\n client: HttpClient.HttpClient\n): Effect.Effect<ReadonlyArray<LLMEvent>, LLMError> =>\n Effect.gen(function* () {\n const body = yield* toOpenAiRequestBody(request, config)\n const serializedBody = yield* encodeJsonString(body, 'Could not serialize OpenAI request')\n\n const httpRequest = HttpClientRequest.post(config.chatCompletionsUrl ?? 'https://api.openai.com/v1/chat/completions').pipe(\n HttpClientRequest.setHeaders({\n accept: 'application/json',\n authorization: `Bearer ${Redacted.value(config.apiKey)}`,\n 'content-type': 'application/json',\n ...config.extraHeaders\n }),\n HttpClientRequest.bodyText(serializedBody, 'application/json')\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(Effect.mapError(toHttpClientLlmError('OpenAI request failed', true)))\n\n if (response.status < 200 || response.status >= 300) {\n const errorText = yield* response.text.pipe(\n Effect.mapError(\n error =>\n new LLMError({\n cause: 'provider_error',\n message: `Could not read OpenAI error body: ${error.message}`,\n retryable: false\n })\n )\n )\n\n const provider = classifyProviderFailure({\n provider: 'openai',\n status: response.status,\n headers: response.headers,\n body: errorText\n })\n\n return yield* Effect.fail(\n new LLMError({\n cause: providerFailureCause(provider.kind),\n message: `OpenAI returned ${response.status}`,\n retryable: providerFailureRetryable(provider.kind),\n provider\n })\n )\n }\n\n const json = yield* parseOpenAiResponseJson(response)\n const parsed = yield* Schema.decodeUnknownEffect(OpenAiChatCompletionResponse)(json).pipe(\n Effect.mapError(\n error =>\n new LLMError({\n cause: 'invalid_response',\n message: `Invalid OpenAI response: ${unknownToMessage(error)}`,\n retryable: false\n })\n )\n )\n const choice = parsed.choices[0]\n\n if (choice === undefined) {\n return yield* Effect.fail(\n new LLMError({\n cause: 'invalid_response',\n message: 'OpenAI response contained no choices',\n retryable: false\n })\n )\n }\n\n const events = yield* toLlmEvents(choice.message)\n\n if (parsed.usage === undefined) {\n return events\n }\n\n return [...events, LLMUsage.make({ usage: toAgentUsage(parsed.usage) })]\n }).pipe(Effect.withSpan('OpenAiProvider.stream'))\n\nexport const makeOpenAiProviderLayer = (config: OpenAiProviderConfig) =>\n Layer.effect(\n LLMProvider,\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return LLMProvider.of({\n stream: request =>\n Stream.fromEffect(sendOpenAiRequest(config, request, client)).pipe(\n Stream.flatMap(events => Stream.fromIterable(events))\n )\n })\n })\n )\n\nexport const OpenAiProviderLayer = Layer.effect(\n LLMProvider,\n Effect.gen(function* () {\n const config = yield* OpenAiConfig\n const client = yield* HttpClient.HttpClient\n\n return LLMProvider.of({\n stream: request =>\n Stream.fromEffect(sendOpenAiRequest(config, request, client)).pipe(\n Stream.flatMap(events => Stream.fromIterable(events))\n )\n })\n })\n).pipe(Layer.provide(Layer.mergeAll(OpenAiConfigLayer, FetchHttpClient.layer)))\n"],"mappings":"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"}
1
+ {"version":3,"file":"provider.mjs","names":["LLMTextDelta"],"sources":["../../../src/providers/openai/provider.ts"],"sourcesContent":["import { Config, Context, Effect, Layer, Option, Redacted, Stream } from 'effect'\nimport {\n FetchHttpClient,\n HttpClient,\n HttpClientRequest,\n type HttpClientError,\n type HttpClientResponse\n} from 'effect/unstable/http'\nimport * as Schema from 'effect/Schema'\nimport {\n ToolCall,\n AgentInputUsage,\n AgentOutputUsage,\n AgentUsage,\n attachmentSourceText,\n attachmentSourceUrl,\n assistantContent,\n assistantHostToolCalls,\n isTextDocumentMimeType,\n messageContextText,\n prependMessageContextToContent,\n type AgentMessage,\n type Content,\n type ContentPart,\n type ToolDef\n} from '@yolk-sdk/agent/protocol'\nimport {\n LLMError,\n LLMDone,\n LLMProvider,\n LLMTextDelta,\n LLMToolCall,\n LLMUsage,\n type LLMEvent,\n type LLMRequest\n} from '@yolk-sdk/agent/loop'\nimport {\n classifyProviderFailure,\n providerErrorInfo,\n providerFailureCause,\n providerFailureRetryable\n} from '../provider-error.ts'\nimport { validateProviderTranscript } from '../transcript.ts'\n\nexport type OpenAiProviderConfig = {\n readonly chatCompletionsUrl?: string\n readonly maxCompletionTokens?: number\n readonly extraHeaders?: Readonly<Record<string, string>>\n readonly apiKey: Redacted.Redacted<string>\n}\n\ntype OpenAiTextContentPart = {\n readonly type: 'text'\n readonly text: string\n}\n\ntype OpenAiImageContentPart = {\n readonly type: 'image_url'\n readonly image_url: {\n readonly url: string\n }\n}\n\ntype OpenAiUserContent = string | ReadonlyArray<OpenAiTextContentPart | OpenAiImageContentPart>\n\ntype OpenAiToolCall = {\n readonly id: string\n readonly type: 'function'\n readonly function: {\n readonly name: string\n readonly arguments: string\n }\n}\n\ntype OpenAiMessage =\n | { readonly role: 'system'; readonly content: string }\n | { readonly role: 'user'; readonly content: OpenAiUserContent }\n | {\n readonly role: 'assistant'\n readonly content: string | null\n readonly tool_calls?: ReadonlyArray<OpenAiToolCall>\n }\n | { readonly role: 'tool'; readonly tool_call_id: string; readonly content: string }\n\ntype OpenAiTool = {\n readonly type: 'function'\n readonly function: {\n readonly name: string\n readonly description: string\n readonly parameters: unknown\n }\n}\n\ntype OpenAiRequestBody = {\n readonly model: string\n readonly messages: ReadonlyArray<OpenAiMessage>\n readonly max_completion_tokens: number\n readonly tools?: ReadonlyArray<OpenAiTool>\n readonly parallel_tool_calls?: true\n}\n\nclass OpenAiFunctionResponse extends Schema.Class<OpenAiFunctionResponse>('OpenAiFunctionResponse')(\n {\n name: Schema.String,\n arguments: Schema.String\n }\n) {}\n\nclass OpenAiToolCallResponse extends Schema.Class<OpenAiToolCallResponse>('OpenAiToolCallResponse')(\n {\n id: Schema.String,\n type: Schema.Literals(['function']),\n function: OpenAiFunctionResponse\n }\n) {}\n\nclass OpenAiMessageResponse extends Schema.Class<OpenAiMessageResponse>('OpenAiMessageResponse')({\n content: Schema.NullOr(Schema.String),\n tool_calls: Schema.optional(Schema.Array(OpenAiToolCallResponse))\n}) {}\n\nclass OpenAiChoiceResponse extends Schema.Class<OpenAiChoiceResponse>('OpenAiChoiceResponse')({\n message: OpenAiMessageResponse\n}) {}\n\nclass OpenAiPromptTokensDetails extends Schema.Class<OpenAiPromptTokensDetails>(\n 'OpenAiPromptTokensDetails'\n)({\n cached_tokens: Schema.optional(Schema.Number)\n}) {}\n\nclass OpenAiCompletionTokensDetails extends Schema.Class<OpenAiCompletionTokensDetails>(\n 'OpenAiCompletionTokensDetails'\n)({\n reasoning_tokens: Schema.optional(Schema.Number)\n}) {}\n\nclass OpenAiUsageResponse extends Schema.Class<OpenAiUsageResponse>('OpenAiUsageResponse')({\n prompt_tokens: Schema.Number,\n completion_tokens: Schema.Number,\n prompt_tokens_details: Schema.optional(OpenAiPromptTokensDetails),\n completion_tokens_details: Schema.optional(OpenAiCompletionTokensDetails)\n}) {}\n\nclass OpenAiChatCompletionResponse extends Schema.Class<OpenAiChatCompletionResponse>(\n 'OpenAiChatCompletionResponse'\n)({\n choices: Schema.Array(OpenAiChoiceResponse),\n usage: Schema.optional(OpenAiUsageResponse)\n}) {}\n\nclass OpenAiConfig extends Context.Service<OpenAiConfig, OpenAiProviderConfig>()(\n '@app/OpenAiConfig'\n) {}\n\nconst OpenAiConfigLayer = Layer.effect(\n OpenAiConfig,\n Effect.gen(function* () {\n const apiKey = yield* Config.redacted('OPENAI_API_KEY')\n return { apiKey }\n }).pipe(\n Effect.mapError(\n () =>\n new LLMError({\n cause: 'provider_error',\n message: 'OPENAI_API_KEY not found',\n retryable: false\n })\n )\n )\n)\n\nconst unknownToMessage = (error: unknown) =>\n error instanceof Error ? error.message : String(error)\n\nconst encodeJsonString = (value: unknown, message: string) =>\n Schema.encodeUnknownEffect(Schema.UnknownFromJsonString)(value).pipe(\n Effect.mapError(\n error =>\n new LLMError({\n cause: 'provider_error',\n message: `${message}: ${unknownToMessage(error)}`,\n retryable: false\n })\n )\n )\n\nconst decodeJsonString = (raw: string, message: string) =>\n Schema.decodeUnknownEffect(Schema.UnknownFromJsonString)(raw).pipe(\n Effect.mapError(\n error =>\n new LLMError({\n cause: 'invalid_response',\n message: `${message}: ${unknownToMessage(error)}`,\n retryable: false\n })\n )\n )\n\nconst unsupportedContentError = (contentType: string) =>\n new LLMError({\n cause: 'provider_error',\n message: `${contentType} content is not supported by the OpenAI provider yet`,\n retryable: false\n })\n\nconst textDocumentToOpenAiPart = (part: Extract<ContentPart, { readonly _tag: 'Document' }>) =>\n attachmentSourceText(part.source).pipe(\n Effect.mapError(() => unsupportedContentError('Invalid document text')),\n Effect.flatMap(\n Option.match({\n onNone: () => Effect.fail(unsupportedContentError('Unresolved document source')),\n onSome: text => {\n const block: OpenAiTextContentPart = {\n type: 'text',\n text: `Document: ${part.title ?? part.filename}\\n\\n${text}`\n }\n\n return Effect.succeed(block)\n }\n })\n )\n )\n\nconst contentPartToUserPart = (\n part: ContentPart\n): Effect.Effect<OpenAiTextContentPart | OpenAiImageContentPart, LLMError> => {\n switch (part._tag) {\n case 'Text':\n return Effect.succeed({ type: 'text', text: part.text })\n case 'Image':\n return Option.match(attachmentSourceUrl(part.source, part.mimeType), {\n onNone: () => Effect.fail(unsupportedContentError('Unresolved image source')),\n onSome: url => Effect.succeed({ type: 'image_url', image_url: { url } })\n })\n case 'Document':\n return isTextDocumentMimeType(part.mimeType)\n ? textDocumentToOpenAiPart(part)\n : Effect.fail(unsupportedContentError('Document'))\n case 'Audio':\n return Effect.fail(unsupportedContentError('Audio'))\n }\n}\n\nconst contentToUserContent = (content: Content): Effect.Effect<OpenAiUserContent, LLMError> =>\n typeof content === 'string'\n ? Effect.succeed(content)\n : Effect.forEach(content, contentPartToUserPart)\n\nconst contentPartToText = (part: ContentPart, owner: string): Effect.Effect<string, LLMError> => {\n switch (part._tag) {\n case 'Text':\n return Effect.succeed(part.text)\n case 'Image':\n return Effect.fail(unsupportedContentError(`${owner} image`))\n case 'Document':\n return Effect.fail(unsupportedContentError(`${owner} document`))\n case 'Audio':\n return Effect.fail(unsupportedContentError(`${owner} audio`))\n }\n}\n\nconst contentToText = (content: Content, owner: string): Effect.Effect<string, LLMError> =>\n typeof content === 'string'\n ? Effect.succeed(content)\n : Effect.forEach(content, part => contentPartToText(part, owner)).pipe(\n Effect.map(textParts => textParts.join('\\n'))\n )\n\nconst serializeToolArguments = (params: unknown) =>\n encodeJsonString(params, 'Could not serialize OpenAI tool arguments')\n\nconst toolCallToOpenAiToolCall = (call: ToolCall): Effect.Effect<OpenAiToolCall, LLMError> =>\n Effect.gen(function* () {\n return {\n id: call.id,\n type: 'function',\n function: {\n name: call.name,\n arguments: yield* serializeToolArguments(call.params)\n }\n }\n })\n\nconst toOpenAiMessage = (message: AgentMessage): Effect.Effect<OpenAiMessage, LLMError> =>\n Effect.gen(function* () {\n switch (message._tag) {\n case 'User':\n return {\n role: 'user',\n content: yield* contentToUserContent(\n prependMessageContextToContent(message.content, messageContextText(message))\n )\n }\n case 'Assistant': {\n const content = prependMessageContextToContent(\n assistantContent(message),\n messageContextText(message)\n )\n const toolCalls = yield* Effect.forEach(\n assistantHostToolCalls(message),\n toolCallToOpenAiToolCall\n )\n\n if (toolCalls.length > 0) {\n return {\n role: 'assistant',\n content: yield* contentToText(content, 'Assistant'),\n tool_calls: toolCalls\n }\n }\n\n return {\n role: 'assistant',\n content: yield* contentToText(content, 'Assistant')\n }\n }\n case 'ToolResult':\n return {\n role: 'tool',\n tool_call_id: message.toolCallId,\n content: yield* contentToText(\n prependMessageContextToContent(message.content, messageContextText(message)),\n 'Tool result'\n )\n }\n }\n })\n\nconst toOpenAiTool = (tool: ToolDef): OpenAiTool => ({\n type: 'function',\n function: {\n name: tool.name,\n description: tool.description,\n parameters: tool.parameters\n }\n})\n\nexport const toOpenAiRequestBody = (\n request: LLMRequest,\n config?: { readonly maxCompletionTokens?: number }\n): Effect.Effect<OpenAiRequestBody, LLMError> =>\n Effect.gen(function* () {\n yield* validateProviderTranscript(request.messages)\n const systemMessage: OpenAiMessage = { role: 'system', content: request.systemPrompt }\n const requestMessages = yield* Effect.forEach(request.messages, toOpenAiMessage)\n const messages = [systemMessage, ...requestMessages]\n\n const body = {\n model: request.model,\n messages,\n max_completion_tokens: config?.maxCompletionTokens ?? 4096\n }\n\n if (request.tools.length === 0) {\n return body\n }\n\n return {\n ...body,\n tools: request.tools.map(toOpenAiTool),\n parallel_tool_calls: true\n }\n })\n\nconst parseToolArguments = (raw: string) =>\n decodeJsonString(raw, 'Invalid OpenAI tool arguments JSON')\n\nconst toLlmEvents = (\n message: OpenAiMessageResponse\n): Effect.Effect<ReadonlyArray<LLMEvent>, LLMError> =>\n Effect.gen(function* () {\n const content = message.content ?? ''\n const textEvents = content.length > 0 ? [LLMTextDelta.make({ text: content })] : []\n const toolCallEvents = yield* Effect.forEach(message.tool_calls ?? [], call =>\n parseToolArguments(call.function.arguments).pipe(\n Effect.map(params =>\n LLMToolCall.make({\n call: ToolCall.make({\n id: call.id,\n name: call.function.name,\n params\n })\n })\n )\n )\n )\n\n if (toolCallEvents.length > 0) {\n return [...textEvents, ...toolCallEvents, LLMDone.make({ stopReason: 'tool_use' })]\n }\n\n return [...textEvents, LLMDone.make({ stopReason: 'stop' })]\n })\n\nconst toAgentUsage = (usage: OpenAiUsageResponse) =>\n AgentUsage.make({\n input: AgentInputUsage.make({\n total: usage.prompt_tokens,\n uncached: usage.prompt_tokens - (usage.prompt_tokens_details?.cached_tokens ?? 0),\n cacheRead: usage.prompt_tokens_details?.cached_tokens\n }),\n output: AgentOutputUsage.make({\n total: usage.completion_tokens,\n reasoning: usage.completion_tokens_details?.reasoning_tokens,\n text: usage.completion_tokens - (usage.completion_tokens_details?.reasoning_tokens ?? 0)\n })\n })\n\nconst toHttpClientLlmError =\n (message: string, retryable: boolean) => (error: HttpClientError.HttpClientError) =>\n new LLMError({\n cause: 'provider_error',\n message: `${message}: ${error.message}`,\n retryable,\n provider: providerErrorInfo({\n provider: 'openai',\n kind: retryable ? 'network' : 'unknown'\n })\n })\n\nconst parseOpenAiResponseJson = (\n response: HttpClientResponse.HttpClientResponse\n): Effect.Effect<unknown, LLMError> =>\n response.json.pipe(\n Effect.mapError(\n error =>\n new LLMError({\n cause: 'invalid_response',\n message: `Could not parse OpenAI response JSON: ${error.message}`,\n retryable: false\n })\n )\n )\n\nconst sendOpenAiRequest = (\n config: OpenAiProviderConfig,\n request: LLMRequest,\n client: HttpClient.HttpClient\n): Effect.Effect<ReadonlyArray<LLMEvent>, LLMError> =>\n Effect.gen(function* () {\n const body = yield* toOpenAiRequestBody(request, config)\n const serializedBody = yield* encodeJsonString(body, 'Could not serialize OpenAI request')\n\n const httpRequest = HttpClientRequest.post(config.chatCompletionsUrl ?? 'https://api.openai.com/v1/chat/completions').pipe(\n HttpClientRequest.setHeaders({\n accept: 'application/json',\n authorization: `Bearer ${Redacted.value(config.apiKey)}`,\n 'content-type': 'application/json',\n ...config.extraHeaders\n }),\n HttpClientRequest.bodyText(serializedBody, 'application/json')\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(Effect.mapError(toHttpClientLlmError('OpenAI request failed', true)))\n\n if (response.status < 200 || response.status >= 300) {\n const errorText = yield* response.text.pipe(\n Effect.mapError(\n error =>\n new LLMError({\n cause: 'provider_error',\n message: `Could not read OpenAI error body: ${error.message}`,\n retryable: false\n })\n )\n )\n\n const provider = classifyProviderFailure({\n provider: 'openai',\n status: response.status,\n headers: response.headers,\n body: errorText\n })\n\n return yield* Effect.fail(\n new LLMError({\n cause: providerFailureCause(provider.kind),\n message: `OpenAI returned ${response.status}`,\n retryable: providerFailureRetryable(provider.kind),\n provider\n })\n )\n }\n\n const json = yield* parseOpenAiResponseJson(response)\n const parsed = yield* Schema.decodeUnknownEffect(OpenAiChatCompletionResponse)(json).pipe(\n Effect.mapError(\n error =>\n new LLMError({\n cause: 'invalid_response',\n message: `Invalid OpenAI response: ${unknownToMessage(error)}`,\n retryable: false\n })\n )\n )\n const choice = parsed.choices[0]\n\n if (choice === undefined) {\n return yield* Effect.fail(\n new LLMError({\n cause: 'invalid_response',\n message: 'OpenAI response contained no choices',\n retryable: false\n })\n )\n }\n\n const events = yield* toLlmEvents(choice.message)\n\n if (parsed.usage === undefined) {\n return events\n }\n\n return [...events, LLMUsage.make({ usage: toAgentUsage(parsed.usage) })]\n }).pipe(Effect.withSpan('OpenAiProvider.stream'))\n\nexport const makeOpenAiProviderLayer = (config: OpenAiProviderConfig) =>\n Layer.effect(\n LLMProvider,\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return LLMProvider.of({\n stream: request =>\n Stream.fromEffect(sendOpenAiRequest(config, request, client)).pipe(\n Stream.flatMap(events => Stream.fromIterable(events))\n )\n })\n })\n )\n\nexport const OpenAiProviderLayer = Layer.effect(\n LLMProvider,\n Effect.gen(function* () {\n const config = yield* OpenAiConfig\n const client = yield* HttpClient.HttpClient\n\n return LLMProvider.of({\n stream: request =>\n Stream.fromEffect(sendOpenAiRequest(config, request, client)).pipe(\n Stream.flatMap(events => Stream.fromIterable(events))\n )\n })\n })\n).pipe(Layer.provide(Layer.mergeAll(OpenAiConfigLayer, FetchHttpClient.layer)))\n"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { AgentMessage } from "@yolk-sdk/agent/protocol";
2
+ import { Effect } from "effect";
3
+ import { LLMError } from "@yolk-sdk/agent/loop";
4
+
5
+ //#region src/providers/transcript.d.ts
6
+ declare const validateProviderTranscript: (messages: ReadonlyArray<AgentMessage>) => Effect.Effect<void, LLMError>;
7
+ //#endregion
8
+ export { validateProviderTranscript };
9
+ //# sourceMappingURL=transcript.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript.d.mts","names":[],"sources":["../../src/providers/transcript.ts"],"mappings":";;;;;cAOa,0BAAA,GACX,QAAA,EAAU,aAAA,CAAc,YAAA,MACvB,MAAA,CAAO,MAAA,OAAa,QAAA"}
@@ -0,0 +1,17 @@
1
+ import { validateNoDanglingHostToolCalls } from "@yolk-sdk/agent/protocol";
2
+ import { Effect } from "effect";
3
+ import { LLMError } from "@yolk-sdk/agent/loop";
4
+ //#region src/providers/transcript.ts
5
+ const validateProviderTranscript = (messages) => {
6
+ const validation = validateNoDanglingHostToolCalls(messages);
7
+ if (validation._tag === "Valid") return Effect.void;
8
+ return Effect.fail(new LLMError({
9
+ cause: "validation_error",
10
+ message: validation.message,
11
+ retryable: false
12
+ }));
13
+ };
14
+ //#endregion
15
+ export { validateProviderTranscript };
16
+
17
+ //# sourceMappingURL=transcript.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript.mjs","names":[],"sources":["../../src/providers/transcript.ts"],"sourcesContent":["import { Effect } from 'effect'\nimport {\n validateNoDanglingHostToolCalls,\n type AgentMessage\n} from '@yolk-sdk/agent/protocol'\nimport { LLMError } from '@yolk-sdk/agent/loop'\n\nexport const validateProviderTranscript = (\n messages: ReadonlyArray<AgentMessage>\n): Effect.Effect<void, LLMError> => {\n const validation = validateNoDanglingHostToolCalls(messages)\n\n if (validation._tag === 'Valid') {\n return Effect.void\n }\n\n return Effect.fail(\n new LLMError({\n cause: 'validation_error',\n message: validation.message,\n retryable: false\n })\n )\n}\n"],"mappings":";;;;AAOA,MAAa,8BACX,aACkC;CAClC,MAAM,aAAa,gCAAgC,QAAQ;CAE3D,IAAI,WAAW,SAAS,SACtB,OAAO,OAAO;CAGhB,OAAO,OAAO,KACZ,IAAI,SAAS;EACX,OAAO;EACP,SAAS,WAAW;EACpB,WAAW;CACb,CAAC,CACH;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yolk-sdk/agent",
3
- "version": "0.1.0-canary.21",
3
+ "version": "0.1.0-canary.23",
4
4
  "description": "Protocol, loop, runtime, client, compaction, tools, React, providers, OAuth, skillset, and voice primitives for building Yolk agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/loop/run.ts CHANGED
@@ -173,6 +173,52 @@ const subagentCompletedEvent = (input: {
173
173
  })
174
174
  }
175
175
 
176
+ const toolCompletionEvents = (input: {
177
+ readonly call: ToolCall
178
+ readonly result: ToolResult
179
+ readonly model: string
180
+ readonly startedAtMs: number
181
+ readonly endedAtMs: number
182
+ }): ReadonlyArray<AgentEvent> => {
183
+ const completed = subagentCompletedEvent(input)
184
+ const toolCompleted = ToolExecutionCompleted.make({
185
+ call: input.call,
186
+ result: input.result,
187
+ createdAtMs: input.endedAtMs
188
+ })
189
+
190
+ return completed === undefined ? [toolCompleted] : [toolCompleted, completed]
191
+ }
192
+
193
+ const toolErrorResult = (call: ToolCall, error: ToolError) =>
194
+ ToolResult.make({
195
+ toolCallId: call.id,
196
+ content: error.message,
197
+ isError: true
198
+ })
199
+
200
+ const toolErrorEvents = (input: {
201
+ readonly call: ToolCall
202
+ readonly error: ToolError
203
+ readonly model: string
204
+ readonly startedAtMs: number
205
+ readonly endedAtMs: number
206
+ }): ReadonlyArray<AgentEvent> => [
207
+ ToolExecutionError.make({
208
+ call: input.call,
209
+ message: input.error.message,
210
+ code: toolErrorCode(input.error),
211
+ createdAtMs: input.endedAtMs
212
+ }),
213
+ ...toolCompletionEvents({
214
+ call: input.call,
215
+ result: toolErrorResult(input.call, input.error),
216
+ model: input.model,
217
+ startedAtMs: input.startedAtMs,
218
+ endedAtMs: input.endedAtMs
219
+ })
220
+ ]
221
+
176
222
  const unsupportedInputError = (message: string) =>
177
223
  new LLMError({
178
224
  cause: 'validation_error',
@@ -400,22 +446,15 @@ const makeToolExecutionStream = (
400
446
  executor.execute(call).pipe(
401
447
  Effect.flatMap(result =>
402
448
  Clock.currentTimeMillis.pipe(
403
- Effect.map(endedAtMs => {
404
- const completed = subagentCompletedEvent({
449
+ Effect.map(endedAtMs =>
450
+ toolCompletionEvents({
405
451
  call,
406
452
  result,
407
453
  model,
408
454
  startedAtMs,
409
455
  endedAtMs
410
456
  })
411
- const toolCompleted = ToolExecutionCompleted.make({
412
- call,
413
- result,
414
- createdAtMs: endedAtMs
415
- })
416
-
417
- return completed === undefined ? [toolCompleted] : [toolCompleted, completed]
418
- })
457
+ )
419
458
  )
420
459
  )
421
460
  )
@@ -424,16 +463,16 @@ const makeToolExecutionStream = (
424
463
  Stream.catchTag('ToolError', error =>
425
464
  Stream.fromEffect(Clock.currentTimeMillis).pipe(
426
465
  Stream.flatMap(endedAtMs =>
427
- Stream.make(
428
- ToolExecutionError.make({
466
+ Stream.fromIterable(
467
+ toolErrorEvents({
429
468
  call,
430
- message: error.message,
431
- code: toolErrorCode(error),
432
- createdAtMs: endedAtMs
469
+ error,
470
+ model,
471
+ startedAtMs,
472
+ endedAtMs
433
473
  })
434
474
  )
435
- ),
436
- Stream.concat(Stream.fail(error))
475
+ )
437
476
  )
438
477
  )
439
478
  )
@@ -201,6 +201,17 @@ export const attachmentSourceDataUrl = (source: AttachmentSource, mimeType: stri
201
201
  }
202
202
  }
203
203
 
204
+ export const attachmentSourceUrl = (source: AttachmentSource, mimeType: string) => {
205
+ switch (source._tag) {
206
+ case 'InlineBase64':
207
+ return Option.some(`data:${mimeType};base64,${source.data}`)
208
+ case 'Url':
209
+ return Option.some(source.url)
210
+ case 'Ref':
211
+ return Option.none<string>()
212
+ }
213
+ }
214
+
204
215
  const normalizeMimeType = (mimeType: string) => mimeType.split(';', 1)[0]?.trim().toLowerCase() ?? ''
205
216
 
206
217
  const textDocumentMimeTypeByExtension: Readonly<Record<string, string>> = {
@@ -11,6 +11,7 @@ export {
11
11
  attachmentSourceDataUrl,
12
12
  AttachmentSource,
13
13
  attachmentSourceText,
14
+ attachmentSourceUrl,
14
15
  type AttachmentContentPart,
15
16
  type AttachmentSourceResolver,
16
17
  attachmentSourcePreview,
@@ -96,8 +97,16 @@ export {
96
97
  assistantContent,
97
98
  assistantHostToolCalls,
98
99
  assistantReasoningText,
100
+ danglingHostToolCalls,
99
101
  messageContextText,
100
- prependMessageContextToContent
102
+ prependMessageContextToContent,
103
+ repairDanglingHostToolCalls,
104
+ validateNoDanglingHostToolCalls
105
+ } from './message.ts'
106
+ export type {
107
+ DanglingHostToolCall,
108
+ RepairDanglingHostToolCallsOptions,
109
+ TranscriptInvariantValidation
101
110
  } from './message.ts'
102
111
  export { AgentReasoningEffort } from './reasoning.ts'
103
112
  export {
@@ -87,6 +87,25 @@ export class ToolResultMessage extends Schema.TaggedClass<ToolResultMessage>()('
87
87
  export const AgentMessage = Schema.Union([UserMessage, AssistantAgentMessage, ToolResultMessage])
88
88
  export type AgentMessage = typeof AgentMessage.Type
89
89
 
90
+ export type DanglingHostToolCall = {
91
+ readonly call: ToolCall
92
+ readonly assistantMessageIndex: number
93
+ readonly beforeMessageIndex?: number
94
+ }
95
+
96
+ export type TranscriptInvariantValidation =
97
+ | { readonly _tag: 'Valid' }
98
+ | {
99
+ readonly _tag: 'DanglingHostToolCalls'
100
+ readonly calls: ReadonlyArray<DanglingHostToolCall>
101
+ readonly message: string
102
+ }
103
+
104
+ export type RepairDanglingHostToolCallsOptions = {
105
+ readonly content?: (call: ToolCall) => Content
106
+ readonly structuredContent?: (call: ToolCall) => unknown
107
+ }
108
+
90
109
  export const assistantContent = (message: AssistantAgentMessage): Content => {
91
110
  const parts = message.parts.flatMap(part => (part._tag === 'Text' ? [part.content] : []))
92
111
  const first = parts[0]
@@ -108,6 +127,114 @@ export const assistantReasoningText = (message: AssistantAgentMessage) =>
108
127
  export const assistantHostToolCalls = (message: AssistantAgentMessage) =>
109
128
  message.parts.flatMap(part => (part._tag === 'HostToolCall' ? [part.call] : []))
110
129
 
130
+ type PendingHostToolCall = {
131
+ readonly call: ToolCall
132
+ readonly assistantMessageIndex: number
133
+ }
134
+
135
+ const pendingHostToolCalls = (message: AgentMessage, messageIndex: number) =>
136
+ message._tag === 'Assistant'
137
+ ? assistantHostToolCalls(message).map(call => ({ call, assistantMessageIndex: messageIndex }))
138
+ : []
139
+
140
+ const danglingHostToolCall = (
141
+ pending: PendingHostToolCall,
142
+ beforeMessageIndex: number | undefined
143
+ ): DanglingHostToolCall => ({
144
+ call: pending.call,
145
+ assistantMessageIndex: pending.assistantMessageIndex,
146
+ ...(beforeMessageIndex === undefined ? {} : { beforeMessageIndex })
147
+ })
148
+
149
+ const danglingHostToolCallSummary = (calls: ReadonlyArray<DanglingHostToolCall>) =>
150
+ calls.map(({ call }) => `${call.name} (${call.id})`).join(', ')
151
+
152
+ const danglingHostToolResultContent = (call: ToolCall) =>
153
+ `Tool ${call.name} did not return a result before the transcript continued.`
154
+
155
+ const danglingHostToolResultMessage = (
156
+ call: ToolCall,
157
+ options: RepairDanglingHostToolCallsOptions | undefined
158
+ ) => {
159
+ const structuredContent = options?.structuredContent?.(call)
160
+
161
+ return ToolResultMessage.make({
162
+ toolCallId: call.id,
163
+ content: options?.content?.(call) ?? danglingHostToolResultContent(call),
164
+ isError: true,
165
+ ...(structuredContent === undefined ? {} : { structuredContent })
166
+ })
167
+ }
168
+
169
+ export const danglingHostToolCalls = (
170
+ messages: ReadonlyArray<AgentMessage>
171
+ ): ReadonlyArray<DanglingHostToolCall> => {
172
+ const dangling: Array<DanglingHostToolCall> = []
173
+ let pending: ReadonlyArray<PendingHostToolCall> = []
174
+
175
+ for (const [messageIndex, message] of messages.entries()) {
176
+ if (message._tag !== 'ToolResult' && pending.length > 0) {
177
+ dangling.push(...pending.map(call => danglingHostToolCall(call, messageIndex)))
178
+ pending = []
179
+ }
180
+
181
+ pending = [...pending, ...pendingHostToolCalls(message, messageIndex)]
182
+
183
+ if (message._tag === 'ToolResult') {
184
+ pending = pending.filter(call => call.call.id !== message.toolCallId)
185
+ }
186
+ }
187
+
188
+ dangling.push(...pending.map(call => danglingHostToolCall(call, undefined)))
189
+
190
+ return dangling
191
+ }
192
+
193
+ export const validateNoDanglingHostToolCalls = (
194
+ messages: ReadonlyArray<AgentMessage>
195
+ ): TranscriptInvariantValidation => {
196
+ const dangling = danglingHostToolCalls(messages)
197
+
198
+ if (dangling.length === 0) {
199
+ return { _tag: 'Valid' }
200
+ }
201
+
202
+ return {
203
+ _tag: 'DanglingHostToolCalls',
204
+ calls: dangling,
205
+ message: `Transcript has host tool calls without tool results: ${danglingHostToolCallSummary(dangling)}`
206
+ }
207
+ }
208
+
209
+ export const repairDanglingHostToolCalls = (
210
+ messages: ReadonlyArray<AgentMessage>,
211
+ options?: RepairDanglingHostToolCallsOptions
212
+ ): ReadonlyArray<AgentMessage> => {
213
+ const repaired: Array<AgentMessage> = []
214
+ let pending: ReadonlyArray<ToolCall> = []
215
+
216
+ for (const message of messages) {
217
+ if (message._tag !== 'ToolResult' && pending.length > 0) {
218
+ repaired.push(...pending.map(call => danglingHostToolResultMessage(call, options)))
219
+ pending = []
220
+ }
221
+
222
+ repaired.push(message)
223
+
224
+ if (message._tag === 'Assistant') {
225
+ pending = [...pending, ...assistantHostToolCalls(message)]
226
+ }
227
+
228
+ if (message._tag === 'ToolResult') {
229
+ pending = pending.filter(call => call.id !== message.toolCallId)
230
+ }
231
+ }
232
+
233
+ repaired.push(...pending.map(call => danglingHostToolResultMessage(call, options)))
234
+
235
+ return repaired
236
+ }
237
+
111
238
  const formatCreatedAtMs = (createdAtMs: number) =>
112
239
  Number.isFinite(createdAtMs) ? new Date(createdAtMs).toISOString() : String(createdAtMs)
113
240
 
@@ -11,7 +11,6 @@ import {
11
11
  AgentOutputUsage,
12
12
  AgentUsage,
13
13
  ToolCall,
14
- attachmentSourceBase64,
15
14
  attachmentSourceText,
16
15
  assistantContent,
17
16
  assistantHostToolCalls,
@@ -48,6 +47,7 @@ import {
48
47
  providerFailureCause,
49
48
  providerFailureRetryable
50
49
  } from '../provider-error.ts'
50
+ import { validateProviderTranscript } from '../transcript.ts'
51
51
 
52
52
  export type AnthropicClaudeProviderConfig = {
53
53
  readonly token: OAuthAccessToken
@@ -65,20 +65,30 @@ type AnthropicSystemBlock = AnthropicTextBlock
65
65
 
66
66
  type AnthropicImageBlock = {
67
67
  readonly type: 'image'
68
- readonly source: {
69
- readonly type: 'base64'
70
- readonly media_type: string
71
- readonly data: string
72
- }
68
+ readonly source:
69
+ | {
70
+ readonly type: 'base64'
71
+ readonly media_type: string
72
+ readonly data: string
73
+ }
74
+ | {
75
+ readonly type: 'url'
76
+ readonly url: string
77
+ }
73
78
  }
74
79
 
75
80
  type AnthropicDocumentBlock = {
76
81
  readonly type: 'document'
77
- readonly source: {
78
- readonly type: 'base64'
79
- readonly media_type: 'application/pdf'
80
- readonly data: string
81
- }
82
+ readonly source:
83
+ | {
84
+ readonly type: 'base64'
85
+ readonly media_type: 'application/pdf'
86
+ readonly data: string
87
+ }
88
+ | {
89
+ readonly type: 'url'
90
+ readonly url: string
91
+ }
82
92
  readonly title?: string
83
93
  }
84
94
 
@@ -442,6 +452,60 @@ const unsupportedContentError = (contentType: string) =>
442
452
  retryable: false
443
453
  })
444
454
 
455
+ const imageToAnthropicBlock = (
456
+ part: Extract<ContentPart, { readonly _tag: 'Image' }>
457
+ ): Effect.Effect<AnthropicImageBlock, LLMError> => {
458
+ switch (part.source._tag) {
459
+ case 'InlineBase64':
460
+ return Effect.succeed({
461
+ type: 'image',
462
+ source: {
463
+ type: 'base64',
464
+ media_type: part.mimeType,
465
+ data: part.source.data
466
+ }
467
+ })
468
+ case 'Url':
469
+ return Effect.succeed({
470
+ type: 'image',
471
+ source: {
472
+ type: 'url',
473
+ url: part.source.url
474
+ }
475
+ })
476
+ case 'Ref':
477
+ return Effect.fail(unsupportedContentError('Unresolved image source'))
478
+ }
479
+ }
480
+
481
+ const pdfDocumentToAnthropicBlock = (
482
+ part: Extract<ContentPart, { readonly _tag: 'Document' }>
483
+ ): Effect.Effect<AnthropicDocumentBlock, LLMError> => {
484
+ switch (part.source._tag) {
485
+ case 'InlineBase64':
486
+ return Effect.succeed({
487
+ type: 'document',
488
+ source: {
489
+ type: 'base64',
490
+ media_type: 'application/pdf',
491
+ data: part.source.data
492
+ },
493
+ title: part.title ?? part.filename
494
+ })
495
+ case 'Url':
496
+ return Effect.succeed({
497
+ type: 'document',
498
+ source: {
499
+ type: 'url',
500
+ url: part.source.url
501
+ },
502
+ title: part.title ?? part.filename
503
+ })
504
+ case 'Ref':
505
+ return Effect.fail(unsupportedContentError('Unresolved document source'))
506
+ }
507
+ }
508
+
445
509
  const textDocumentToAnthropicBlock = (part: Extract<ContentPart, { readonly _tag: 'Document' }>) =>
446
510
  attachmentSourceText(part.source).pipe(
447
511
  Effect.mapError(() => unsupportedContentError('Invalid document text')),
@@ -465,33 +529,12 @@ const contentPartToUserBlock = (part: ContentPart): Effect.Effect<AnthropicUserB
465
529
  case 'Text':
466
530
  return Effect.succeed({ type: 'text', text: part.text })
467
531
  case 'Image':
468
- return Option.match(attachmentSourceBase64(part.source), {
469
- onNone: () => Effect.fail(unsupportedContentError('Unresolved image source')),
470
- onSome: data => Effect.succeed({
471
- type: 'image',
472
- source: {
473
- type: 'base64',
474
- media_type: part.mimeType,
475
- data
476
- }
477
- })
478
- })
532
+ return imageToAnthropicBlock(part)
479
533
  case 'Document':
480
534
  return isTextDocumentMimeType(part.mimeType)
481
535
  ? textDocumentToAnthropicBlock(part)
482
536
  : part.mimeType === 'application/pdf'
483
- ? Option.match(attachmentSourceBase64(part.source), {
484
- onNone: () => Effect.fail(unsupportedContentError('Unresolved document source')),
485
- onSome: data => Effect.succeed({
486
- type: 'document',
487
- source: {
488
- type: 'base64',
489
- media_type: 'application/pdf',
490
- data
491
- },
492
- title: part.title ?? part.filename
493
- })
494
- })
537
+ ? pdfDocumentToAnthropicBlock(part)
495
538
  : Effect.fail(unsupportedContentError(`Document ${part.mimeType}`))
496
539
  case 'Audio':
497
540
  return Effect.fail(unsupportedContentError('Audio'))
@@ -802,6 +845,7 @@ export const toAnthropicClaudeRequestBody = (
802
845
  config?: { readonly maxTokens?: number; readonly stream?: boolean }
803
846
  ): Effect.Effect<AnthropicRequestBody, LLMError> =>
804
847
  Effect.gen(function* () {
848
+ yield* validateProviderTranscript(request.messages)
805
849
  const rawMessages = yield* Effect.forEach(request.messages, toAnthropicMessage)
806
850
  const billingSystemBlock = yield* makeAnthropicClaudeBillingSystemBlock(rawMessages)
807
851
  const messages = prependSystemPromptToFirstUserMessage(rawMessages, request.systemPrompt)