diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4e64c332..8e0bc281d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -242,8 +242,11 @@ Image-bearing messages require a qualified profile/placement before persistence and image support from the selected Runtime before native delivery. These checks apply to that operation only; ordinary text retains offline queueing. Initial, prepared and active paths use the same content and preserve receipt ownership. -The qualified public profile is inline PNG/JPEG on Codex/Claude `none`; workspace -images, MiniMax images and remote URLs remain explicit implementation gaps. Core +The qualified public profile is inline PNG/JPEG on Codex/Claude `none` and +Core-managed Docker `openai_hosted`. Self-hosted/user-managed images, MiniMax +images and remote URLs remain explicit implementation gaps. Hosted images reuse +the existing preparation, active-input and workspace authority; they do not add +a downloader, a mount or a separate execution lifecycle. Core does not fetch or transform media. See [message input coverage](contracts/agents-api/message-input.md). User-managed onboarding creates a `self_hosted` Session first, then passes its @@ -2264,8 +2267,8 @@ Idle and initial-input Session creation qualify the resolved configuration befor persistence; saved Agent resources remain independent of engine restrictions. Claude additionally requires medium verbosity and explicit object-root function schemas. Function-result batches normalize through the existing shared parser. Claude accepts -text results and, on `none`, successful ordered inline PNG/JPEG results; -workspace images, failed image results and +text results and, on `none` and Core-managed Docker `openai_hosted`, successful +ordered inline PNG/JPEG results. Unqualified placements, failed image results and invalid/remote references reject before any batch write, preserving pending calls and retry identity. Public qualification receives the full neutral result so success-dependent limitations remain in the profile. Image-bearing delivery alone diff --git a/contracts/agents-api/README.md b/contracts/agents-api/README.md index ae9b8f68e..d0d352226 100644 --- a/contracts/agents-api/README.md +++ b/contracts/agents-api/README.md @@ -418,7 +418,7 @@ operation and placement; native support is not public admission by itself. | Engine | Qualified placements and limits | | --- | --- | | `codex` (default) | Qualified `none` and Docker `openai_hosted`; public functions with ordered text/image results; service-origin HTTP MCP on `none` only; verbosity follows native policy | -| `claude_sdk` | Qualified `none` and Docker `openai_hosted`; medium verbosity, object-root function schemas and text-only results; qualified anonymous/static-bearer service-origin HTTP MCP on `none` | +| `claude_sdk` | Qualified `none` and Docker `openai_hosted`; medium verbosity, object-root function schemas and text or successful inline PNG/JPEG results; qualified anonymous/static-bearer service-origin HTTP MCP on `none` | | `mcode` | Qualified `none` text and Docker `openai_hosted`; medium verbosity; public functions/service-origin MCP, image input and complete public usage breakdown remain unsupported | All three profiles implement user-managed `self_hosted` enrollment at `/workspace` @@ -654,7 +654,8 @@ inheritance uses the same resolved tools. The bounded [deferred discovery path]( adds type-only `tool_search` for its qualified profile. Other discovery combinations, other tool kinds, the native 64-definition cap and unique nonblank names of at most 512 bytes remain compatibility gaps. Claude SDK additionally requires object-root schemas. It accepts text and -successful inline PNG/JPEG function results on `none`; failed/workspace images and remote references +successful inline PNG/JPEG function results on `none` and Docker `openai_hosted`; +failed images, unqualified placements and remote references remain gaps. See [function image coverage](function-result-images.md). Codex internal Goal/Skills/user-input/discovery semantics need upstream evidence; their presence alone does not prove a tool-set mismatch. diff --git a/contracts/agents-api/function-result-images.md b/contracts/agents-api/function-result-images.md index 61e468a56..54a038f70 100644 --- a/contracts/agents-api/function-result-images.md +++ b/contracts/agents-api/function-result-images.md @@ -2,7 +2,8 @@ The pinned official function result accepts a string or ordered text/image content, independently of success. Our qualified Claude subset is successful inline PNG/JPEG -on `environment:none`. Error images, workspace images and remote URLs reject before +on `environment:none` and Core-managed Docker `openai_hosted`. Error images, +unqualified placements and remote URLs reject before batch persistence, without consuming the pending call. These are implementation gaps, not narrower official types. MiniMax public functions remain unqualified. @@ -50,7 +51,13 @@ unsupported placement, operation-specific Runtime support and batch atomicity. The bundled JPEG fixture has yellow, blue, red and green vertical bands; it contains no metadata or credentials. PNG markers are generated with randomized band order. +The [Docker workspace workflow](message-input.md#docker-workspace-acceptance) +uses the same function-result contract alongside native file tools, public +Files/Artifacts and cold Core/Runtime continuation. It checks Claude's rejected +error/remote result directly on an outstanding call before accepting a valid +image on that same call; no mixed-message rejection substitutes for this check. + The accepted combinations and run evidence are recorded in the task board. This -batch does not qualify workspace image results, all native image limits, provider +coverage does not qualify self-hosted/user-managed image results, all native image limits, provider parity, arbitrary managed output rewrites, crash recovery or full Agents API compatibility. No downloader, image converter or second tool loop belongs in Core. diff --git a/contracts/agents-api/harnesses.md b/contracts/agents-api/harnesses.md index 0dba1093b..5ed53135c 100644 --- a/contracts/agents-api/harnesses.md +++ b/contracts/agents-api/harnesses.md @@ -72,17 +72,18 @@ syntactically or everything either upstream harness can theoretically perform. | Docker hosted text execution, native local tools | Qualified | Qualified | | Files, immutable Artifacts, cancellation, restart/history recovery | Qualified | Qualified | | Public functions in `none` | Qualified | Qualified; object-root schemas; text or successful inline PNG/JPEG results | -| Public functions alongside hosted workspace tools | Qualified | Qualified; object-root schemas and text results | +| Public functions alongside hosted workspace tools | Qualified | Qualified; object-root schemas and text or successful inline PNG/JPEG results | | HTTP MCP and static-bearer Vault credentials in `none` | Qualified | Qualified subset | | Required MCP initialization | Qualified | Qualified on `none`; native readiness before initial input | | Hosted HTTP MCP | Gap | Gap | -| Function image results | Supported subset; early acknowledgement is transport-only | Successful inline PNG/JPEG on `none`; native resizing allowed, error/workspace images rejected | +| Function image results | Supported subset; early acknowledgement is transport-only | Successful inline PNG/JPEG on `none` and Docker `openai_hosted`; native resizing allowed, error images rejected | | Non-default verbosity | Native/model-dependent support | No equivalent qualified; medium only | | Public detailed Usage | Supported native counters | Native raw usage retained; public breakdown gap | | V1 `self_hosted` daemon enrollment at `/workspace` | [Qualified deployment scope](user-managed-runtime-v1.md) | [Qualified deployment scope](user-managed-runtime-v1.md) | | Deferred function discovery | Unqualified; explicit rejection | [Single-agent text/function profile](tool-search.md) | | Structured output | Unqualified; explicit rejection | [Qualified single-agent function profile](structured-output.md) | -| Explicit reasoning, message images | Shared service gaps | Shared service gaps | +| Message images | Inline PNG/JPEG on `none` and Docker `openai_hosted` | Inline PNG/JPEG on `none` and Docker `openai_hosted` | +| Explicit reasoning | Shared service gap | Shared service gap | | Six Subagent reads | [Qualified scope](subagents.md) | [Qualified scope](subagents.md) | This inventory records supported combinations, not a feature-equality checklist. diff --git a/contracts/agents-api/message-input.md b/contracts/agents-api/message-input.md index 4e5b4ae79..bfaee72e7 100644 --- a/contracts/agents-api/message-input.md +++ b/contracts/agents-api/message-input.md @@ -7,8 +7,8 @@ events share validation and atomic admission. ## Supported profile -Codex and Claude SDK support inline PNG/JPEG data URIs on `environment:none`, for -initial and active input and subsequent Turns. Use a real vision-capable model. +Codex and Claude SDK support inline PNG/JPEG data URIs on `environment:none` and +Core-managed Docker `openai_hosted`, for initial, prepared and active input. Use a real vision-capable model. The existing 1 MiB HTTP and 512 KiB durable input limits still apply. A successful events response acknowledges persistence, not native consumption. Active input advances its durable receipt only after the adapter confirms application. @@ -90,11 +90,50 @@ five repetitions, and shared input/dispatch race checks passed. These checks do not qualify workspace images or additional native/provider combinations. Native-only probes are feasibility evidence, not public qualification. -Workspace image workflows, MiniMax Code image input, remote HTTP(S) image URLs, +## Docker workspace acceptance + +`tests/official_workspace_images_native.py` exposes `verify_workspace_images` +for an operator-owned standalone deployment. Supply the fixed SDK clients for +two tenants, their raw HTTP transport, a real vision model, the selected harness, +a cold Core/Runtime restart callback and a private evidence path. It creates and +deletes its own hosted Sessions; it never supplies model responses or credentials. + +The common workflow covers initial text/PNG/text input, prepared image-only JPEG +followed by a separate message, active PNG input while a function waits, and a +6000x2100 PNG function result. The model must read the band order from image pixels +and write the corresponding bytes with native tools. Files listing and immutable +Artifact downloads verify those bytes. SDK and HTTP Items must retain the original +ordered input/results. The same workflow checks retry/conflict, unsupported-input +non-mutation, tenant and same-tenant Session isolation, cold history continuation, +pending cancellation and ordinary text after cancellation. + +Real Kimi K3 acceptance on 2026-09-22 passed this workflow for Codex 0.153.4 and +Claude SDK 0.3.269/native 2.1.269. Evidence is retained under +`zju_a100_2:~/.parsar/remediation/20260922/workspace-images/`: +`codex/public-run-_lr5uiy_/` (152.78s) and +`claude_sdk/public-run-sb65p9e9/` (197.84s). Each run completed seven public Turns, +including one cancelled Turn, and cleaned up both owned hosted Sessions. Initial +Codex attempts exposed two acceptance-script errors: treating an earlier idle +event as the submitted Turn's completion and sending a scalar message to the +array-only events endpoint in a conflict probe. Both failures are retained; +production lifecycle behavior was not changed to obtain passing results. + +Here `openai_hosted` means the Core-managed Docker deployment, with daemon, native +harness, tools and workspace in one sandbox. Image admission uses Environment type +and the common operation-specific Runtime support; it adds no provider-name branch, +media downloader, file permission or preparation lifecycle. Docker evidence does +not qualify other providers or user-managed deployment. Claude keeps its native +image-result receipt; Codex retains its documented transport-only result +acknowledgement. Neither implies crash-safe exactly-once tool effects. + +## Remaining gaps + +Self-hosted/user-managed image workflows, MiniMax Code image input, remote HTTP(S) image URLs, other media types and full upstream error/default semantics remain unqualified. MiniMax's fixed ACP advertises `image:false`; its adapter rejects images. These are implementation gaps, not changes to the official protocol. JPEG parsing/conversion -has deterministic coverage; the recorded real visual fixtures are PNG. Empty +has deterministic coverage; the original `none` message fixtures are PNG, and +the hosted workflow also exercises JPEG. Empty messages, local payload limits and native batch-size parity need upstream evidence. -Function-result image support is unchanged by this batch. No full protocol +Function-result image support has its own [coverage record](function-result-images.md). No full protocol compatibility or support for arbitrary vision-model/provider combinations is claimed. diff --git a/contracts/agents-api/openapi.yaml b/contracts/agents-api/openapi.yaml index ecf80aaba..be0a2b2a5 100644 --- a/contracts/agents-api/openapi.yaml +++ b/contracts/agents-api/openapi.yaml @@ -2683,53 +2683,54 @@ paths: keys or failed credential lookup/decryption never fall back to anonymous execution. Omitted stream defaults to false; stream and agent_id cannot be null. Metadata may be null, but its values must be strings. Initial input accepts a string - or ordered user-message array. Codex and Claude SDK on none also accept inline - PNG/JPEG image content; other image combinations and remote URLs are unsupported. - None initial input atomically starts a Turn; self_hosted initial input is - reserved while returning its Environment connection target, with execution - deferred to native readiness and Session failure on initial timeout. Omitted - or null input creates an idle Session. With stream=true, returns live Session - events starting at creation; disconnect does not cancel execution. New Sessions - retain their authenticated creator; all creation retries require the same - typed subject, including across key rotation. Saved-Agent retries and inline - requests using Vault attachments or credential references retain caller intent - independently of later resource changes; unrelated inline retries preserve - resolved/default equivalences. Unknown historical creators reject retries; - known creators without recorded intent retain resolved-snapshot retry rules. - These conflict policies are local and not verified hosted parity. Creation - retries observe future events without replay; retry with stream=false to retrieve - the Session. Claude SDK environment:none supports qualified object-root json_schema - output with medium verbosity, single-Agent execution and ordinary functions; - other combinations remain unsupported. Non-text initial input remains unsupported. - Basic Codex and Claude SDK openai_hosted creation requires an explicitly configured - managed provider. The Claude workspace profile supports non-deferred function - tools with text results alongside native workspace tools; HTTP MCP remains - unsupported. Idle Sessions provision automatically; initial provisioning has - no caller connection action. Network defaults to enabled; disabled and restricted - exact ASCII hostnames are supported. Restricted policy requires 1–100 allowed - domains. Unsupported hostname forms and startup installations are rejected. - Confidential env, system/npm/Python packages and ordered setup commands use - the shared initialization lifecycle; requested network applies after setup. - Initial inline and tenant-owned file_id files freeze encrypted bytes before - provisioning, then install through the common Core lifecycle before native - execution or live Files access. Referenced files/env/packages/setup overrides - are rejected pending semantic verification. Tenant-owned environment_template_id - references inherit omitted network and allow only narrowing overrides. Referenced - network:null is explicitly unsupported pending semantic verification. Core - freezes effective configuration; template updates/deletion do not alter Session - snapshots or same-intent creation retries. Inline or tenant-owned skill_reference - Skills share initialization. Templates preserve default/latest/explicit selectors; - Session creation freezes concrete metadata and encrypted content atomically. - Skill-list omission inherits and a supplied list replaces; null overrides - and null version selectors remain unqualified and reject. Source deletion/default - updates cannot change committed Session Skill contents. Deferred function - discovery uses type-only tool_search and per-function defer_loading in the - qualified single-agent Claude environment:none function profile, including - qualified inline image messages and text results. Explicit web_search mode - disabled and programmatic_tool_calling enabled false use frozen common Runtime - controls. Enabled forms remain unqualified. Omitted programmatic configuration - preserves native behavior, a documented difference from the official default-on - behavior. Other combinations remain unqualified; see the operation coverage. + or ordered user-message array. Codex and Claude SDK on none and qualified + openai_hosted also accept inline PNG/JPEG image content; other image combinations + and remote URLs are unsupported. None initial input atomically starts a Turn; + self_hosted initial input is reserved while returning its Environment connection + target, with execution deferred to native readiness and Session failure on + initial timeout. Omitted or null input creates an idle Session. With stream=true, + returns live Session events starting at creation; disconnect does not cancel + execution. New Sessions retain their authenticated creator; all creation retries + require the same typed subject, including across key rotation. Saved-Agent + retries and inline requests using Vault attachments or credential references + retain caller intent independently of later resource changes; unrelated inline + retries preserve resolved/default equivalences. Unknown historical creators + reject retries; known creators without recorded intent retain resolved-snapshot + retry rules. These conflict policies are local and not verified hosted parity. + Creation retries observe future events without replay; retry with stream=false + to retrieve the Session. Claude SDK environment:none supports qualified object-root + json_schema output with medium verbosity, single-Agent execution and ordinary + functions; other combinations remain unsupported. Other non-text initial input + remains unsupported. Basic Codex and Claude SDK openai_hosted creation requires + an explicitly configured managed provider. The Claude workspace profile supports + non-deferred function tools with text or successful inline PNG/JPEG results + alongside native workspace tools; HTTP MCP remains unsupported. Idle Sessions + provision automatically; initial provisioning has no caller connection action. + Network defaults to enabled; disabled and restricted exact ASCII hostnames + are supported. Restricted policy requires 1–100 allowed domains. Unsupported + hostname forms and startup installations are rejected. Confidential env, system/npm/Python + packages and ordered setup commands use the shared initialization lifecycle; + requested network applies after setup. Initial inline and tenant-owned file_id + files freeze encrypted bytes before provisioning, then install through the + common Core lifecycle before native execution or live Files access. Referenced + files/env/packages/setup overrides are rejected pending semantic verification. + Tenant-owned environment_template_id references inherit omitted network and + allow only narrowing overrides. Referenced network:null is explicitly unsupported + pending semantic verification. Core freezes effective configuration; template + updates/deletion do not alter Session snapshots or same-intent creation retries. + Inline or tenant-owned skill_reference Skills share initialization. Templates + preserve default/latest/explicit selectors; Session creation freezes concrete + metadata and encrypted content atomically. Skill-list omission inherits and + a supplied list replaces; null overrides and null version selectors remain + unqualified and reject. Source deletion/default updates cannot change committed + Session Skill contents. Deferred function discovery uses type-only tool_search + and per-function defer_loading in the qualified single-agent Claude environment:none + function profile, including qualified inline image messages and text results. + Explicit web_search mode disabled and programmatic_tool_calling enabled false + use frozen common Runtime controls. Enabled forms remain unqualified. Omitted + programmatic configuration preserves native behavior, a documented difference + from the official default-on behavior. Other combinations remain unqualified; + see the operation coverage. parameters: - description: agents=v1 in: header @@ -3221,31 +3222,33 @@ paths: - application/json description: For environment none, atomically accepts text messages, cancellation and function results. Messages steer active work or start a queued Turn. The - supported self_hosted and openai_hosted profiles accept text-only batches. - Under the Session lock, matching retries retain their original target; new - active messages append to the current Turn, while idle messages reserve work - and wait up to the original five-minute connection/admission deadline. Return - 204 only after durable admission, without claiming native application; active - messages create no Turn or reservation. Cancellation-only prepared-environment - batches use existing durable cancellation admission and return 204 without - waiting for native exit; a new cancellation conflicts while a pre-Turn reservation - is pending. Homogeneous tool_result-only prepared-environment batches reuse - existing scoped result admission and application receipts without creating - a Turn or bypassing a pending reservation. Mixed prepared-environment batches - remain unsupported. HTTP expiry/cancellation use local 409 environment_input_expired/environment_input_cancelled + supported self_hosted profile accepts text-only messages; qualified Codex + and Claude SDK openai_hosted profiles also accept inline PNG/JPEG. Under the + Session lock, matching retries retain their original target; new active messages + append to the current Turn, while idle messages reserve work and wait up to + the original five-minute connection/admission deadline. Return 204 only after + durable admission, without claiming native application; active messages create + no Turn or reservation. Cancellation-only prepared-environment batches use + existing durable cancellation admission and return 204 without waiting for + native exit; a new cancellation conflicts while a pre-Turn reservation is + pending. Homogeneous tool_result-only prepared-environment batches reuse existing + scoped result admission and application receipts without creating a Turn or + bypassing a pending reservation. Mixed prepared-environment batches remain + unsupported. HTTP expiry/cancellation use local 409 environment_input_expired/environment_input_cancelled errors; exact hosted failure mapping is unverified. Losing execution ownership returns 503. The response write deadline accommodates the admission window for either prepared Environment, independently of new-hosted-admission and executor URL settings. Disconnecting the waiting HTTP request does not cancel retained work or restart its deadline. Retry keys identify the whole ordered batch. Function output accepts text or ordered text/image parts subject to - engine support; Claude SDK accepts text results and, on none, successful inline - PNG/JPEG results, preserving ordered content; error images and remote references - reject before admission. Native image resizing may change bytes. Runtime image-result - support is checked only for image-bearing delivery. Codex and Claude SDK on - none accept ordered inline PNG/JPEG image messages. Workspace profiles and - other engines remain text-only; remote image URLs are unsupported. Image references - are retained unchanged without service-side downloads. + engine support; Claude SDK accepts text results and, on none and qualified + openai_hosted, successful inline PNG/JPEG results, preserving ordered content; + error images and remote references reject before admission. Native image resizing + may change bytes. Runtime image-result support is checked only for image-bearing + delivery. Codex and Claude SDK on none and qualified openai_hosted accept + ordered inline PNG/JPEG image messages. Self-hosted profiles and other engines + remain text-only; remote image URLs are unsupported. Image references are + retained unchanged without service-side downloads. parameters: - description: agents=v1 in: header diff --git a/services/agents-api/internal/api/handler.go b/services/agents-api/internal/api/handler.go index 3aa075e3e..24255c410 100644 --- a/services/agents-api/internal/api/handler.go +++ b/services/agents-api/internal/api/handler.go @@ -124,7 +124,7 @@ func NewHandler(s ResourceStore, auth *Authenticator, engine string, options ... // createSession atomically reserves or admits initial text with the Session. // @Summary Create an execution Session -// @Description Supports inline configuration or a tenant-owned saved agent_id with per-Session field replacements. Execution supports model/instructions, text verbosity, non-deferred function tools, adapter-qualified multi_agent with persisted Subagent reads, implicit reasoning, service tier auto and environment type none, subject to the configured engine. Codex additionally supports HTTP MCP with explicit service origin, native allowed_tools and boolean required defaulting to false. Session vault_ids attach only project-owned Vaults; credential_id selects an attached static bearer credential for the exact HTTPS URL, while null/omission selects a unique match or remains anonymous. Ambiguous selection rejects creation. Frozen private selections never populate an omitted public credential_id; missing decryption configuration fails dispatch without anonymous fallback. Required initialization uses native startup before the first native Turn, including cold resume, and requires a separately advertised capability; exact hosted creation timing and error parity remain unverified. Other MCP origins and OAuth remain unsupported. The self_hosted profile requires Codex, an absolute workspace_directory and empty capability_directories, with optional non-deferred function tools and HTTP MCP using explicit service origin, optionally authenticated by the attached Vault rules. Remote MCP and remote Bearer authentication each require separately advertised combination support; old peers cannot receive unsupported work. Omitted/null capability_directories use the empty-list default; self_hosted requires configured execution plus executor registry. Claude SDK currently requires medium verbosity and object-root function schemas. It supports anonymous or attached static-bearer service-origin HTTP MCP on none with boolean required and separately advertised MCP/bearer/required runtime support. Required servers must be connected before the first native input is released; pending or failed startup rejects execution. The shared Vault selection and immutable binding rules apply; unsupported native labels/tool names reject before persistence. An attached Vault with no matching credential may remain anonymous; missing keys or failed credential lookup/decryption never fall back to anonymous execution. Omitted stream defaults to false; stream and agent_id cannot be null. Metadata may be null, but its values must be strings. Initial input accepts a string or ordered user-message array. Codex and Claude SDK on none also accept inline PNG/JPEG image content; other image combinations and remote URLs are unsupported. None initial input atomically starts a Turn; self_hosted initial input is reserved while returning its Environment connection target, with execution deferred to native readiness and Session failure on initial timeout. Omitted or null input creates an idle Session. With stream=true, returns live Session events starting at creation; disconnect does not cancel execution. New Sessions retain their authenticated creator; all creation retries require the same typed subject, including across key rotation. Saved-Agent retries and inline requests using Vault attachments or credential references retain caller intent independently of later resource changes; unrelated inline retries preserve resolved/default equivalences. Unknown historical creators reject retries; known creators without recorded intent retain resolved-snapshot retry rules. These conflict policies are local and not verified hosted parity. Creation retries observe future events without replay; retry with stream=false to retrieve the Session. Claude SDK environment:none supports qualified object-root json_schema output with medium verbosity, single-Agent execution and ordinary functions; other combinations remain unsupported. Non-text initial input remains unsupported. Basic Codex and Claude SDK openai_hosted creation requires an explicitly configured managed provider. The Claude workspace profile supports non-deferred function tools with text results alongside native workspace tools; HTTP MCP remains unsupported. Idle Sessions provision automatically; initial provisioning has no caller connection action. Network defaults to enabled; disabled and restricted exact ASCII hostnames are supported. Restricted policy requires 1–100 allowed domains. Unsupported hostname forms and startup installations are rejected. Confidential env, system/npm/Python packages and ordered setup commands use the shared initialization lifecycle; requested network applies after setup. Initial inline and tenant-owned file_id files freeze encrypted bytes before provisioning, then install through the common Core lifecycle before native execution or live Files access. Referenced files/env/packages/setup overrides are rejected pending semantic verification. Tenant-owned environment_template_id references inherit omitted network and allow only narrowing overrides. Referenced network:null is explicitly unsupported pending semantic verification. Core freezes effective configuration; template updates/deletion do not alter Session snapshots or same-intent creation retries. Inline or tenant-owned skill_reference Skills share initialization. Templates preserve default/latest/explicit selectors; Session creation freezes concrete metadata and encrypted content atomically. Skill-list omission inherits and a supplied list replaces; null overrides and null version selectors remain unqualified and reject. Source deletion/default updates cannot change committed Session Skill contents. Deferred function discovery uses type-only tool_search and per-function defer_loading in the qualified single-agent Claude environment:none function profile, including qualified inline image messages and text results. Explicit web_search mode disabled and programmatic_tool_calling enabled false use frozen common Runtime controls. Enabled forms remain unqualified. Omitted programmatic configuration preserves native behavior, a documented difference from the official default-on behavior. Other combinations remain unqualified; see the operation coverage. +// @Description Supports inline configuration or a tenant-owned saved agent_id with per-Session field replacements. Execution supports model/instructions, text verbosity, non-deferred function tools, adapter-qualified multi_agent with persisted Subagent reads, implicit reasoning, service tier auto and environment type none, subject to the configured engine. Codex additionally supports HTTP MCP with explicit service origin, native allowed_tools and boolean required defaulting to false. Session vault_ids attach only project-owned Vaults; credential_id selects an attached static bearer credential for the exact HTTPS URL, while null/omission selects a unique match or remains anonymous. Ambiguous selection rejects creation. Frozen private selections never populate an omitted public credential_id; missing decryption configuration fails dispatch without anonymous fallback. Required initialization uses native startup before the first native Turn, including cold resume, and requires a separately advertised capability; exact hosted creation timing and error parity remain unverified. Other MCP origins and OAuth remain unsupported. The self_hosted profile requires Codex, an absolute workspace_directory and empty capability_directories, with optional non-deferred function tools and HTTP MCP using explicit service origin, optionally authenticated by the attached Vault rules. Remote MCP and remote Bearer authentication each require separately advertised combination support; old peers cannot receive unsupported work. Omitted/null capability_directories use the empty-list default; self_hosted requires configured execution plus executor registry. Claude SDK currently requires medium verbosity and object-root function schemas. It supports anonymous or attached static-bearer service-origin HTTP MCP on none with boolean required and separately advertised MCP/bearer/required runtime support. Required servers must be connected before the first native input is released; pending or failed startup rejects execution. The shared Vault selection and immutable binding rules apply; unsupported native labels/tool names reject before persistence. An attached Vault with no matching credential may remain anonymous; missing keys or failed credential lookup/decryption never fall back to anonymous execution. Omitted stream defaults to false; stream and agent_id cannot be null. Metadata may be null, but its values must be strings. Initial input accepts a string or ordered user-message array. Codex and Claude SDK on none and qualified openai_hosted also accept inline PNG/JPEG image content; other image combinations and remote URLs are unsupported. None initial input atomically starts a Turn; self_hosted initial input is reserved while returning its Environment connection target, with execution deferred to native readiness and Session failure on initial timeout. Omitted or null input creates an idle Session. With stream=true, returns live Session events starting at creation; disconnect does not cancel execution. New Sessions retain their authenticated creator; all creation retries require the same typed subject, including across key rotation. Saved-Agent retries and inline requests using Vault attachments or credential references retain caller intent independently of later resource changes; unrelated inline retries preserve resolved/default equivalences. Unknown historical creators reject retries; known creators without recorded intent retain resolved-snapshot retry rules. These conflict policies are local and not verified hosted parity. Creation retries observe future events without replay; retry with stream=false to retrieve the Session. Claude SDK environment:none supports qualified object-root json_schema output with medium verbosity, single-Agent execution and ordinary functions; other combinations remain unsupported. Other non-text initial input remains unsupported. Basic Codex and Claude SDK openai_hosted creation requires an explicitly configured managed provider. The Claude workspace profile supports non-deferred function tools with text or successful inline PNG/JPEG results alongside native workspace tools; HTTP MCP remains unsupported. Idle Sessions provision automatically; initial provisioning has no caller connection action. Network defaults to enabled; disabled and restricted exact ASCII hostnames are supported. Restricted policy requires 1–100 allowed domains. Unsupported hostname forms and startup installations are rejected. Confidential env, system/npm/Python packages and ordered setup commands use the shared initialization lifecycle; requested network applies after setup. Initial inline and tenant-owned file_id files freeze encrypted bytes before provisioning, then install through the common Core lifecycle before native execution or live Files access. Referenced files/env/packages/setup overrides are rejected pending semantic verification. Tenant-owned environment_template_id references inherit omitted network and allow only narrowing overrides. Referenced network:null is explicitly unsupported pending semantic verification. Core freezes effective configuration; template updates/deletion do not alter Session snapshots or same-intent creation retries. Inline or tenant-owned skill_reference Skills share initialization. Templates preserve default/latest/explicit selectors; Session creation freezes concrete metadata and encrypted content atomically. Skill-list omission inherits and a supplied list replaces; null overrides and null version selectors remain unqualified and reject. Source deletion/default updates cannot change committed Session Skill contents. Deferred function discovery uses type-only tool_search and per-function defer_loading in the qualified single-agent Claude environment:none function profile, including qualified inline image messages and text results. Explicit web_search mode disabled and programmatic_tool_calling enabled false use frozen common Runtime controls. Enabled forms remain unqualified. Omitted programmatic configuration preserves native behavior, a documented difference from the official default-on behavior. Other combinations remain unqualified; see the operation coverage. // @Tags Sessions // @Accept json // @Produce json,text/event-stream diff --git a/services/agents-api/internal/api/inputs.go b/services/agents-api/internal/api/inputs.go index 23183ecd1..7ce8ecb7b 100644 --- a/services/agents-api/internal/api/inputs.go +++ b/services/agents-api/internal/api/inputs.go @@ -24,7 +24,7 @@ type Option func(*Handler) func WithExecution(s InputSubmitter) Option { return func(h *Handler) { h.inputs = s } } // @Summary Submit Session input events -// @Description For environment none, atomically accepts text messages, cancellation and function results. Messages steer active work or start a queued Turn. The supported self_hosted and openai_hosted profiles accept text-only batches. Under the Session lock, matching retries retain their original target; new active messages append to the current Turn, while idle messages reserve work and wait up to the original five-minute connection/admission deadline. Return 204 only after durable admission, without claiming native application; active messages create no Turn or reservation. Cancellation-only prepared-environment batches use existing durable cancellation admission and return 204 without waiting for native exit; a new cancellation conflicts while a pre-Turn reservation is pending. Homogeneous tool_result-only prepared-environment batches reuse existing scoped result admission and application receipts without creating a Turn or bypassing a pending reservation. Mixed prepared-environment batches remain unsupported. HTTP expiry/cancellation use local 409 environment_input_expired/environment_input_cancelled errors; exact hosted failure mapping is unverified. Losing execution ownership returns 503. The response write deadline accommodates the admission window for either prepared Environment, independently of new-hosted-admission and executor URL settings. Disconnecting the waiting HTTP request does not cancel retained work or restart its deadline. Retry keys identify the whole ordered batch. Function output accepts text or ordered text/image parts subject to engine support; Claude SDK accepts text results and, on none, successful inline PNG/JPEG results, preserving ordered content; error images and remote references reject before admission. Native image resizing may change bytes. Runtime image-result support is checked only for image-bearing delivery. Codex and Claude SDK on none accept ordered inline PNG/JPEG image messages. Workspace profiles and other engines remain text-only; remote image URLs are unsupported. Image references are retained unchanged without service-side downloads. +// @Description For environment none, atomically accepts text messages, cancellation and function results. Messages steer active work or start a queued Turn. The supported self_hosted profile accepts text-only messages; qualified Codex and Claude SDK openai_hosted profiles also accept inline PNG/JPEG. Under the Session lock, matching retries retain their original target; new active messages append to the current Turn, while idle messages reserve work and wait up to the original five-minute connection/admission deadline. Return 204 only after durable admission, without claiming native application; active messages create no Turn or reservation. Cancellation-only prepared-environment batches use existing durable cancellation admission and return 204 without waiting for native exit; a new cancellation conflicts while a pre-Turn reservation is pending. Homogeneous tool_result-only prepared-environment batches reuse existing scoped result admission and application receipts without creating a Turn or bypassing a pending reservation. Mixed prepared-environment batches remain unsupported. HTTP expiry/cancellation use local 409 environment_input_expired/environment_input_cancelled errors; exact hosted failure mapping is unverified. Losing execution ownership returns 503. The response write deadline accommodates the admission window for either prepared Environment, independently of new-hosted-admission and executor URL settings. Disconnecting the waiting HTTP request does not cancel retained work or restart its deadline. Retry keys identify the whole ordered batch. Function output accepts text or ordered text/image parts subject to engine support; Claude SDK accepts text results and, on none and qualified openai_hosted, successful inline PNG/JPEG results, preserving ordered content; error images and remote references reject before admission. Native image resizing may change bytes. Runtime image-result support is checked only for image-bearing delivery. Codex and Claude SDK on none and qualified openai_hosted accept ordered inline PNG/JPEG image messages. Self-hosted profiles and other engines remain text-only; remote image URLs are unsupported. Image references are retained unchanged without service-side downloads. // @Tags Sessions // @Accept json // @Security BearerAuth diff --git a/services/agents-api/internal/engine/claude.go b/services/agents-api/internal/engine/claude.go index f08a13353..962c95a5b 100644 --- a/services/agents-api/internal/engine/claude.go +++ b/services/agents-api/internal/engine/claude.go @@ -14,13 +14,13 @@ func claudeProfile() Profile { ProgrammaticToolCallingDisable: true, StructuredOutput: true, ToolSearch: true, - MessageImagePlacements: []string{"none"}, + MessageImagePlacements: []string{"none", "openai_hosted"}, Placements: []string{"none", "openai_hosted", "self_hosted"}, MCPBearer: true, ValidateConfiguration: validateClaudeConfiguration, ValidateTools: validateClaudeTools, ValidateFunctionResult: func(placement string, result proto.FunctionResultPayload) error { for _, part := range result.Content { - if part.Type == "input_image" && (!result.Success || placement != "none") { + if part.Type == "input_image" && (!result.Success || (placement != "none" && placement != "openai_hosted")) { return ErrInvalidInput } } diff --git a/services/agents-api/internal/engine/codex.go b/services/agents-api/internal/engine/codex.go index ff5def2ee..62647f603 100644 --- a/services/agents-api/internal/engine/codex.go +++ b/services/agents-api/internal/engine/codex.go @@ -11,7 +11,7 @@ func codexProfile() Profile { return Profile{ ProgrammaticToolCallingDisable: true, Placements: []string{"none", "self_hosted", "openai_hosted"}, - MessageImagePlacements: []string{"none"}, + MessageImagePlacements: []string{"none", "openai_hosted"}, WebSearchControl: true, TextVerbosity: true, MCPBearer: true, ValidateConfiguration: func(agent v1.Agent, _ *v1.Environment, _ bool) error { return rejectSubagentTools(agent, "function", "mcp") diff --git a/services/agents-api/internal/execution/function_images_test.go b/services/agents-api/internal/execution/function_images_test.go index e32ef8c0d..6512c9b15 100644 --- a/services/agents-api/internal/execution/function_images_test.go +++ b/services/agents-api/internal/execution/function_images_test.go @@ -33,7 +33,7 @@ func TestFunctionImageAdmission(t *testing.T) { } }) } - for _, placement := range []string{"openai_hosted", "self_hosted"} { + for _, placement := range []string{"self_hosted"} { url := image if err := profile.ValidateFunctionResult(placement, proto.FunctionResultPayload{Success: true, Content: []proto.InputContent{{Type: "input_image", ImageURL: &url}}}); !errors.Is(err, engine.ErrInvalidInput) { t.Fatal("unqualified image placement", placement, err) diff --git a/services/agents-api/internal/execution/workspace_images_test.go b/services/agents-api/internal/execution/workspace_images_test.go new file mode 100644 index 000000000..d6f749738 --- /dev/null +++ b/services/agents-api/internal/execution/workspace_images_test.go @@ -0,0 +1,34 @@ +package execution + +import ( + "encoding/json" + "errors" + "testing" + + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/engine" + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" +) + +func TestHostedImageQualification(t *testing.T) { + image := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aXioAAAAASUVORK5CYII=" + message := store.Input{Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"inspect"},{"type":"input_image","image_url":"` + image + `"}]}]}`)} + result := store.Input{Kind: "tool_result", Payload: json.RawMessage(`{"call_id":"call","result":{"success":true,"output":[{"type":"input_image","image_url":"` + image + `"}]}}`)} + for _, kind := range []string{"codex", "claude_sdk", "mcode"} { + profile, _ := (engine.Catalog{}).Lookup(kind) + for _, placement := range []string{"none", "openai_hosted", "self_hosted"} { + err := validateProfileInputs(profile, placement, []store.Input{message}) + want := kind != "mcode" && placement != "self_hosted" + if (err == nil) != want { + t.Fatalf("%s/%s: %v", kind, placement, err) + } + } + } + profile, _ := (engine.Catalog{}).Lookup("claude_sdk") + if err := validateProfileInputs(profile, "openai_hosted", []store.Input{result}); err != nil { + t.Fatal(err) + } + result.Payload = json.RawMessage(`{"call_id":"call","result":{"success":false,"output":[{"type":"input_image","image_url":"` + image + `"}]}}`) + if err := validateProfileInputs(profile, "openai_hosted", []store.Input{result}); !errors.Is(err, store.ErrInvalidInput) { + t.Fatal("native failed image admitted", err) + } +} diff --git a/services/agents-api/tests/official_workspace_images_native.py b/services/agents-api/tests/official_workspace_images_native.py new file mode 100644 index 000000000..6744b60d6 --- /dev/null +++ b/services/agents-api/tests/official_workspace_images_native.py @@ -0,0 +1,253 @@ +"""Common hosted image acceptance through fixed SDK, HTTP and real native tools.""" + +import base64 +import importlib.metadata +import json +from pathlib import Path +import secrets +import time + +from image_fixture import picture + + +def verify_workspace_images(client, foreign, http, model, kind, restart, evidence): + """restart(session_id, environment_id) cold-restarts the owned Core/Runtime.""" + pin = json.loads((Path(__file__).resolve().parents[3] / "contracts/agents-api/upstream.json").read_text()) + distribution = importlib.metadata.distribution("openai") + assert distribution.version == pin["sdk_version"] + assert json.loads(distribution.read_text("direct_url.json"))["vcs_info"]["commit_id"] == pin["commit"] + sessions = client.beta.agents.sessions + root = str(client.base_url).rstrip("/") + "/agents" + headers = {"Authorization": "Bearer " + client.api_key, "OpenAI-Beta": "agents=v1"} + foreign_headers = {**headers, "Authorization": "Bearer " + foreign.api_key} + proof = {"engine": kind, "checks": [], "runs": [], "calls": []} + expected_messages = [] + sid = None + + def save(): + Path(evidence).write_text(json.dumps(proof, indent=2)) + + def check(name): + proof["checks"].append(name) + save() + print(name, flush=True) + + def text(value): + return {"type": "input_text", "text": value} + + def messages(parts): + return [{"role": "user", "content": parts}] + + def event(value): + return {"type": "agent.session.input.message", "input": value} + + def image_messages(url, path): + return messages([text("Inspect this image."), {"type": "input_image", "image_url": url}, + text("Remember its four band colors in left-to-right order.")]) + messages([ + text("Use native workspace tools to write exactly the four lowercase color names, separated by commas and no newline, to " + path + + ". Create the parent directory if necessary. Do not call get_visual. Then reply with the same colors.")]) + + def until(predicate, timeout=240): + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + result = predicate() + if result: + return result + time.sleep(0.2) + raise AssertionError("Public workflow did not reach expected state") + + def items(): + response = http.get(root + "/sessions/" + sid + "/items", headers=headers, params={"order": "asc", "limit": 100}) + assert response.status_code == 200 + raw = response.json()["data"] + assert not response.json()["has_more"], "Acceptance exceeded its bounded item page" + assert raw == [i.to_dict() for i in sessions.items.list(sid, order="asc", limit=100).data] + return raw + + def history(): + stored = items() + assert [i["content"] for i in stored if i["type"] == "message" and i.get("role") == "user"] == [m["content"] for m in expected_messages] + outputs = {i["call_id"]: i for i in stored if i["type"] == "function_call_output"} + for call in proof["calls"]: + assert outputs[call["call_id"]]["output"] == call["output"] + return stored + + def post(events, key=None, foreign_request=False): + hdr = dict(foreign_headers if foreign_request else headers) + if key: + hdr["Idempotency-Key"] = key + return http.post(root + "/sessions/" + sid + "/events", headers=hdr, json={"events": events}) + + def verify_file(path, expected, turn): + answers = [i for i in items() if i["type"] == "message" and i.get("role") == "assistant"] + answer = " ".join(p["text"] for p in answers[-1]["content"] if p["type"] == "output_text").lower() + positions = [answer.find(color) for color in expected] + assert all(p >= 0 for p in positions) and positions == sorted(positions), answer + artifacts = list(sessions.artifacts.list(sid, limit=100)) + matches = [a for a in artifacts if a.path == path and a.turn_id == turn] + assert len(matches) == 1, [a.to_dict() for a in artifacts] + artifact = matches[0] + with sessions.artifacts.with_streaming_response.content(artifact.id, session_id=sid) as response: + assert response.read() == ",".join(expected).encode() + assert any(f.path == path for f in client.beta.agents.environments.files.list(eid, path="/workspace/outputs")) + for suffix in ("", "/content"): + assert http.get(root + "/sessions/" + sid + "/artifacts/" + artifact.id + suffix, headers=foreign_headers).status_code == 404 + + def run(value, handler=None): + observed = [] + handled = False + with sessions.events.stream(sid, timeout=300) as stream: + sessions.events.create(sid, events=[event(value)]) + expected_messages.extend(value) + for received in stream: + observed.append(received.to_dict()) + if received.type == "agent.session.requires_action": + assert handler is not None and not handled + assert len(received.session.required_actions) == 1 + action = received.session.required_actions[0] + assert action.name == "get_visual" + handled = True + handler(action) + assert received.type not in {"agent.session.failed", "agent.session.turn.failed"}, received.to_dict() + if received.type == "agent.session.idle" and any(e["type"] == "agent.session.turn.created" for e in observed): + break + else: + raise AssertionError("SSE ended without idle") + proof["runs"].append(observed) + save() + types = [e["type"] for e in observed] + terminal = [t for t in types if t in {"agent.session.turn.completed", "agent.session.turn.cancelled"}] + assert len(terminal) == 1 + assert types[-1] == "agent.session.idle" + assert types.index("agent.session.turn.created") < types.index(terminal[0]) < len(types) - 1 + assert len({e["event_id"] for e in observed}) == len(observed) + assert handled == (handler is not None) + turn = sessions.turns.list(sid, order="desc").data[0] + assert terminal[0] == "agent.session.turn." + turn.status + history() + return turn + + def submit(action, output, validate=False): + result = {"type": "agent.session.input.tool_result", "turn_id": action.turn_id, + "call_id": action.call_id, "success": True, "output": output} + key = "result-" + action.call_id + before = items() + if validate: + if kind == "claude_sdk": + invalid = [{**result, "success": False}, {**result, "output": [{"type": "input_image", "image_url": "https://example.test/image.png"}]}] + for bad in invalid: + assert post([bad], key).status_code == 400 + assert items() == before + assert sessions.retrieve(sid).required_actions[0].call_id == action.call_id + assert post([result], foreign_request=True).status_code == 404 + assert post([{**result, "call_id": "unknown-call"}]).status_code in {400, 404, 409} + assert items() == before + sessions.events.create(sid, events=[result], idempotency_key=key) + assert post([result], key).status_code == 204 + assert post([{**result, "output": "conflict"}], key).status_code == 409 + proof["calls"].append(result) + + colors = ["red", "green", "blue", "yellow"] + secrets.SystemRandom().shuffle(colors) + initial = image_messages(picture(colors), "/workspace/outputs/initial.txt") + try: + session = sessions.create(agent={"model": model, "tools": [{"type": "function", "name": "get_visual", + "description": "Wait for a visual supplied by the caller.", "parameters": {"type": "object", "properties": {}, "additionalProperties": False}}]}, + environment={"type": "openai_hosted"}, input=initial) + sid, eid = session.id, session.environment.id + proof.update(session=sid, environment=eid, initial_colors=colors) + expected_messages.extend(initial) + save() + + def initial_done(): + turns = sessions.turns.list(sid, order="asc", limit=100).data + if not turns or turns[-1].status not in {"completed", "failed", "cancelled"}: + return False + assert len(turns) == 1 and turns[0].status == "completed", [t.to_dict() for t in turns] + return turns[0] + + first = until(initial_done) + verify_file("/workspace/outputs/initial.txt", colors, first.id) + history() + check("initial_png_native_workspace_files_artifact") + + jpeg = "data:image/jpeg;base64," + base64.b64encode((Path(__file__).parent / "testdata/function-bands.jpg").read_bytes()).decode() + idle = messages([{"type": "input_image", "image_url": jpeg}]) + messages([text( + "Write this image's four lowercase band colors from left to right, comma-separated with no newline, to /workspace/outputs/idle.txt using native tools. Reply with those colors. Do not call get_visual.")]) + turn = run(idle) + verify_file("/workspace/outputs/idle.txt", ["yellow", "blue", "red", "green"], turn.id) + check("prepared_image_only_jpeg_and_message_boundary") + + active_colors = colors[1:] + colors[:1] + def active(action): + incoming = image_messages(picture(active_colors), "/workspace/outputs/active.txt") + batch = [event(incoming)] + sessions.events.create(sid, events=batch, idempotency_key="active-image") + expected_messages.extend(incoming) + assert post(batch, "active-image").status_code == 204 + assert post([event(messages([text("conflict")]))], "active-image").status_code == 409 + submit(action, "The user supplied an image. Follow its instructions, then stop.") + turn = run(messages([text("Call get_visual once and wait. Then follow the incoming image instructions.")]), active) + verify_file("/workspace/outputs/active.txt", active_colors, turn.id) + check("active_image_input_retry_and_native_tools") + + result_colors = colors[2:] + colors[:2] + output = [text("Inspect this visual."), {"type": "input_image", "image_url": picture(result_colors, 15)}, text("Remember these band colors.")] + turn = run(messages([text("Call get_visual exactly once. Read its returned image and use native tools to write its four lowercase band colors in left-to-right order, comma-separated with no newline, to /workspace/outputs/result.txt. Reply with the colors. Do not call get_visual again.")]), + lambda action: submit(action, output, validate=True)) + verify_file("/workspace/outputs/result.txt", result_colors, turn.id) + check("large_function_image_result_receipt_retry_isolation_and_artifact") + + before = history() + invalid = [event(messages([text("must not persist")])), event(messages([{"type": "input_image", "image_url": "https://example.test/image.png"}]))] + assert post(invalid).status_code == 400 + assert history() == before + for suffix in ("", "/items", "/turns", "/artifacts"): + assert http.get(root + "/sessions/" + sid + suffix, headers=foreign_headers).status_code == 404 + assert post([event(initial)], foreign_request=True).status_code == 404 + assert http.get(root + "/environments/" + eid + "/files", headers=foreign_headers).status_code == 404 + check("unsupported_message_batch_is_atomic_and_foreign_resources_hidden") + + other = sessions.create(agent={"model": model}, environment={"type": "openai_hosted"}) + try: + source_artifact = list(sessions.artifacts.list(sid, limit=100))[0] + for suffix in ("", "/content"): + assert http.get(root + "/sessions/" + other.id + "/artifacts/" + source_artifact.id + suffix, headers=headers).status_code == 404 + response = http.post(root + "/sessions/" + other.id + "/events", headers=headers, json={"events": [proof["calls"][-1]]}) + assert response.status_code in {400, 404, 409} + assert sessions.items.list(other.id).data == [] + until(lambda: client.beta.agents.environments.retrieve(other.environment.id).status == "connected") + response = http.get(root + "/environments/" + other.environment.id + "/files", headers=headers, params={"path": "/workspace/outputs"}) + assert response.status_code in {200, 404} + if response.status_code == 200: + assert response.json()["data"] == [] + assert history() == before + check("same_tenant_session_result_artifact_and_workspace_isolation") + finally: + sessions.delete(other.id) + + restart(sid, eid) + assert history() == before + turn = run(messages([text("Recall the most recent image returned by get_visual from conversation history, without reading any file or calling get_visual. Write its four lowercase band colors in order, comma-separated with no newline, to /workspace/outputs/resumed.txt using native tools, then reply with them.")])) + verify_file("/workspace/outputs/resumed.txt", result_colors, turn.id) + assert len(sessions.turns.list(sid, limit=100).data) == 5 + check("cold_core_runtime_history_continuation_without_replay") + + pending = [] + def cancel(action): + pending.append(action.call_id) + for _ in range(2): + sessions.events.create(sid, events=[{"type": "agent.session.input.cancel"}], idempotency_key="cancel-image-pending") + turn = run(messages([text("Call get_visual exactly once and wait for its result.")]), cancel) + assert turn.status == "cancelled" and not sessions.retrieve(sid).required_actions + assert not any(i["type"] == "function_call_output" and i["call_id"] == pending[0] for i in items()) + run(messages([text("Reply only PLAIN_OK. Do not call any tools.")])) + assert len(sessions.turns.list(sid, limit=100).data) == 7 + assert any(i["type"] == "message" and i.get("role") == "assistant" and any("PLAIN_OK" in p.get("text", "") for p in i["content"]) for i in items()) + check("pending_cancel_retry_and_text_continuation") + proof["passed"] = True + return proof["checks"] + finally: + save() + if sid: + sessions.delete(sid)