Skip to content

artifactr.core

Pure rules for collaborating on artifacts: no I/O, no async (ADR-0001).

Hosts call needs to learn what to load, then commit or record to decide a change, and persist the returned CommitResult. change_notes and resume are pure projections over the log.

Artifacts

Artifact types, stored versions and the type registry. See Defining artifact types.

Artifact pydantic-model

Bases: BaseModel

Base class for artifact types.

Subclass it with ordinary Pydantic fields. Override render_for_agent to control what the agent sees, and describe_change to summarize changes in your own words.

Config:

  • extra: forbid
  • validate_assignment: True

kind class-attribute

kind: str

The registered type name, derived from the class name unless given as name=.

write_policy class-attribute

write_policy: WritePolicy = 'direct'

Whether agents change this type directly or through proposals.

render_for_agent

render_for_agent() -> str

Return the text the agent sees for this artifact. Defaults to indented JSON.

describe_change

describe_change(before: Any) -> str | None

Summarize the change from before to this state, or return None.

before is always an instance of the same type. Annotate it as Self when you override this method; the base annotation is Any only so that overrides type-check. When this returns None, the summary is generated from the patch.

to_json

to_json() -> dict[str, JsonValue]

Return the artifact's data as JSON-compatible values.

MarkdownArtifact pydantic-model

Bases: Artifact

An artifact whose content is one Markdown text field.

It is edited with anchored text replacements (TextEdits) as well as JSON Patch.

Fields:

render_for_agent

render_for_agent() -> str

Return the Markdown text.

Versioned pydantic-model

Bases: BaseModel

A stored artifact: its data plus identity, version and last author.

Config:

  • frozen: True

Fields:

kind property

kind: str

The artifact's registered type name. Serialized, so readers can tell types apart.

edit

edit(
    change: Callable[[A], object],
    *,
    summary: str | None = None,
    thread_id: ThreadId | None = None,
) -> EditArtifact

Build an edit command by mutating a copy of the data.

change receives a deep copy of the data and mutates it in place; its return value is ignored. The difference becomes a JSON Patch based on this version.

edit_text

edit_text(
    old: str,
    new: str,
    *,
    field: str = "text",
    summary: str | None = None,
    thread_id: ThreadId | None = None,
) -> EditArtifact

Build an edit command that replaces the unique occurrence of old with new.

archive

archive(
    *, thread_id: ThreadId | None = None
) -> ArchiveArtifact

Build a command that archives this artifact at this version.

WritePolicy module-attribute

WritePolicy = Literal['direct', 'propose']

How an artifact type accepts agents' changes: applied directly, or as proposals.

create_artifact

create_artifact(
    artifact: Artifact,
    *,
    artifact_id: ArtifactId | None = None,
    thread_id: ThreadId | None = None,
) -> CreateArtifact

Build a command that creates artifact.

artifact_types

artifact_types() -> Mapping[str, type[Artifact]]

Return a read-only view of every registered artifact type, by name.

get_artifact_type

get_artifact_type(kind: str) -> type[Artifact]

Return the artifact type registered under kind.

Raises:

Type Description
NotFound

If no type is registered under that name.

load_artifact

load_artifact(
    kind: str, data: Mapping[str, Any]
) -> Artifact

Validate JSON data as an instance of the artifact type registered under kind.

Raises:

Type Description
NotFound

If no type is registered under that name.

ValidationFailed

If the data does not validate.

load_versioned

load_versioned(
    *,
    id: ArtifactId,
    kind: str,
    version: int,
    data: Mapping[str, Any],
    updated_by: Actor,
    archived: bool = False,
) -> Versioned[Artifact]

Rebuild a stored artifact from its parts, validating the data against its type.

Actors

Who did something. See Opening a workspace.

Actor module-attribute

Any actor, discriminated by kind.

UserActor pydantic-model

Bases: BaseModel

A person, identified by the host application's user id.

Config:

  • frozen: True

Fields:

participant property

participant: str

A key that is equal for every action by the same participant.

display_name property

display_name: str

How this actor is named in change notes.

AgentActor pydantic-model

Bases: BaseModel

The built-in agent of one thread, acting within one run.

Config:

  • frozen: True

Fields:

participant property

participant: str

