Repository navigation
fix(openai-base): run the computer use loop #1831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
07228a7
d43c6ec
98cb24c
04cf74d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@tanstack/openai-base': patch | ||
| --- | ||
|
|
||
| The computer use loop of `computerUseTool()` runs. A `computer_call` from the model is now a tool call to `computer_use_preview`, with the `action` and the `pending_safety_checks` as its input. The tool result goes back as `computer_call_output`, with the first image of the result as the screenshot. When the call does not fail, the output acknowledges the safety checks of the call. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,20 @@ | ||
| import type { ResponseInputItem } from 'openai/resources/responses/responses' | ||
| import type { | ||
| ResponseComputerToolCall, | ||
| ResponseComputerToolCallOutputScreenshot, | ||
| ResponseInputItem, | ||
| } from 'openai/resources/responses/responses' | ||
|
|
||
| /** | ||
| * OpenAI Responses tools the app must run. | ||
| * A container `shell` runs on the provider. `apply_patch`, `local_shell`, | ||
| * and `shell` with `environment.type: "local"` (or no environment) do not. | ||
| * `computer_use_preview`, and `shell` with `environment.type: "local"` (or | ||
| * no environment) do not. | ||
| */ | ||
| export type OpenAIUserToolName = 'shell' | 'apply_patch' | 'local_shell' | ||
| export type OpenAIUserToolName = | ||
| | 'shell' | ||
| | 'apply_patch' | ||
| | 'local_shell' | ||
| | 'computer_use_preview' | ||
|
|
||
| export interface OpenAIUserExecutedCall { | ||
| name: OpenAIUserToolName | ||
|
|
@@ -71,12 +80,47 @@ export function hostedShellCallIds( | |
| export function readUserToolName(metadata: unknown): OpenAIUserToolName | null { | ||
| if (!isRecord(metadata)) return null | ||
| const name = metadata.openaiUserTool | ||
| if (name === 'shell' || name === 'apply_patch' || name === 'local_shell') { | ||
| if ( | ||
| name === 'shell' || | ||
| name === 'apply_patch' || | ||
| name === 'local_shell' || | ||
| name === 'computer_use_preview' | ||
| ) { | ||
| return name | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| /** | ||
| * A computer action. It goes back to the API as the model sent it, so only | ||
| * its `type` is checked. | ||
| */ | ||
| function isComputerAction( | ||
| value: unknown, | ||
| ): value is NonNullable<ResponseComputerToolCall['action']> { | ||
| return isRecord(value) && typeof value.type === 'string' | ||
| } | ||
|
|
||
| /** The safety checks of a computer call, with only their known fields. */ | ||
| function safetyChecks( | ||
| value: unknown, | ||
| ): Array<ResponseComputerToolCall.PendingSafetyCheck> { | ||
| if (!Array.isArray(value)) return [] | ||
| return value.flatMap((check) => | ||
| isRecord(check) && typeof check.id === 'string' | ||
| ? [ | ||
| { | ||
| id: check.id, | ||
| ...(typeof check.code === 'string' ? { code: check.code } : {}), | ||
| ...(typeof check.message === 'string' | ||
| ? { message: check.message } | ||
| : {}), | ||
| }, | ||
| ] | ||
| : [], | ||
| ) | ||
| } | ||
|
|
||
| function readItemId(metadata: unknown): string | undefined { | ||
| if (!isRecord(metadata)) return undefined | ||
| return typeof metadata.itemId === 'string' && metadata.itemId.length > 0 | ||
|
|
@@ -135,6 +179,19 @@ export function readUserExecutedCall( | |
| } | ||
| } | ||
|
|
||
| if (item.type === 'computer_call') { | ||
| if (!callId || !isComputerAction(item.action)) return null | ||
| return { | ||
| name: 'computer_use_preview', | ||
| callId, | ||
| ...(itemId ? { itemId } : {}), | ||
| input: { | ||
| action: item.action, | ||
| pending_safety_checks: safetyChecks(item.pending_safety_checks), | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| if (item.type === 'local_shell_call') { | ||
| if (!isRecord(item.action)) return null | ||
| const command = stringList(item.action.command) | ||
|
|
@@ -234,6 +291,18 @@ export function userToolRequestItem( | |
| } | ||
| } | ||
|
|
||
| if (name === 'computer_use_preview') { | ||
| if (!isComputerAction(args.action)) return null | ||
| return { | ||
| type: 'computer_call', | ||
| id: itemId ?? toolCall.id, | ||
| call_id: toolCall.id, | ||
| status: 'completed', | ||
| action: args.action, | ||
| pending_safety_checks: safetyChecks(args.pending_safety_checks), | ||
| } | ||
| } | ||
|
|
||
| const commands = stringList(args.commands) | ||
| if (!commands) return null | ||
| return { | ||
|
|
@@ -294,13 +363,38 @@ function shellOutputList(content: unknown): Array<{ | |
| return [shellEntry(JSON.stringify(parsed ?? ''))] | ||
| } | ||
|
|
||
| /** Replay the app's tool result as the matching Responses output item. */ | ||
| /** | ||
| * Replay the app's tool result as the matching Responses output item. | ||
| * `computer` is what a `computer_use_preview` result needs: the screenshot, | ||
| * and whether the call failed. Only a computer result reads the screenshot. | ||
| * The output of a call that did not fail acknowledges the safety checks of | ||
| * the call. | ||
| */ | ||
| export function userToolResultItem( | ||
| toolCall: ToolCallLike, | ||
| content: unknown, | ||
| computer?: { | ||
| screenshot: () => ResponseComputerToolCallOutputScreenshot | ||
| failed: boolean | ||
| }, | ||
| ): ResponseInputItem | null { | ||
| const name = readUserToolName(toolCall.metadata) | ||
| if (!name) return null | ||
|
|
||
| if (name === 'computer_use_preview') { | ||
| const checks = computer?.failed | ||
| ? [] | ||
| : safetyChecks( | ||
| parseArguments(toolCall.function.arguments).pending_safety_checks, | ||
| ) | ||
| return { | ||
| type: 'computer_call_output', | ||
| call_id: toolCall.id, | ||
| output: computer?.screenshot() ?? { type: 'computer_screenshot' }, | ||
| ...(checks.length > 0 ? { acknowledged_safety_checks: checks } : {}), | ||
|
Comment on lines
+390
to
+394
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Do not treat a missing screenshot as a successful observation. If an 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| const parsed = parseContent(content) | ||
|
|
||
| if (name === 'apply_patch') { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Convert screenshots only for computer results.
If a
shell,local_shell, orapply_patchresult contains a file-source image, this new call invokesconvertContentPartToInput. An adapter without file-source support throws beforeuserToolResultItemcan serialize that non-computer result. Previously, those result paths did not convert an image to a screenshot. CallscreenshotOfonly when the owner iscomputer_use_preview.🤖 Prompt for AI Agents