Skip to content

Artifact Storage and Lifetime

Context reduction, producer memory, persistent storage, and history are separate concerns. Close the owning session when finished.

Explicit Streaming

Artifact admission protects model context even for ordinary materialized tool results, but it cannot undo producer memory already allocated by user code. A tool that constructs a 500 MB string has already paid for that allocation before Kedi can measure it.

Tools that need bounded producer memory must opt into ArtifactStream:

from collections.abc import Iterator

import kedi


def chunks() -> Iterator[str]:
    with open("application.log", encoding="utf-8") as stream:
        while chunk := stream.read(64 * 1024):
            yield chunk


@kedi.tool
def read_application_log() -> kedi.ArtifactStream[str]:
    return kedi.ArtifactStream.text(chunks())

ArtifactStream.text, ArtifactStream.bytes, and ArtifactStream.json_items accept synchronous or asynchronous sources. Kedi does not infer stream semantics from arbitrary iterables or generators; the explicit wrapper makes single-use ownership and failure behavior unambiguous.

The stream pipeline is:

producer chunks
  -> incremental canonical encoder
  -> threshold buffer
  -> transactional store writer after threshold crossing
  -> compact reference

Before the threshold is crossed, Kedi buffers only enough content to decide whether the result stays inline. Once crossed, the buffer and later chunks are written incrementally. Session quota is reserved and grown as chunks arrive. Failure, cancellation, invalid chunk type, or quota rejection aborts the writer, releases reservations, closes the source, and exposes no partial artifact.

If a stream completes below the threshold, Kedi reconstructs its declared native value. If artifacts are disabled, a stream may stay inline only up to the 100,000-byte hard tool-result limit.

The bundled filesystem.read_text_file and skill read_skill tools use ArtifactStream automatically during adapter tool calls. Their direct Kedi/Python call contract remains str; the incremental transport is an internal execution detail and does not alter their schema. Small bounded tools such as directory and artifact metadata listing remain materialized. Sandbox and subagent results are artifact-admitted after completion because their underlying engines do not expose incremental result chunks.

Serialization and Stores

Canonical serialization

Kedi supports four payload classes:

Value Codec Media type Persistence
str UTF-8 text text/plain; charset=utf-8 yes
bytes raw bytes application/octet-stream yes
JSON-compatible values, Pydantic models, dataclasses canonical JSON application/json yes
Other Python objects opaque process-local object application/x-python-object memory only

Serializable mutable values are snapshotted when admitted. JSON encoding is canonical and records bounded RFC 6901 pointer ranges so file-backed path reads can avoid materializing the complete document. Opaque objects use a safe type preview, have no content digest, and cannot enter the file store.

Kedi never uses pickle and never imports an arbitrary class while loading an artifact.

Memory store

The memory store preserves native Python values, including opaque objects. It reduces model context, but it is not resident-memory offload: the process still owns the full value until release, expiry, or manager close.

File store

The file store persists supported text, bytes, and JSON projections and does not retain an unbounded native-value cache. Reads and literal searches operate on the persisted projection. Paths are confined to the configured root, symlink escapes are rejected, and payload and metadata writes are atomic.

File records are restored only for their owning session. Expired records are not revived. A value that cannot be represented safely by a supported codec raises ArtifactSerializationError instead of falling back to unsafe persistence.

Quotas, Expiry, and Concurrency

Admission checks both active artifact count and active payload bytes. Streaming writes reserve count and bytes transactionally, so concurrent producers cannot individually pass a stale quota check and overcommit the session.

The fixed TTL is measured from creation and never moves. idle_ttl is measured from the last successful leased access. An artifact expires at the earlier of the two deadlines.

Reads acquire a lease. If release or expiry occurs during an active read, the record enters pending_release or expired, the current reader may finish, and the payload is deleted when the final lease closes. New reads fail immediately.

Artifact states are:

active -> pending_release -> released
active ------------------> released
active ------------------> expired
pending_release ----------> expired

Cleanup is both lazy and background-driven. Runtime operations perform an expiry check when the configured interval has elapsed, and one process-level cleanup service tracks active managers. Kedi does not create one cleanup thread per runtime.

Closing the manager unregisters cleanup, closes stores and code-runtime pools, and releases in-process accounting. An application must not reuse a closed session.

History and Cache Stability

ArtifactHistory is a thread-safe, append-only lifecycle log with monotonically increasing sequence numbers. It records tool calls, tool results, artifact creation, bounded reads, release, and expiry. Portable conversation history contains references and bounded tool results, never the original large payload.

Within one cache epoch, artifact lifecycle operations never delete, reorder, or rewrite earlier model messages. Releasing or expiring a payload therefore does not invalidate an already cached provider prefix. The old reference remains in history, but later attempts to dereference it receive the precise released or expired error.

Provider-native checkpoints follow the same rule: release and expiry do not mutate their existing prefix. Conversation compaction is a separate explicit operation that starts a new cache epoch; artifact lifecycle does not perform hidden history compaction.

Kedi remains stateless by default. Use an explicit Python session() when separate calls must share model history and artifact ownership.