@opencode-ai/ai
Advanced tools
@@ -220,2 +220,7 @@ import { Effect, Option, Schema } from "effect"; | ||
| }); | ||
| // The spec requires `id` on every output item, but some gateways drop it from | ||
| // later item events (Bedrock Mantle renames it to `item_id` on | ||
| // `output_item.done` and `response.completed.output`). Decode it as optional | ||
| // and let `normalize` recover or mint it once before the parser runs. | ||
| // https://www.openresponses.org/specification#extending-items | ||
| export const StreamItem = Schema.StructWithRest(Schema.Struct({ | ||
@@ -645,3 +650,2 @@ type: Schema.String, | ||
| }); | ||
| const isReasoningItem = (item) => item.type === "reasoning" && typeof item.id === "string"; | ||
| const NO_EVENTS = []; | ||
@@ -679,3 +683,24 @@ // `response.completed` / `response.incomplete` are clean finishes that emit a | ||
| }; | ||
| export const outputItemID = (state, event) => event.output_index === undefined ? event.item_id : (state.outputItems[event.output_index] ?? event.item_id); | ||
| const outputItemID = (state, event) => event.output_index === undefined ? event.item_id : (state.outputItems[event.output_index] ?? event.item_id); | ||
| const ITEM_ID_PREFIX = { | ||
| message: "msg", | ||
| reasoning: "rs", | ||
| function_call: "fc", | ||
| compaction: "cmp", | ||
| }; | ||
| // An item without an id adopts the id already open in its output slot, | ||
| // otherwise it gets a locally minted one. | ||
| const resolveItem = (state, item, index) => ({ | ||
| ...item, | ||
| id: item.id ?? | ||
| (index === undefined ? undefined : state.outputItems[index]) ?? | ||
| `${ITEM_ID_PREFIX[item.type] ?? "item"}_${crypto.randomUUID().replaceAll("-", "")}`, | ||
| }); | ||
| // Registered output slots are authoritative for `item_id` routing, and items | ||
| // are resolved here so everything downstream can rely on `item.id`. | ||
| export const normalize = (state, input) => ({ | ||
| ...input, | ||
| item_id: input.item_id === undefined ? undefined : outputItemID(state, input), | ||
| item: input.item ? resolveItem(state, input.item, input.output_index) : input.item, | ||
| }); | ||
| const startReasoningSummaryPart = (state, itemID, index) => { | ||
@@ -756,9 +781,10 @@ const item = state.reasoningItems[itemID]; | ||
| const item = event.item; | ||
| if (item?.type === "message" && item.id !== undefined) { | ||
| const itemID = item.id; | ||
| if (state.completedMessages.has(itemID)) | ||
| if (!item) | ||
| return [state, NO_EVENTS]; | ||
| if (item.type === "message") { | ||
| if (state.completedMessages.has(item.id)) | ||
| return [state, NO_EVENTS]; | ||
| const phase = messagePhase(item.phase); | ||
| const completedMessages = new Set(state.completedMessages); | ||
| if (state.message !== undefined && state.message.id !== itemID) | ||
| if (state.message !== undefined && state.message.id !== item.id) | ||
| completedMessages.add(state.message.id); | ||
@@ -768,3 +794,3 @@ // A new message closes earlier messages, including ones that never streamed. | ||
| const lifecycle = [...state.lifecycle.text] | ||
| .filter((id) => id !== itemID) | ||
| .filter((id) => id !== item.id) | ||
| .reduce((lifecycle, id) => { | ||
@@ -781,4 +807,4 @@ completedMessages.add(id); | ||
| message: { | ||
| id: itemID, | ||
| phase: phase === undefined && state.message?.id === itemID ? state.message.phase : phase, | ||
| id: item.id, | ||
| phase: phase === undefined && state.message?.id === item.id ? state.message.phase : phase, | ||
| }, | ||
@@ -789,3 +815,3 @@ }, | ||
| } | ||
| if (item && isReasoningItem(item)) { | ||
| if (item.type === "reasoning") { | ||
| if (state.reasoningItems[item.id] !== undefined) | ||
@@ -811,8 +837,7 @@ return [state, NO_EVENTS]; | ||
| } | ||
| if (item?.type !== "function_call" || !item.call_id) | ||
| if (item.type !== "function_call" || !item.call_id) | ||
| return [state, NO_EVENTS]; | ||
| const id = item.id ?? item.call_id; | ||
| if (Object.values(state.tools).some((tool) => tool?.id === item.call_id) || state.completedTools.has(item.call_id)) | ||
| if (state.tools[item.id] !== undefined || state.completedTools.has(item.id)) | ||
| return [state, NO_EVENTS]; | ||
| const metadata = item.id !== undefined ? providerMetadata(state, { itemId: item.id }) : undefined; | ||
| const metadata = providerMetadata(state, { itemId: item.id }); | ||
| const events = []; | ||
@@ -824,3 +849,3 @@ const lifecycle = Lifecycle.stepStart(state.lifecycle, events); | ||
| lifecycle, | ||
| tools: ToolStream.start(state.tools, id, { | ||
| tools: ToolStream.start(state.tools, item.id, { | ||
| id: item.call_id, | ||
@@ -894,4 +919,4 @@ name: item.name ?? "", | ||
| if (item.type === "compaction") { | ||
| if (!item.id || typeof item.encrypted_content !== "string") | ||
| return yield* ProviderShared.eventError(state.id, "Compaction output is missing its id or encrypted content"); | ||
| if (typeof item.encrypted_content !== "string") | ||
| return yield* ProviderShared.eventError(state.id, "Compaction output is missing its encrypted content"); | ||
| if (state.completedCompactions.has(item.id)) | ||
@@ -911,3 +936,3 @@ return [state, NO_EVENTS]; | ||
| } | ||
| if (item.type === "message" && item.id !== undefined) { | ||
| if (item.type === "message") { | ||
| if (state.completedMessages.has(item.id)) | ||
@@ -947,29 +972,18 @@ return [state, NO_EVENTS]; | ||
| return [state, NO_EVENTS]; | ||
| const callID = item.call_id; | ||
| if (state.completedTools.has(callID)) | ||
| if (state.completedTools.has(item.id)) | ||
| return [state, NO_EVENTS]; | ||
| const metadata = item.id !== undefined ? providerMetadata(state, { itemId: item.id }) : undefined; | ||
| const fallback = item.id ?? callID; | ||
| // Match the pending tool by call id so item events that disagree on | ||
| // whether `item.id` is present still resolve the same call. | ||
| const registered = state.tools[fallback] !== undefined | ||
| ? fallback | ||
| : Object.keys(state.tools).find((key) => state.tools[key]?.id === callID); | ||
| const id = registered ?? fallback; | ||
| const tools = registered !== undefined | ||
| const metadata = providerMetadata(state, { itemId: item.id }); | ||
| const registered = state.tools[item.id] !== undefined; | ||
| const tools = registered | ||
| ? state.tools | ||
| : ToolStream.start(state.tools, id, { | ||
| id: callID, | ||
| name: item.name, | ||
| providerMetadata: metadata, | ||
| }); | ||
| : ToolStream.start(state.tools, item.id, { id: item.call_id, name: item.name, providerMetadata: metadata }); | ||
| const result = item.arguments === undefined | ||
| ? yield* ToolStream.finish(state.id, tools, id) | ||
| : yield* ToolStream.finishWithInput(state.id, tools, id, item.arguments); | ||
| ? yield* ToolStream.finish(state.id, tools, item.id) | ||
| : yield* ToolStream.finishWithInput(state.id, tools, item.id, item.arguments); | ||
| const events = []; | ||
| const finished = result.events ?? []; | ||
| // A done-only call never streamed a start event, so open its lifecycle here. | ||
| const resultEvents = registered !== undefined || finished.length === 0 | ||
| const resultEvents = registered || finished.length === 0 | ||
| ? finished | ||
| : [LLMEvent.toolInputStart({ id: callID, name: item.name, providerMetadata: metadata }), ...finished]; | ||
| : [LLMEvent.toolInputStart({ id: item.call_id, name: item.name, providerMetadata: metadata }), ...finished]; | ||
| const lifecycle = resultEvents.length ? Lifecycle.stepStart(state.lifecycle, events) : state.lifecycle; | ||
@@ -984,3 +998,3 @@ events.push(...resultEvents); | ||
| tools: result.tools, | ||
| completedTools: new Set([...state.completedTools, callID]), | ||
| completedTools: new Set([...state.completedTools, item.id]), | ||
| }, | ||
@@ -990,3 +1004,3 @@ events, | ||
| } | ||
| if (isReasoningItem(item)) { | ||
| if (item.type === "reasoning") { | ||
| if (state.reasoningItems[item.id]?.open === false) | ||
@@ -1074,13 +1088,10 @@ return [state, NO_EVENTS]; | ||
| if (event.type === "response.completed") { | ||
| for (const item of event.response?.output ?? []) { | ||
| if (item.type !== "compaction" && item.type !== "function_call") | ||
| // An output item's array position is its output index. | ||
| for (const item of (event.response?.output ?? []).map((item, index) => resolveItem(state, item, index))) { | ||
| // Terminal recovery cannot insert a checkpoint before already-emitted content. | ||
| if (item.type === "compaction" && state.lifecycle.stepStarted && !state.completedCompactions.has(item.id)) | ||
| return yield* ProviderShared.eventError(state.id, "Cannot recover a compaction checkpoint after output has been emitted"); | ||
| const recoverable = item.type === "compaction" || (item.type === "function_call" && current.tools[item.id] !== undefined); | ||
| if (!recoverable) | ||
| continue; | ||
| if (item.type === "compaction") { | ||
| // Terminal recovery cannot insert a checkpoint before already-emitted content. | ||
| if (state.lifecycle.stepStarted && !state.completedCompactions.has(item.id ?? "")) | ||
| return yield* ProviderShared.eventError(state.id, "Cannot recover a compaction checkpoint after output has been emitted"); | ||
| } | ||
| if (item.type === "function_call" && | ||
| (!item.call_id || !Object.values(current.tools).some((tool) => tool?.id === item.call_id))) | ||
| continue; | ||
| const [next, emitted] = yield* onOutputItemDone(current, item); | ||
@@ -1146,7 +1157,5 @@ current = next; | ||
| }; | ||
| export const step = (state, input) => { | ||
| // The OpenAPI requires string IDs but imposes no minLength; empty is not missing. | ||
| const event = input.item_id !== undefined && outputItemID(state, input) !== input.item_id | ||
| ? { ...input, item_id: outputItemID(state, input) } | ||
| : input; | ||
| // Callers must pass events through `normalize` first. The OpenAPI requires | ||
| // string IDs but imposes no minLength; empty is not missing. | ||
| export const step = (state, event) => { | ||
| if (event.type === "response.output_text.delta" || event.type === "response.output_text.done") { | ||
@@ -1188,12 +1197,8 @@ if (event.item_id === undefined) | ||
| if (event.type === "response.output_item.added") { | ||
| if (event.item?.type === "message" && event.item.id === undefined) | ||
| return ProviderShared.eventError(state.id, `${event.type} message is missing id`); | ||
| if (event.item && | ||
| isReasoningItem(event.item) && | ||
| if (event.item?.type === "reasoning" && | ||
| state.reasoningItems[event.item.id] === undefined && | ||
| state.lifecycle.reasoning.size > 0) | ||
| return ProviderShared.eventError(state.id, `${event.type} started reasoning before the previous item ended`); | ||
| const id = event.item?.id ?? (event.item?.type === "function_call" ? event.item.call_id : undefined); | ||
| return Effect.succeed(onOutputItemAdded(event.output_index !== undefined && id !== undefined | ||
| ? { ...state, outputItems: { ...state.outputItems, [event.output_index]: id } } | ||
| return Effect.succeed(onOutputItemAdded(event.output_index !== undefined && event.item | ||
| ? { ...state, outputItems: { ...state.outputItems, [event.output_index]: event.item.id } } | ||
| : state, event)); | ||
@@ -1205,7 +1210,4 @@ } | ||
| : ProviderShared.eventError(state.id, `${event.type} is missing item_id`); | ||
| if (event.type === "response.output_item.done") { | ||
| if (event.item?.type === "message" && event.item.id === undefined) | ||
| return ProviderShared.eventError(state.id, `${event.type} message is missing id`); | ||
| if (event.type === "response.output_item.done") | ||
| return onOutputItemDone(state, event.item); | ||
| } | ||
| if (event.type === "response.completed" || event.type === "response.incomplete") | ||
@@ -1250,3 +1252,3 @@ return onResponseFinish(state, event); | ||
| initial, | ||
| step, | ||
| step: (state, event) => step(state, normalize(state, event)), | ||
| terminal, | ||
@@ -1253,0 +1255,0 @@ }, |
@@ -157,6 +157,7 @@ import { Effect, Encoding, Schema } from "effect"; | ||
| }; | ||
| const step = (state, event) => { | ||
| const step = (state, input) => { | ||
| const event = OpenResponses.normalize(state, input); | ||
| if (event.type === "response.reasoning_text.delta") | ||
| return event.item_id !== undefined | ||
| ? Effect.succeed(OpenResponses.onReasoningDelta(state, event, OpenResponses.outputItemID(state, event) ?? event.item_id)) | ||
| ? Effect.succeed(OpenResponses.onReasoningDelta(state, event, event.item_id)) | ||
| : ProviderShared.eventError(ADAPTER, `${event.type} is missing item_id`); | ||
@@ -163,0 +164,0 @@ if (event.type === "response.output_item.done" && event.item && ResponsesHostedTools.isItem(event.item, HOSTED_TOOLS)) |
| import { Effect } from "effect"; | ||
| import { type AIError, type ToolResultPart } from "../../schema/index.js"; | ||
| import { OpenResponses } from "../open-responses.js"; | ||
| export type Item = OpenResponses.StreamItem & { | ||
| readonly id: string; | ||
| export type Item = OpenResponses.OutputItem & { | ||
| readonly status?: string; | ||
@@ -25,4 +24,4 @@ readonly action?: unknown; | ||
| export type Definitions = Readonly<Record<string, Definition>>; | ||
| export declare const isItem: <Tools extends Definitions>(item: OpenResponses.StreamItem, tools: Tools) => item is Item; | ||
| export declare const isItem: <Tools extends Definitions>(item: OpenResponses.OutputItem, tools: Tools) => item is Item; | ||
| export declare const onDone: (state: OpenResponses.ParserState, item: Item, tools: Definitions) => Effect.Effect<OpenResponses.StepResult, AIError>; | ||
| export * as ResponsesHostedTools from "./responses-hosted-tools.js"; |
@@ -5,3 +5,3 @@ import { Effect } from "effect"; | ||
| import { Lifecycle } from "./lifecycle.js"; | ||
| export const isItem = (item, tools) => item.type in tools && typeof item.id === "string" && item.id.length > 0; | ||
| export const isItem = (item, tools) => item.type in tools; | ||
| export const onDone = Effect.fn("ResponsesHostedTools.onDone")(function* (state, item, tools) { | ||
@@ -8,0 +8,0 @@ const tool = tools[item.type]; |
@@ -56,3 +56,4 @@ import { Effect, Schema } from "effect"; | ||
| // handled by the baseline); only its hosted tool vocabulary differs. | ||
| const step = (state, event) => { | ||
| const step = (state, input) => { | ||
| const event = OpenResponses.normalize(state, input); | ||
| if (event.type === "response.output_item.done" && event.item && ResponsesHostedTools.isItem(event.item, HOSTED_TOOLS)) | ||
@@ -59,0 +60,0 @@ return ResponsesHostedTools.onDone(state, event.item, HOSTED_TOOLS); |
+3
-3
| { | ||
| "$schema": "https://json.schemastore.org/package.json", | ||
| "version": "0.0.0-beta-18965", | ||
| "version": "0.0.0-beta-18985", | ||
| "name": "@opencode-ai/ai", | ||
@@ -33,3 +33,3 @@ "type": "module", | ||
| "@effect/platform-node": "4.0.0-rc.112", | ||
| "@opencode-ai/http-recorder": "0.0.0-beta-18965", | ||
| "@opencode-ai/http-recorder": "0.0.0-beta-18985", | ||
| "@tsconfig/bun": "1.0.9", | ||
@@ -43,3 +43,3 @@ "@types/bun": "1.3.13", | ||
| "@smithy/util-utf8": "4.2.2", | ||
| "@opencode-ai/schema": "0.0.0-beta-18965", | ||
| "@opencode-ai/schema": "0.0.0-beta-18985", | ||
| "aws4fetch": "1.0.20", | ||
@@ -46,0 +46,0 @@ "effect": "4.0.0-rc.112", |
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1658529
037827
0.02%+ Added
- Removed