Skip to content

Artifacts & resources

What it is / when to use it

PenguiFlow supports artifacts to keep large/binary data out of LLM context while still making it available to:

  • UIs (downloads, previews, rich components),
  • downstream systems (storage, audits),
  • later steps in a workflow.

Use artifacts when tools produce:

  • files generated by tools,
  • structured UI outputs,
  • references passed between steps without inlining large payloads.

Artifacts are also the mechanism behind many MCP resource workflows (see MCP resources).

Non-goals / boundaries

  • Artifacts are not an access-control system by themselves. ArtifactScope is metadata; enforcement is the host/app responsibility.
  • Artifacts are not guaranteed durable unless your ArtifactStore is durable and retention/TTL permits it.
  • Do not store secrets in artifacts unless you apply the same controls you would for any sensitive blob store.

Contract surface

Core runtime types

Artifacts live in penguiflow.artifacts:

  • ArtifactStore protocol (put_bytes, put_text, get, get_ref, exists, delete)
  • ArtifactRef (the only thing that should enter LLM context)
  • ArtifactRetentionConfig (TTL + size/count limits)

Planner integration

ReactPlanner can use an artifact store in three ways:

  1. explicit: ReactPlanner(..., artifact_store=...)
  2. discovered: if state_store exposes artifact_store (duck-typed) or implements ArtifactStore
  3. fallback: NoOpArtifactStore (artifacts become “no-op” references)

When the planner stores an artifact, it emits:

  • PlannerEvent(event_type="artifact_stored", extra={artifact_id, mime_type, ...})

Tool outputs: marking artifact fields

If a tool output has a field that must not be inlined into LLM context, mark it:

from pydantic import BaseModel, Field


class ToolResult(BaseModel):
    summary: str
    binary_blob: str | None = Field(default=None, json_schema_extra={"artifact": True})

The planner’s artifact collector extracts these fields into trajectory.artifacts and can redact them from the LLM-visible observation.

ToolNode integration (automatic extraction)

ToolNode has its own artifact extraction pipeline configured via:

  • ExternalToolConfig.artifact_extraction

It can:

  • clamp large inline strings into ctx._artifacts.put_text(...) (internal)
  • extract binary content into ctx._artifacts.put_bytes(...) (internal)
  • turn MCP resource_link blocks into “stubs” that point to resources tooling

Note: ctx._artifacts is the raw ArtifactStore used by penguiflow internals. Tool developers should use ctx.artifacts (the ScopedArtifacts facade) which provides upload()/download()/list() with automatic scope injection.

Operational defaults

Retention defaults (start here)

  • set a TTL that matches your UX (“how long should a user be able to download this?”)
  • cap per-artifact and per-session size
  • use cleanup_strategy="lru" unless you have a strong reason

Extraction defaults (start safe)

  • keep max_inline_size conservative to avoid prompt bloat
  • prefer returning summaries + artifact refs rather than full payloads
  • for multi-tenant systems: always populate artifact scope (session/tenant/user) at the app boundary

Failure modes & recovery

Artifacts missing/expired

Symptoms

  • UI cannot fetch an artifact by id

Likely causes

  • TTL expired
  • size/eviction limits removed it
  • using NoOpArtifactStore

Fix

  • increase TTL / retention limits
  • use a durable ArtifactStore in production
  • store durable references in your own storage layer when needed

Prompt bloat despite artifacts

Likely causes

  • tool outputs are not marked with json_schema_extra={"artifact": True}
  • ToolNode extraction is not configured/tuned

Fix

  • mark artifact fields in tool outputs
  • lower max_inline_size and ensure large strings become artifacts

Observability

Record at minimum:

  • artifact counts and sizes by session/trace
  • artifact_stored events (rate + size distribution)
  • artifact store eviction/TTL behavior (implementation specific)

Security / multi-tenancy notes

  • ArtifactRef is safe to show to the LLM; raw bytes/base64 are not.
  • Treat artifact ids as sensitive identifiers if your download endpoint is publicly reachable.
  • Use scoped ids and enforce access checks on retrieval endpoints.

Runnable example: artifact store for planner runs

from penguiflow.artifacts import ArtifactRetentionConfig, InMemoryArtifactStore
from penguiflow.planner import ReactPlanner

artifact_store = InMemoryArtifactStore(
    retention=ArtifactRetentionConfig(
        ttl_seconds=3600,
        max_artifact_bytes=50_000_000,
        max_session_bytes=500_000_000,
        cleanup_strategy="lru",
    )
)

planner = ReactPlanner(
    llm="gpt-4o-mini",
    catalog=catalog,
    artifact_store=artifact_store,
)

Troubleshooting checklist

  • Artifacts never appear: ensure you’re not using NoOpArtifactStore and your tool outputs mark artifact fields (or ToolNode extraction is enabled).
  • Artifacts leak across sessions: enforce ArtifactScope at your host boundary and avoid shared download URLs.
  • Large payloads still inline: tune max_inline_size and mark fields with json_schema_extra={"artifact": True}.