JuliusBrussee/caveman · error · ValueError
artifact_id is required
Error message
artifact_id is required
What it means
`get_artifact()` requires a non-empty string `artifact_id` because it is interpolated into the authenticated GET path `/sdk/v1/artifacts/{id}`. Passing `None`, a non-string, or a whitespace-only string is rejected before any request; the id is URL-quoted with no safe characters when the call is made.
Solutions
- Pass the exact `artifact_id` string returned by `page_artifact` (from the [cave-artifact id=...] reference).
- Guard before calling: `if isinstance(aid, str) and aid.strip(): client.get_artifact(aid)`.
- Fix the parsing/extraction code that produced the empty id — parse the reference block rather than splitting on whitespace.
Example fix
// before
client.get_artifact(ref.get("artifact_id")) # may be None/empty
// after
aid = ref.get("artifact_id")
if isinstance(aid, str) and aid.strip():
client.get_artifact(aid) Defensive patterns
Strategy: type-guard
Validate before calling
aid = ref.get("artifact_id")
if isinstance(aid, str) and aid.strip():
artifact = client.get_artifact(aid)
else:
raise LookupError(f"no usable artifact id in reference: {ref!r}") Type guard
def has_artifact_id(ref: object) -> TypeGuard[str]:
return isinstance(ref, str) and bool(ref.strip()) Try / catch
try:
artifact = client.get_artifact(aid)
except ValueError as e:
log.error("get_artifact called with bad id %r: %s", aid, e) Prevention
- Store the artifact_id string directly from page_artifact's structured return rather than re-parsing the display block.
- Type artifact-id-bearing fields as non-optional in your dataclasses.
- Write a regex test fixture covering the [cave-artifact id=...] format you parse.
When it happens
Trigger: Calling `client.get_artifact("")`, `client.get_artifact(None)`, or passing an id extracted from a malformed artifact reference (e.g. the [cave-artifact id=...] block parsed incorrectly).
Common situations: A regex over the artifact reference block capturing the wrong group, a previous `page_artifact` failure leaving the id unset, or downstream code storing the id in an optional field that defaulted to empty.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- ASGI context resolver or request bounds are invalid
- assemble requires model and session_id
- assemble requires model and sessionId
- assembly slot id must be non-empty and unique
- assembly slot has unknown stability
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/d9608e180dac79e7.
Report an issue: GitHub.
Appendix: source
Thrown at packages/sdk/python/caveman_cloud/core.py:1217
"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.
``trace.artifacts.page(value, options)`` is the mirror-named entry point
for :meth:`page_artifact`; both hit ``/sdk/v1/artifacts`` with an
identical wire body.
"""
return _Artifacts(self)
def checkpoint(self, messages: list[Any], options: dict[str, Any]) -> dict[str, Any]:
# `workflow` mirrors the TS SDK body; the gateway's checkpoint.Request
# carries it (omitempty) so both SDKs send the identical wire.
return self._request("/sdk/v1/checkpoints", {"messages": messages, "options": options, "workflow": self.workflow})
def expand(self, source_ref: str) -> dict[str, Any]:View on GitHub (pinned to 3ee70a1026)