A key that is equal for every run of the same thread's agent.

display_name property

display_name: str

How this actor is named in change notes.

ExternalAgentActor pydantic-model

Bases: BaseModel

An agent outside artifactr, connected over MCP.

Config:

  • frozen: True

Fields:

  • kind (Literal['external_agent'])
  • client_id (str)
  • name (str | None)

participant property

participant: str

A key that is equal for every action by the same client.

display_name property

display_name: str

How this actor is named in change notes.

SystemActor pydantic-model

Bases: BaseModel

The application itself, for automated changes.

Config:

  • frozen: True

Fields:

participant property

participant: str

A key that is equal for every action by the same system component.

display_name property

display_name: str

How this actor is named in change notes.

EvaluatorActor pydantic-model

Bases: BaseModel

An evaluator: a judge or decision model whose verdicts are recorded as feedback.

Evaluators only give feedback; every other command from one is forbidden.

Config:

  • frozen: True

Fields:

version pydantic-field

version: str

The evaluator's version, such as a hash of a trained judge, so verdicts never mix.

participant property

participant: str

A key that is equal for every verdict of the same evaluator version.

display_name property

display_name: str

How this actor is named in change notes.

is_agent

is_agent(actor: Actor) -> bool

Return whether the actor is an agent, built-in or external.

Agents are subject to artifact write policies; people and the system are not.

same_participant

same_participant(a: Actor, b: Actor) -> bool

Return whether two actors are the same participant.

Commands

Intents to change a workspace. See Commands and outcomes.

Command module-attribute

Any command, discriminated by type.

CreateArtifact pydantic-model

Bases: _Command

Create an artifact.

If the artifact type's write policy (or the thread's mode) requires an agent to propose, the command is recorded as a proposal with proposal_id instead.

Fields:

EditArtifact pydantic-model

Bases: _Command

Apply a patch to an artifact, based on a specific version.

If the artifact type's write policy (or the thread's mode) requires an agent to propose, the command is recorded as a proposal with proposal_id instead.

Fields:

ArchiveArtifact pydantic-model

Bases: _Command

Archive an artifact. Archived artifacts can be read but not changed.

Fields:

ProposeChange pydantic-model

Bases: _Command

Propose a change for someone else to accept, whatever the write policy.

Fields:

ProposedChange module-attribute

ProposedChange = Annotated[
    CreateArtifact | EditArtifact | ArchiveArtifact,
    Field(discriminator="type"),
]

A change that a proposal would make when accepted.

RespondToProposal pydantic-model

Bases: _Command

Accept or reject a pending proposal.

changes is an optional patch applied on top of the proposed result when accepting, so a person can accept a proposal with their own edits.

Fields:

CreateThread pydantic-model

Bases: _Command

Create a thread.

Fields:

PostMessage pydantic-model

Bases: _Command

Post a message, or a notice, in a thread, attributed to the actor who commits it.

Fields:

MessageKind module-attribute

MessageKind = Literal['message', 'notice']

What a message is for: the thread's agent acts on a message, while a notice informs people and starts or steers no turn (ADR-0051).

SetFocus pydantic-model

Bases: _Command

Set the artifacts a thread is focused on.

Fields:

SetThreadMode pydantic-model

Bases: _Command

Switch a thread between edit and suggest mode.

Fields:

ThreadMode module-attribute

ThreadMode = Literal['edit', 'suggest']

How agents change artifacts in a thread: directly, or always through proposals.

AnswerDeferred pydantic-model

Bases: _Command

Answer a paused run's question, or approve or deny one of its tool calls.

Fields:

  • type (Literal['answer_deferred'])
  • run_id (RunId)
  • tool_call_id (str)
  • answer (JsonValue)
  • approved (bool | None)

GiveFeedback pydantic-model

Bases: _Command

Give feedback of a registered type on an artifact version, a thread, a turn or a message.

value holds the feedback type's fields; core validates it against the type, checks the type can be given on the target, and checks the target exists.

Fields:

Feedback

Typed feedback on artifacts, threads, turns and messages. See Evaluation.

Feedback pydantic-model

Bases: BaseModel

Base class for feedback types.

