JuliusBrussee/caveman · error · RuntimeError
cave artifact response missing artifact_id
Error message
cave artifact response missing artifact_id
What it means
`page_artifact()` posts content to the gateway and expects a response confirming storage: `stored: true` plus a non-empty string `artifact_id`. If the gateway answers with `stored` not true/false, or claims storage without a usable `artifact_id`, the SDK treats the response as corrupt and raises rather than returning a reference that could never be retrieved.
Solutions
- Log the raw response body and check the gateway URL/auth — a malformed response usually means the request never reached the real artifact endpoint.
- Verify SDK and gateway versions match (older gateways may not return `artifact_id`); upgrade the gateway or pin the SDK accordingly.
- Retry the store once, then fall back to inlining the content instead of referencing an artifact.
- Inspect intermediate proxies that may rewrite the response body.
Example fix
// before
ref = client.page_artifact(payload, options={"source": "tool"})
// after
try:
ref = client.page_artifact(payload, options={"source": "tool"})
except RuntimeError:
ref = json.dumps(payload) # fall back to inlining Defensive patterns
Strategy: try-catch
Validate before calling
resp = client._post("/sdk/v1/artifacts", body) # only if using internals
if not isinstance(resp.get("artifact_id"), str) or not resp["artifact_id"].strip():
log.error("artifact store returned bad body: %s", resp) Type guard
def is_valid_artifact_response(resp: dict) -> bool:
return resp.get("stored") is True and isinstance(resp.get("artifact_id"), str) and bool(resp["artifact_id"].strip()) Try / catch
try:
ref = client.page_artifact(payload, options={"source": "tool"})
except RuntimeError as e:
log.error("artifact store failed: %s", e)
ref = json.dumps(payload) # inline fallback Prevention
- Log the full gateway response body on failure to spot proxies returning 200 error pages.
- Pin SDK and gateway versions; test artifact round-trips (store then get) after upgrades.
- Have an inline-content fallback so a broken artifact path degrades gracefully.
When it happens
Trigger: The gateway returns a 200/2xx body without `stored` or with `stored: null`, or returns `stored: true` but `artifact_id` missing, non-string, or whitespace-only (e.g. an error JSON body served with a success status).
Common situations: A proxy or gateway returning an HTML/JSON error page with HTTP 200, a gateway version mismatch where the artifact endpoint changed shape, or an auth layer swallowing the request and returning an empty success body.
Understand the failure class
Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.
Related errors
- artifact_id is required
- cave artifact response missing artifact_id
- not_registered
- Agno middleware requires a nonempty native session_id
- Agno scope resolver must return a Caveman Scope
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/4caa7d2774b0d631.
Report an issue: GitHub.
Appendix: source
Thrown at packages/sdk/python/caveman_cloud/core.py:1205
``workflow``, and same stub format). :attr:`artifacts` exposes ``page``
as the mirror-named entry point; ``page_artifact`` stays as a
backwards-compatible alias.
"""
if options.get("strategy") == "verbatim":
return value
# `workflow` mirrors the TS SDK body so both SDKs send the identical wire.
response = self._request(
"/sdk/v1/artifacts",
{"value": value, "options": options, "workflow": self.workflow},
extra_headers={
"x-cave-artifact-envelope": "value-v1",
"x-cave-artifact-source": str(options["source"]),
},
)
if response.get("stored") is False:
return value
if response.get("stored") is not True or not isinstance(response.get("artifact_id"), str) or not response["artifact_id"].strip():
raise RuntimeError("cave artifact response missing artifact_id")
content_type = options.get("content_type") or "application/json"
return (
f"[cave-artifact id={response['artifact_id']} source={options['source']} type={content_type}]\n"
f"summary: artifact stored.\n"
f"retrieve: authenticated GET /sdk/v1/artifacts/{response['artifact_id']} or trace.artifacts.get(\"{response['artifact_id']}\").\n"
f"[/cave-artifact]"
)
def get_artifact(self, artifact_id: str) -> Any:
"""Retrieve and JSON-decode one artifact produced by :meth:`page_artifact`."""
if not isinstance(artifact_id, str) or not artifact_id.strip():
raise ValueError("artifact_id is required")
return self._get(f"/sdk/v1/artifacts/{quote(artifact_id, safe='')}")
@property
def artifacts(self) -> "_Artifacts":
"""Artifact handle mirroring the TS ``trace.artifacts`` namespace.
View on GitHub (pinned to 3ee70a1026)