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.