Subclass it with ordinary Pydantic fields, and declare the targets it applies to with targets=. Field types decide how each field is scored in evaluation backends: bounded numbers are numeric, bool is yes or no, Literal and Enum are categories, and str is free text.

feedback_type class-attribute

feedback_type: str

The registered type name, derived from the class name unless given as name=.

targets class-attribute

The kinds of target this type of feedback can be given on.

FeedbackTarget

FeedbackTarget = Annotated[
    ArtifactTarget
    | ThreadTarget
    | TurnTarget
    | MessageTarget,
    Field(discriminator=kind),
]

What a piece of feedback is about, discriminated by kind.

ArtifactTarget pydantic-model

Bases: BaseModel

Feedback on one version of an artifact.

Config:

  • frozen: True
  • extra: forbid

Fields:

ThreadTarget pydantic-model

Bases: BaseModel

Feedback on a whole thread: the session.

Config:

  • frozen: True
  • extra: forbid

Fields:

TurnTarget pydantic-model

Bases: BaseModel

Feedback on the agent's turn: its run, which may span pauses.

Config:

  • frozen: True
  • extra: forbid

Fields:

MessageTarget pydantic-model

Bases: BaseModel

Feedback on one message in a thread.

Messages live only in the log, so the target names the thread they were posted in, and, for the agent's messages, the run that posted them (the run_id of its message_posted), which links the feedback to the run's trace.

Config:

  • frozen: True
  • extra: forbid

Fields:

TargetKind

TargetKind = Literal[
    "artifact", "thread", "turn", "message"
]

What feedback can be about: an artifact version, a thread, a turn (a run) or a message.

TARGET_KINDS module-attribute

TARGET_KINDS: frozenset[TargetKind] = frozenset(
    {"artifact", "thread", "turn", "message"}
)

Every kind of target.

feedback_types

feedback_types() -> Mapping[str, type[Feedback]]

Return a read-only view of every registered feedback type, by name.

get_feedback_type

get_feedback_type(feedback_type: str) -> type[Feedback]

Return the feedback type registered under a name.

Raises:

Type Description
NotFound

If no feedback type is registered under that name.

load_feedback

load_feedback(
    feedback_type: str,
    target: ArtifactTarget
    | ThreadTarget
    | TurnTarget
    | MessageTarget,
    value: Mapping[str, Any],
) -> Feedback

Validate feedback of a registered type, given on a target.

Raises:

Type Description
NotFound

If no feedback type is registered under that name.

ValidationFailed

If the type cannot be given on that kind of target, or the value does not validate.

Outcomes

What a command did.

Outcome module-attribute

Outcome = Annotated[
    Applied | Proposed | Resolved | Recorded,
    Field(discriminator="type"),
]

What a command did, discriminated by type.

Applied pydantic-model

Bases: _Outcome

The change was applied: the artifact is now at version.

Fields:

Proposed pydantic-model

Bases: _Outcome

The change was recorded as a proposal instead of being applied.

Fields:

Resolved pydantic-model

Bases: _Outcome

A proposal was accepted (the artifact is now at version) or rejected.

Fields:

Recorded pydantic-model

Bases: _Outcome

The command was recorded; it changed no artifact.

Fields:

run_id pydantic-field

run_id: RunId | None = None

The run a message or an answer started or resumed, or None if it started none.

Only the runner that carries the command out knows it, so the runner sets it, and core never does (ADR-0048).

Rejections

The ways a command can fail. See Rejections.

Rejection

Rejection(message: str)

Bases: Exception

Base class for every reason core refuses a command.

details

details() -> dict[str, JsonValue]

Return the rejection's typed fields; subclasses extend this.

payload

payload() -> dict[str, JsonValue]

Return the rejection as JSON-compatible data for the wire.

VersionConflict

VersionConflict(artifact_id: str, *, base: int, head: int)

Bases: Rejection

The command was based on a version that is no longer current.

details

details() -> dict[str, JsonValue]

Return the artifact and both versions.

ValidationFailed

ValidationFailed(message: str, errors: list[JsonValue])

Bases: Rejection

The resulting data does not validate against the artifact type.

details

details() -> dict[str, JsonValue]

Return Pydantic's validation errors.

PatchFailed

PatchFailed(message: str)

Bases: Rejection

The patch does not apply to the data.

NotFound

NotFound(entity: str, id: str)

Bases: Rejection

Something the command refers to does not exist.

details

details() -> dict[str, JsonValue]

Return what was missing.

Forbidden

Forbidden(message: str)

Bases: Rejection

The actor may not perform this command.

InvalidState

InvalidState(message: str)

Bases: Rejection

The command does not apply to the current state, such as answering a finished run.

UnsupportedProtocol

UnsupportedProtocol(message: str)

Bases: Rejection

The client asked for a protocol version the server does not speak.

Patches

The two ways artifact data changes. See How changes are expressed.

Patch module-attribute

Patch = Annotated[
    JsonPatch | TextEdits, Field(discriminator="kind")
]

Any patch, discriminated by kind.

JsonPatch pydantic-model

Bases: BaseModel

An RFC 6902 JSON Patch.

Config:

  • frozen: True

Fields:

TextEdits pydantic-model

Bases: BaseModel

A sequence of anchored replacements in one text field.

Each old must occur exactly once in the field at the moment it is applied. An empty old is allowed only when the field is empty, to write its first content.

Config:

  • frozen: True

Fields:

TextEdit pydantic-model

Bases: BaseModel

Replace one exact occurrence of old with new.

Config:

  • frozen: True

Fields:

apply_patch

apply_patch(
    data: dict[str, JsonValue], patch: Patch
) -> dict[str, JsonValue]

Apply a patch to JSON data, returning new data.

Parameters:

Name Type Description Default
data dict[str, JsonValue]

The artifact's current JSON data. It is not modified.

required
patch Patch

The patch to apply.

required

Returns:

Type Description
dict[str, JsonValue]

The patched data.

Raises:

Type Description
PatchFailed

If the patch does not apply.

diff

diff(
    old: dict[str, JsonValue], new: dict[str, JsonValue]
) -> JsonPatch

Return the JSON Patch that turns old into new.

describe_patch

describe_patch(patch: Patch) -> str

Return a short, human-readable description of a patch.

Used as the change summary when neither the command nor the artifact type provides one.

Entities

Threads, proposals, revisions and runs, as stored.

Thread pydantic-model

Bases: BaseModel

A chat, with its mode and the artifacts it is focused on.

Config:

  • frozen: True

Fields:

Proposal pydantic-model

Bases: BaseModel

A change proposed by one participant for another to accept or reject.

Config:

  • frozen: True

Fields:

artifact_id property

artifact_id: ArtifactId

The artifact the proposal creates, changes or archives.

Revision pydantic-model

Bases: BaseModel

One immutable version of an artifact (ADR-0004).

Config:

  • frozen: True

Fields:

trace_id pydantic-field

trace_id: TraceId | None = None

The OpenTelemetry trace the change was committed in, if it was traced; set by the workspace (ADR-0033).

Run pydantic-model

Bases: BaseModel

An agent run, which may pause for deferred requests and resume.

Config:

  • frozen: True

Fields:

trace_ids pydantic-field

trace_ids: tuple[TraceId, ...] = ()

The OpenTelemetry trace of each traced attempt, oldest first: a run that pauses and resumes runs once per attempt, each in its own trace.

all_answered property

all_answered: bool

Whether every pending request of a paused run has been answered.

RunStatus module-attribute

RunStatus = Literal[
    "running", "paused", "completed", "stopped", "failed"
]

DeferredAnswer pydantic-model

Bases: BaseModel

The answer to one deferred request of a paused run.

Config:

  • frozen: True

Fields:

  • answer (JsonValue)
  • approved (bool | None)

Events

Facts on a workspace's log, and the envelope each travels in. See The log.

Envelope pydantic-model

Bases: BaseModel

A stored event with its position and attribution. Its shape is also the wire shape.

Fields:

seq pydantic-field

seq: int

The event's position in its workspace's log, gap-free from 1.

traceparent pydantic-field

traceparent: str | None = None

The W3C trace context of the span that committed the event, so what it causes can link back to it.

Event module-attribute

Event = Annotated[
    Annotated[ThreadCreated, Tag("thread_created")]
    | Annotated[
        ThreadModeChanged, Tag("thread_mode_changed")
    ]
    | Annotated[FocusChanged, Tag("focus_changed")]
    | Annotated[MessagePosted, Tag("message_posted")]
    | Annotated[ArtifactCreated, Tag("artifact_created")]
    | Annotated[ArtifactChanged, Tag("artifact_changed")]
    | Annotated[ArtifactArchived, Tag("artifact_archived")]
    | Annotated[ProposalCreated, Tag("proposal_created")]
    | Annotated[ProposalResolved, Tag("proposal_resolved")]
    | Annotated[RunStarted, Tag("run_started")]
    | Annotated[ToolCalled, Tag("tool_called")]
    | Annotated[ToolReturned, Tag("tool_returned")]
    | Annotated[RunPaused, Tag("run_paused")]
    | Annotated[DeferredAnswered, Tag("deferred_answered")]
    | Annotated[RunEnded, Tag("run_ended")]
    | Annotated[FeedbackGiven, Tag("feedback_given")]
    | Annotated[AppEvent, Tag("app_event")]
    | Annotated[UnknownEvent, Tag("unknown")],
    Discriminator(_event_tag),
]

Any event, discriminated by type; unknown types validate as UnknownEvent.

UnknownEvent pydantic-model

Bases: BaseModel

An event type this version does not know, kept as-is so it round-trips.

Config:

  • frozen: True
  • extra: allow

Fields:

ThreadCreated pydantic-model

Bases: _Event

A thread was created.

Fields:

ThreadModeChanged pydantic-model

Bases: _Event

A thread switched between edit and suggest mode.

Fields:

FocusChanged pydantic-model

Bases: _Event

The set of artifacts a thread is focused on changed.

Fields:

MessagePosted pydantic-model

Bases: _Event

A message, or a notice, was posted in a thread. Its author is the envelope's actor.

Fields:

ArtifactCreated pydantic-model

Bases: _Event

An artifact was created at version 1.

Fields:

ArtifactChanged pydantic-model

Bases: _Event

An artifact was changed by a patch.

Fields:

ArtifactArchived pydantic-model

Bases: _Event

An artifact was archived.

Fields:

ProposalCreated pydantic-model

Bases: _Event

A change was proposed.

Fields:

ProposalResolved pydantic-model

Bases: _Event

A proposal was accepted or rejected.

Fields:

RunEvent module-attribute

Facts about agent runs, recorded by the agent layer rather than commanded.

RunStarted pydantic-model

Bases: _Event

An agent run started, or resumed after a pause.

Fields:

trace_id pydantic-field

trace_id: TraceId | None = None

The OpenTelemetry trace this attempt runs in, when it is traced.

ToolCalled pydantic-model

Bases: _Event

The agent called a tool.

Fields:

ToolReturned pydantic-model

Bases: _Event

A tool call finished.

Fields:

tool_name pydantic-field

tool_name: str = ''

The tool's name, as in its tool_called; empty in events from earlier versions.

RunPaused pydantic-model

Bases: _Event

A run paused until the listed requests are answered.

Fields:

usage pydantic-field

usage: RunUsage | None = None

What the run used up to the pause.

DeferredRequest pydantic-model

Bases: BaseModel

A tool call a paused run is waiting on: a question to answer or a call to approve.

Config:

  • frozen: True

Fields:

  • tool_call_id (str)
  • tool_name (str)
  • kind (Literal['question', 'approval'])
  • args (dict[str, JsonValue])

DeferredAnswered pydantic-model

Bases: _Event

A paused run's request was answered.

Fields:

RunEnded pydantic-model

Bases: _Event

An agent run ended.

Fields:

reason pydantic-field

reason: str | None = None

Why a failed run failed, when a capability or the runner said, such as guardrail_blocked, or abandoned for a run whose claim lapsed.

RunUsage pydantic-model

Bases: BaseModel

Token and request counts for a run.

Config:

  • frozen: True

Fields:

  • requests (int)
  • input_tokens (int)
  • output_tokens (int)

FeedbackGiven pydantic-model

Bases: _Event

A person or an evaluator gave feedback. Who gave it is the envelope's actor.

It is thread-scoped when its target belongs to a thread (a thread, a turn or a message), and workspace-scoped for an artifact version.

Fields:

value pydantic-field

value: dict[str, JsonValue]

The feedback's fields, as validated against its registered type.

AppEvent pydantic-model

Bases: _Event

An application-defined fact. name identifies it; data is free-form JSON.

Fields:

scope_of

scope_of(
    event: KnownEvent,
) -> tuple[ThreadId | None, RunId | None]

Return the thread and run an event belongs to, for its envelope.

delivered_to

delivered_to(
    envelope: Envelope, threads: Collection[ThreadId] | None
) -> bool

Return whether a subscriber following threads receives envelope.

Workspace-scoped events (artifacts and proposals, whose types are in WORKSPACE_SCOPED) reach every subscriber, whichever thread they originated in. Thread-scoped events reach subscribers that follow their thread. None follows every thread. Storage applies the same rule when it reads the log for some threads.

WORKSPACE_SCOPED module-attribute

WORKSPACE_SCOPED: Final = frozenset(
    {
        "artifact_created",
        "artifact_changed",
        "artifact_archived",
        "proposal_created",
        "proposal_resolved",
    }
)

The types of the events every subscriber receives, whichever thread they originated in.

Rules

The host contract: what to load, and how a command or a fact is decided (ADR-0018). Workspace.commit and Workspace.record use these; call them directly only when you write a host of your own.

needs

needs(
    item: Command | Fact,
    *,
    actor: Actor,
    state: State | None = None,
) -> Needs

Return the ids a host must still load before calling commit or record.

Call it repeatedly, loading what it returns, until it returns an empty (falsy) Needs: some commands only know what else they need once their first entities are loaded.

commit

commit(
    command: Command, state: State, *, actor: Actor
) -> CommitResult

Decide a command against the loaded state.

Returns:

Type Description
CommitResult

What to persist, and the outcome to report.

Raises:

Type Description
Rejection

If the command cannot be applied; see artifactr.core.errors.

NotLoaded

If state lacks something needs asked for.

record

record(
    fact: Fact, state: State, *, actor: Actor
) -> CommitResult

Decide a fact about an agent run, or an application event, against the loaded state.

Only the thread's own agent, or the system, may record facts about the thread's runs.

Raises:

Type Description
Rejection

If the fact contradicts the run's state, such as a tool call after the run ended, or the actor may not record it.

NotLoaded

If state lacks something needs asked for.

Fact module-attribute

Fact = RunEvent | AppEvent

A fact recorded by the agent layer rather than commanded.

NotLoaded

Bases: LookupError

The host called a rule without loading an entity that needs asked for.

This is a bug in the host, not a rejection of the command.

State dataclass

State(
    artifacts: Mapping[
        ArtifactId, Versioned[Artifact] | None
    ] = _EMPTY,
    proposals: Mapping[
        ProposalId, Proposal | None
    ] = _EMPTY,
    threads: Mapping[ThreadId, Thread | None] = _EMPTY,
    runs: Mapping[RunId, Run | None] = _EMPTY,
    messages: Mapping[MessageId, bool] = _EMPTY,
)

The slice of a workspace that core needs to decide one command.

Each mapping holds what the host loaded, keyed by id. A key whose value is None was looked up and does not exist; a missing key was not loaded (see needs).

messages class-attribute instance-attribute

messages: Mapping[MessageId, bool] = field(default=_EMPTY)

Whether each message id is already used in the workspace (ADR-0045).

Needs dataclass

Needs(
    artifacts: frozenset[ArtifactId] = frozenset(),
    proposals: frozenset[ProposalId] = frozenset(),
    threads: frozenset[ThreadId] = frozenset(),
    runs: frozenset[RunId] = frozenset(),
    messages: frozenset[MessageId] = frozenset(),
)

Ids a host must load into State before core can decide a command.

CommitResult dataclass

CommitResult(
    outcome: Applied | Proposed | Resolved | Recorded,
    events: tuple[KnownEvent, ...] = (),
    artifacts: tuple[Versioned[Artifact], ...] = (),
    revisions: tuple[Revision, ...] = (),
    proposals: tuple[Proposal, ...] = (),
    threads: tuple[Thread, ...] = (),
    runs: tuple[Run, ...] = (),
    messages: tuple[MessageId, ...] = (),
)

Everything a host persists after a command, in one transaction.

artifacts, proposals, threads and runs are entities to insert or replace; revisions are appended; messages are message ids to record as used; events are appended to the log in order.

Change notes

What others did, told to one viewer. See Change notes.

change_notes

change_notes(
    envelopes: Iterable[Envelope],
    *,
    viewer: Actor,
    focus: Collection[ArtifactId] | None = None,
    notices: bool = False,
) -> list[Note]

Return notes about what others did, in the order it happened.

Parameters:

Name Type Description Default
envelopes Iterable[Envelope]

A slice of the log, in seq order.

required
viewer Actor

Who the notes are for; their own actions are left out.

required
focus Collection[ArtifactId] | None

The artifacts the viewer follows. None means every artifact.

None
notices bool

Include the notices others posted in the viewer's thread, when the viewer is a thread's agent.

False

Returns:

Type Description
list[Note]

One ChangeNote per artifact others changed, one ProposalNote per

list[Note]

relevant proposal event, and, with notices, one NoticeNote per notice.

render_notes

render_notes(notes: Iterable[Note]) -> str

Render notes as a bulleted list, one line each.

Note module-attribute

ChangeNote pydantic-model

Bases: BaseModel

What other participants did to one artifact, coalesced across events.

Config:

  • frozen: True

Fields:

from_version pydantic-field

from_version: int | None

The version before the changes, or None if the artifact was created.

render

render() -> str

Return the note as one line of text.

ProposalNote pydantic-model

Bases: BaseModel

A proposal made by someone else, or a decision on the viewer's own proposal.

Config:

  • frozen: True

Fields:

render

render() -> str

Return the note as one line of text.

NoticeNote pydantic-model

Bases: BaseModel

A notice someone else posted in the viewer's thread.

Config:

  • frozen: True

Fields:

render

render() -> str

Return the note as text.

Live events

A run's token-level output. See Live output.

LiveFrame pydantic-model

Bases: BaseModel

A live event of one run, as sent on the wire.

Config:

  • frozen: True

Fields:

LiveEvent module-attribute

LiveEvent = Annotated[
    PartStarted
    | TextDelta
    | ThinkingDelta
    | ToolArgsDelta
    | PartEnded
    | Draft
    | AppLive,
    Field(discriminator="type"),
]

Any live event, discriminated by type.

PartStarted pydantic-model

Bases: _LiveEvent

The model started a response part: text, thinking, or a tool call.

Fields:

  • type (Literal['part_started'])
  • part (int)
  • part_kind (Literal['text', 'thinking', 'tool_call'])
  • tool_name (str | None)

TextDelta pydantic-model

Bases: _LiveEvent

More text for a text part.

Fields:

ThinkingDelta pydantic-model

Bases: _LiveEvent

More text for a thinking part.

Fields:

ToolArgsDelta pydantic-model

Bases: _LiveEvent

More of a tool call's JSON arguments.

Fields:

PartEnded pydantic-model

Bases: _LiveEvent

A response part is complete.

Fields:

Draft pydantic-model

Bases: _LiveEvent

A full snapshot of an artifact being generated, not yet committed.

Fields:

AppLive pydantic-model

Bases: _LiveEvent

An application's own live event.

Fields:

  • type (Literal['app_live'])
  • name (str)
  • data (JsonValue)

Protocol frames

The thread protocol's frames and its resume rule. See the thread protocol.

PROTOCOL module-attribute

PROTOCOL: Final = 'artifactr.v1'

The protocol version this library speaks.

Hello pydantic-model

Bases: BaseModel

The first frame a client sends on a connection.

Config:

  • frozen: True

Fields:

resume_after_seq pydantic-field

resume_after_seq: int = 0

The last seq the client has: everything after it is replayed.

from_head pydantic-field

from_head: bool = False

Start at the head of the log instead, replaying nothing. resume_after_seq must then be 0; a client that reconnects resumes from welcome.head_seq with it.

threads pydantic-field

threads: tuple[ThreadId, ...] | None = None

Thread-scoped events to receive; None means every thread.

Welcome pydantic-model

Bases: BaseModel

The server's answer to hello.

Config:

  • frozen: True

Fields:

ActiveRun pydantic-model

Bases: BaseModel

A run in progress, as listed in welcome.

Config:

  • frozen: True

Fields:

EventFrame pydantic-model

Bases: Envelope

A durable event, delivered in its envelope.

Config:

  • frozen: True

Fields:

ReplayComplete pydantic-model

Bases: BaseModel

Every event up to up_to_seq has been replayed; what follows is live.

Config:

  • frozen: True

Fields:

  • type (Literal['replay_complete'])
  • up_to_seq (int)

CommandFrame pydantic-model

Bases: BaseModel

A client's command, with an id that correlates it with its result.

command_id is also an idempotency key: the server deduplicates repeated ids.

Config:

  • frozen: True

Fields:

FrameCommand module-attribute

FrameCommand = Annotated[
    Command | StopRun | WatchRun,
    Field(discriminator="type"),
]

Anything a client can ask for in a command frame.

StopRun pydantic-model

Bases: BaseModel

Cancel a run. Handled by the transport, not by core's rules.

Config:

  • frozen: True
  • extra: forbid

Fields:

WatchRun pydantic-model

Bases: BaseModel

Receive a run's live frames on this connection. WebSocket only.

Config:

  • frozen: True
  • extra: forbid

Fields:

CommandResult pydantic-model

Bases: BaseModel

The result of one command frame.

Config:

  • frozen: True

Fields:

ErrorFrame pydantic-model

Bases: BaseModel

A frame the server could not understand, or a failure it could not attribute.

Config:

  • frozen: True

Fields:

ClientFrame module-attribute

ClientFrame = Annotated[
    Hello | CommandFrame, Field(discriminator="type")
]

Any frame a client sends.

ServerFrame module-attribute

ServerFrame = Annotated[
    Welcome
    | EventFrame
    | ReplayComplete
    | LiveFrame
    | CommandResult
    | ErrorFrame,
    Field(discriminator="type"),
]

Any frame the server sends.

resume

resume(
    hello: Hello,
    *,
    head_seq: int,
    first_retained_seq: int = 1,
) -> ResumePlan

Decide where replay starts for a connecting client.

Parameters:

Name Type Description Default
hello Hello

The client's hello frame.

required
head_seq int

The workspace log's latest seq (0 if it is empty).

required
first_retained_seq int

The oldest seq still stored.

1

Raises:

Type Description
UnsupportedProtocol

If the client speaks another protocol version.

ValidationFailed

If the client asks to start at the head and to resume.

ResumePlan pydantic-model

Bases: BaseModel

Where a connection's replay starts.

Config:

  • frozen: True

Fields:

replay_after pydantic-field

replay_after: int

Replay every event with a seq greater than this.

reset pydantic-field

reset: bool = False

Whether the client must discard what it has and rebuild from the replay.

Identifiers

Identifiers are plain strings. These aliases say what a string identifies, and the factories generate ids with a short type prefix.

TenantId

TenantId = str

Identifies a tenant: the top-level isolation boundary.

WorkspaceId

WorkspaceId = str

Identifies a workspace within a tenant.

ArtifactId

ArtifactId = str

Identifies an artifact within a workspace.

ThreadId

ThreadId = str

Identifies a thread (a chat) within a workspace.

RunId

RunId = str

Identifies one agent run, which may span pauses.

ProposalId

ProposalId = str

Identifies a proposal.

MessageId

MessageId = str

Identifies a message in a thread.

TraceId

TraceId = Annotated[
    str, StringConstraints(pattern="^[0-9a-f]{32}$")
]

An OpenTelemetry trace id, as 32 lowercase hexadecimal digits.

new_id

new_id(prefix: str) -> str

Return a new random identifier with the given prefix.

Parameters:

Name Type Description Default
prefix str

A short type tag, such as "thr".

required

Returns:

Type Description
str

An identifier such as thr_3f9c2a1b7d4e5f60.

new_artifact_id

new_artifact_id() -> ArtifactId

Return a new artifact identifier.

new_thread_id

new_thread_id() -> ThreadId

Return a new thread identifier.

new_run_id

new_run_id() -> RunId

Return a new run identifier.

new_proposal_id

new_proposal_id() -> ProposalId

Return a new proposal identifier.

new_message_id

new_message_id() -> MessageId

Return a new message identifier.