Skip to content

Client reference

Top-level

methodic.Chronicle

Chronicle(
    server_url: str,
    api_key: str,
    timeout: int = 30,
    max_upload_workers: int = 2,
    organization_id: str | None = None,
    organization_slug: str | None = None,
)

Client for the Chronicle REST API.

Construct once with the server URL + API key, then call into namespaces:

chronicle = Chronicle(server_url="https://api.methodiclabs.ai", api_key="sk_...")

Or resolve credentials from the environment / a config file (see from_env and from_file) — the form the Chronicle skills use:

chronicle = Chronicle.from_env()  # CHRONICLE_SERVER_URL + CHRONICLE_API_KEY

Then call into namespaces:

# Researcher
exp = chronicle.experiments.create(hypothesis_summary="...", config_yaml="...")
exp.commit().variations.create(config_yaml="...")

# Worker
run = chronicle.run(experiment_id, variation, run_idx)
run.start().heartbeat()
run.upload_asset(asset_type="research_report", content={"summary": "..."})
run.succeed()

Use as a context manager to guarantee the executor and HTTP session are closed.

Construct via :meth:from_env (reads CHRONICLE_SERVER_URL + CHRONICLE_API_KEY) or directly with server_url + api_key.

Organization scope resolves per call, with an optional client default: pass organization_id on the call itself (e.g. experiments.create(..., organization_id=...)), or set a default once — organization_id: in ~/.methodic/config.yaml or $CHRONICLE_ORGANIZATION_ID — and omit it. With a default configured, pass methodic.PERSONAL to force a personal-scope call. There is no per-request header; resolution happens in the SDK call.

organization_id property

organization_id: str | None

The default organization for calls that take an organization_id and were not given one (None = personal scope).

When configured by slug (organization_slug / the CHRONICLE_ORGANIZATION_SLUG env / the organization_slug: config key), the slug is resolved to its principal id via /v1/me/scopes on first read and cached for the client's lifetime; a slug matching no organization you belong to raises ChronicleConfigError.

close

close() -> None

Shut down the upload pool and HTTP session. Idempotent.

experiment

experiment(experiment_id: str) -> Experiment

Get a handle for an existing experiment by id (lazy — no fetch until accessed).

from_env classmethod

from_env(**overrides: Any) -> Chronicle

Construct a client from the ambient environment.

Resolution order, highest precedence first:

  1. explicit keyword arguments — server_url, api_key, timeout, max_upload_workers, organization_id
  2. environment variables — CHRONICLE_SERVER_URL, CHRONICLE_API_KEY, CHRONICLE_TIMEOUT, CHRONICLE_MAX_UPLOAD_WORKERS, CHRONICLE_ORGANIZATION_ID
  3. YAML files under ~/.methodicconfig.yaml (non-secret settings) then credentials.yaml (the API key); or a single file pointed to by CHRONICLE_CONFIG
  4. built-in defaults (server_urlhttps://api.methodiclabs.ai)

api_key has no default; raises ChronicleConfigError if it cannot be resolved from any source, or if a setting is malformed.

This is the entry point the Chronicle skills call, so setting either the environment variables or the config file is enough to run them.

from_file classmethod

from_file(
    config: str | Path | None = None,
    credentials: str | Path | None = None,
    **overrides: Any,
) -> Chronicle

Construct a client from explicit YAML files.

Mirrors the ~/.methodic split so the same files work with either entry point: a non-secret config file and/or a credentials file holding the API key. Each is a flat mapping::

# config.yaml
server_url: https://api.methodiclabs.ai
timeout: 30              # optional
# credentials.yaml
api_key: sk_user_...

Either may carry any subset (a single combined file can be passed as config); credentials wins over config on overlap, and explicit keyword arguments win over both. Unlike from_env, this does not consult environment variables. Raises ChronicleConfigError if a named file is missing or malformed, or if no api_key is present.

run

run(experiment_id: str, variation: int, run: int) -> Run

Construct a Run resource handle bound to one (experiment, variation, run).

variation

variation(experiment_id: str, variation: int) -> Variation

Get a handle for an existing variation by (experiment_id, variation).

Resource handles

methodic.Experiment

Experiment(
    chronicle: Chronicle,
    experiment_id: str,
    *,
    _detail: ExperimentDetail | None = None,
    _create_response: CreateExperimentResponse
    | None = None,
)

Handle for one experiment.

Mutators (commit, conclude, retract) return self so callers can chain (exp.commit().variations.create(...)). Cached detail is dropped after each mutation; the next attribute access re-fetches transparently.

delete

delete() -> dict[str, Any]

Hard-delete this open experiment (see :meth:ExperimentsAPI.delete). The handle is dead after this returns — the experiment row and its cascade are gone.

distill

distill(
    *,
    scope: str,
    variation_id: int | None = None,
    corpus_filter: dict[str, Any] | None = None,
    write_research_report: bool = True,
    reason: str | None = None,
) -> dict[str, Any]

Trigger a distillation agent for this experiment (M9 §17).

fork

fork(
    *,
    hypothesis_summary: str,
    rationale: str | None = None,
    config_yaml: str | None = None,
    slug: str | None = None,
    allow_retracted_parent: bool = False,
) -> "Experiment"

Fork this experiment. Proxies to ExperimentsAPI.fork.

get_agent_config

get_agent_config() -> dict[str, Any]

Read this experiment's agent_config block (M11).

git_status

git_status() -> GitStatus

Lightweight current git-integration state for this experiment.

mint_git_token

mint_git_token() -> GitToken

Mint a 1-hour install token scoped to this experiment's repo.

move

move(
    *,
    organization_id: str,
    team_id: str | None = None,
    visibility: str | None = None,
) -> Experiment

Transfer this experiment into an org (see :meth:ExperimentsAPI.move). Drops cached detail and returns self so calls can chain.

set_agent_config

set_agent_config(config: dict[str, Any]) -> dict[str, Any]

Replace this experiment's agent_config block.

set_continuous_exploration

set_continuous_exploration(
    *,
    enabled: bool,
    trigger_scope: str = "variation",
    cooldown_minutes: int = 0,
) -> dict[str, Any]

Configure the M11 continuous-exploration loop on this experiment. Convenience wrapper that read-modify-writes agent_config.continuous_exploration.

set_report_settings

set_report_settings(settings: dict[str, Any]) -> Experiment

Replace experiment.report_settings. Frozen at commit (server returns 409 once committed). settings shape matches the ReportSettings server type:

{
    "hypothesis": {"mode": "freeform", "freeform_prompt": "..."},
    "takeaways":  {"mode": "template", "template_asset_id": "...", "per_variation": true},
    "research":   {...}
}

Returns self for chaining. Drops cached _detail so the next access re-fetches the updated row.

wait_for_repo

wait_for_repo(
    *, timeout: float = 300.0, poll_interval: float = 2.0
) -> GitStatus

Poll until this experiment's repo is ready (or failed/timeout).

methodic.Variation

Variation(
    chronicle: Chronicle,
    experiment_id: str,
    variation: int,
    *,
    _data: Variation | None = None,
)

Handle for one variation. Holds (experiment_id, variation) and lazy-loaded data.

data property

data: Variation

Server-side variation record. Auto-fetched on first access; refetched after mutations.

list_inputs

list_inputs() -> list[dict[str, Any]]

List this variation's input assets (raw dicts).

list_outputs

list_outputs() -> list[dict[str, Any]]

List this variation's output assets across all runs (raw dicts), newest-first — produced checkpoints, snapshots, and reports.

set_git_ref

set_git_ref(git_ref: str) -> Variation

Bind a branch to this (open) variation. Returns self for chaining.

unlink_input(asset_id: str) -> Variation

Unlink an input asset from this (open) variation. Returns self for chaining.

update

update(
    *,
    hypothesis: str | None = None,
    expected_outcome: str | None = None,
    description: str | None = None,
    name: str | None = None,
) -> Variation

Edit this (open) variation's mutable metadata. Returns self for chaining.

methodic.Run

Run(
    api: RunsAPI,
    experiment_id: str,
    variation: int,
    run: int,
)

Handle for a specific (experiment_id, variation, run) run.

Mutators return self so worker code can chain (run.start().heartbeat()). Asset-upload helpers auto-populate output_of from the bound triple.

create_asset_presigned

create_asset_presigned(
    asset_type: str,
    components: list[str],
    name: str | None = None,
    content_type: str = "application/octet-stream",
) -> AssetUploadInfo

Register a new asset for component upload via presigned URLs.

latest_output

latest_output(
    asset_type: str | None = None,
    *,
    across_runs: bool = True,
    ready_only: bool = True,
) -> dict[str, Any] | None

Return the most recent matching output asset, or None — the resume-discovery helper.

Filters by asset_type (e.g. "checkpoint") and, by default, to state == "ready" (finalized + immutable). Searches across all runs of the variation by default. Pair with :meth:download_asset::

ckpt = run.latest_output("checkpoint")
if ckpt:
    run.download_asset(ckpt["id"], Path("./resume"))

list_outputs

list_outputs(
    *, across_runs: bool = False
) -> list[dict[str, Any]]

List this run's output assets (newest-first). With across_runs=True, list outputs across all runs of the variation — the scope to use when resuming, since the checkpoint to resume from was produced by an earlier run.

register_and_upload_async

register_and_upload_async(
    local_dir: Path,
    asset_type: str,
    upload_tracker: UploadTracker,
    content_type: str = "application/octet-stream",
) -> str

Register every file in a directory, then upload + finalize on a background thread.

succeed

succeed() -> Run

Mark the run succeeded after waiting for any pending async uploads.

upload_asset

upload_asset(
    asset_type: str,
    content: Any,
    name: str | None = None,
    content_type: str = "application/json",
    asset_config: dict[str, Any] | None = None,
) -> dict[str, Any]

Upload a small inline asset (auto-finalized). Linked to this run as output.

upload_directory_async

upload_directory_async(
    local_dir: Path,
    asset_type: str,
    content_type: str = "application/octet-stream",
    upload_tracker: UploadTracker | None = None,
) -> None

Upload every file in a directory on a background thread.

With upload_tracker, uses the register-then-upload flow for crash recovery. Without, a thinner path that uploads and finalizes inline.

Namespaces

methodic.ExperimentsAPI

ExperimentsAPI(transport: Transport, chronicle: Chronicle)

Experiments namespace. Stateless; every method takes the experiment id explicitly.

conclude

conclude(
    experiment_id: str,
    *,
    on_exist_action: str | None = None,
) -> dict[str, Any]

Conclude an experiment: lock all outputs, no new variations. Terminal.

Not idempotent — re-concluding a concluded experiment raises (409 already_concluded). Gates on a satisfying experiment-level takeaways_report; if none exists the server schedules a distillation and the 409 body carries distillation_job_id to poll, then re-issue conclude.

Parameters:

Name Type Description Default
experiment_id str

Target experiment.

required
on_exist_action str | None

"keep" or "regenerate" — required by the server (else 409 takeaways_report_exists) when an experiment-level takeaways_report already exists. keep gates on the existing (approved) report; regenerate supersedes it and distills a fresh one.

None

create

create(
    *,
    hypothesis_summary: str,
    config_yaml: str,
    rationale: str | None = None,
    description: str | None = None,
    accelerate_config_yaml: str | None = None,
    launch_config: dict[str, Any] | None = None,
    parent_experiment_ids: list[str] | None = None,
    allow_retracted_parent: bool = False,
    organization_id: str | None = None,
    team_id: str | None = None,
    visibility: str | None = None,
) -> Experiment

Create a new experiment. Returns a handle with the create-response cached.

Organization scope: pass organization_id (and/or team_id) to create the experiment under that org/team. Omitting it falls back to the client's configured default org (the organization_id setting — see Chronicle); with a default configured, pass methodic.PERSONAL for a personal experiment owned by the calling key's user.

visibility controls who can read the experiment and its reports: "private" (creator + org admins only), "organization" / "team" (the owning org or team gets read + discuss, and its reports become discoverable in search), or "public" (anyone, read-only). Omit it for the scope-derived default — org/team-wide in an org context, private in personal space.

delete

delete(experiment_id: str) -> dict[str, Any]

Hard-delete an open (uncommitted) experiment and everything it owns (variations, runs, asset/research-prompt links, ACLs, auto-roles, and best-effort the GitHub repo + search doc). Returns the server's removal summary.

Refused with ConflictError (409) once the experiment is committed or concluded — retract it instead — or if another experiment was derived from it (remove the descendants first). Requires the Delete action. The underlying asset rows survive (they may be shared across experiments); only this experiment's link rows go.

distill

distill(
    experiment_id: str,
    *,
    scope: str,
    variation_id: int | None = None,
    corpus_filter: dict[str, Any] | None = None,
    write_research_report: bool = True,
    reason: str | None = None,
) -> dict[str, Any]

Trigger a distillation agent for the experiment (M9 §17).

Parameters:

Name Type Description Default
experiment_id str

Target experiment.

required
scope str

"variation", "experiment", or "corpus".

required
variation_id int | None

Required when scope="variation".

None
corpus_filter dict[str, Any] | None

Required when scope="corpus"; shape per DistillCorpusFilter on the server.

None
write_research_report bool

Whether to also write a research_report alongside the takeaways_report for experiment-scoped distillations. Default True.

True
reason str | None

Free-form audit string.

None

Returns the 202 body: {distillation_job_id, scope, expected_outputs, tartarus_instance_id}. Distillation runs async inside tartarus-d; poll GET /experiments/{id}/agents to watch progress.

fork

fork(
    experiment_id: str,
    *,
    hypothesis_summary: str,
    rationale: str | None = None,
    config_yaml: str | None = None,
    slug: str | None = None,
    allow_retracted_parent: bool = False,
) -> Experiment

Fork an experiment: create a new experiment whose repo mirrors the source's full git history, with a lineage edge to the source. Returns a handle to the new (forked) experiment.

get_agent_config

get_agent_config(experiment_id: str) -> dict[str, Any]

Read the experiment's agent_config JSON block (M11).

Returns {} for experiments that never had a block set. Shape per runes/chronicle/designs/agent-flows.md §13: distillation (default-on auto-trigger + auto-on-conclude knobs) and continuous_exploration (opt-in synthesis wake-up loop).

git_status

git_status(experiment_id: str) -> GitStatus

Current git-integration state for the experiment.

Returns lightweight status info — state (pending/ready/failed/archived), repo_url (when ready), failure_reason (when failed). Cheap to poll; UI calls this every couple seconds while state is pending.

iter

iter(
    *,
    status: str | None = None,
    created_by: str | None = None,
    page_size: int | None = None,
) -> Iterator[ExperimentSummary]

Yield every experiment matching the filters, paging server-side as needed.

list

list(
    *,
    status: str | None = None,
    created_by: str | None = None,
    page_size: int | None = None,
    page_token: str | None = None,
) -> ExperimentListPage

One page of experiments matching the filters.

The server paginates on ?limit + ?before=<created_at>_<id> and returns a bare array (no envelope token); page_size / page_token are the SDK's stable names for those. The next cursor is derived from the last row when the page comes back full — a short page is the end. Use :meth:iter to walk every page.

mint_git_token

mint_git_token(experiment_id: str) -> GitToken

Mint a 1-hour install token scoped to this experiment's repo.

The returned token has Administration permission stripped — pushes to agent/* branches will be rejected by branch protection. Use it to clone the repo and push to user/... branches you create.

Raises ServerError(503) if the server has no GitHub App configured; ConflictError(409) if the experiment's repo isn't ready yet.

move

move(
    experiment_id: str,
    *,
    organization_id: str,
    team_id: str | None = None,
    visibility: str | None = None,
) -> dict[str, Any]

Transfer a personal experiment into an organization.

Personal → org only: the experiment's owner becomes organization_id (and team_id if given), the org's admins gain read + administer, and visibility sets who else can read it — "private" (creator + org admins), "organization" / "team" (org/team members get read + discuss; the default in an org context), or "public" (anyone). The original creator keeps their access; the move only adds org reach (owner_subject is unchanged).

Raises ConflictError (409) if the experiment is already owned by an org, or if its slug collides with an existing experiment in the target org (rename it first via PUT /experiments/{id}). Requires Administer on the experiment and membership of the target org.

set_agent_config

set_agent_config(
    experiment_id: str, config: dict[str, Any]
) -> dict[str, Any]

Replace the experiment's agent_config block.

Validates the continuous_exploration sub-block strictly (400 with kind: "invalid_continuous_exploration" on malformed shape); other sub-blocks pass through. Pass {} to clear all knobs back to defaults.

set_continuous_exploration

set_continuous_exploration(
    experiment_id: str,
    *,
    enabled: bool,
    trigger_scope: str = "variation",
    cooldown_minutes: int = 0,
) -> dict[str, Any]

Convenience wrapper around :meth:set_agent_config for the M11 continuous_exploration block.

Merges the new block into the existing agent_config (read first, then PUT) so other knobs (distillation defaults, steering caps, etc.) are preserved.

Parameters:

Name Type Description Default
experiment_id str

Target experiment.

required
enabled bool

Whether the closed-loop wake-up fires on distillation completion.

required
trigger_scope str

"variation", "experiment", or "both". Default "variation".

'variation'
cooldown_minutes int

0 .. 1440. 0 = no coalescing (publish every completion). Larger = coalesce multiple completions into one wake-up.

0

wait_for_repo

wait_for_repo(
    experiment_id: str,
    *,
    timeout: float = 300.0,
    poll_interval: float = 2.0,
) -> GitStatus

Poll git_status until the repo is ready or failed, or timeout.

methodic.VariationsAPI

VariationsAPI(transport: Transport, chronicle: Chronicle)

Variations namespace. Keys every operation on (experiment_id, variation).

commit

commit(
    experiment_id: str,
    variation: int,
    *,
    commit_without_hypothesis: bool = False,
) -> dict[str, Any]

Commit (lock) a variation. Refused (409 missing_variation_hypothesis) when the variation has no recorded hypothesis unless commit_without_hypothesis=True — a deliberate, audited choice to commit without pre-registration.

create

create(
    experiment_id: str,
    *,
    config_yaml: str,
    accelerate_config_yaml: str | None = None,
    launch_config: dict[str, Any] | None = None,
    description: str | None = None,
    input_asset_ids: list[str] | None = None,
    git_ref: str | None = None,
    name: str | None = None,
    hypothesis: str | None = None,
    expected_outcome: str | None = None,
) -> Variation

Create a new variation under experiment_id. Returns a Variation handle.

hypothesis records the falsifiable hypothesis this variation validates (tied to the eval metric) — the variation's pre-registration, the analog of an experiment's hypothesis. expected_outcome is the predicted result vs. baseline. Both can also be set/refined after creation via :meth:update while the variation is open. A variation with no hypothesis can only be committed via the explicit commit_without_hypothesis override.

git_ref optionally associates the variation with a branch on the experiment's GitHub repo. Server captures the branch name now; SHA resolution + the branch-rename-to-agent/... flow happens at variation commit (Phase 3). Pre-Phase-3, registering with git_ref is informational only.

list_inputs

list_inputs(
    experiment_id: str, variation: int
) -> list[dict[str, Any]]

List the variation's input assets (each a dict with id, asset_type, name, …). Use to find a linked code_artifact — e.g. a bundle to clean up after rebinding the variation to git.

list_outputs

list_outputs(
    experiment_id: str, variation: int
) -> list[dict[str, Any]]

List the variation's output assets across all its runs (each a dict with id, asset_type, name, state, created_at, …), newest-first. Use to find produced checkpoints, snapshots, and reports — e.g. the latest ready checkpoint to resume from.

set_git_ref

set_git_ref(
    experiment_id: str, variation: int, git_ref: str
) -> dict[str, Any]

Bind a branch to an open variation — records the variation→branch mapping without pinning a SHA (the SHA locks at commit).

Used by the create-first fork flow: create the variation, name the branch variation/<id>, push it, then bind it here so a tartarus agent (or the commit-time rename) knows which branch belongs to the variation. The server rejects the protected agent/* namespace and returns 409 if the variation is already committed.

unlink_input(
    experiment_id: str, variation: int, asset_id: str
) -> dict[str, Any]

Remove an input-asset link from an open variation.

Unlinks the asset from the variation's inputs without deleting the asset itself — hard-delete the now-orphaned asset separately via chronicle.assets.delete. Refused with 409 once the variation is committed (inputs freeze on commit). Used to drop a stale code_artifact — e.g. a bundle that a later git-ref binding superseded — while the variation is still open.

update

update(
    experiment_id: str,
    variation: int,
    *,
    hypothesis: str | None = None,
    expected_outcome: str | None = None,
    description: str | None = None,
    name: str | None = None,
) -> VariationData

Edit an open variation's mutable metadata. Omitted fields are left untouched; pass "" to clear one. Raises (409) once the variation is committed. Primary use: record or refine the pre-registered hypothesis before commit.

methodic.RunsAPI

RunsAPI(
    transport: Transport,
    assets: AssetsAPI,
    executor: ThreadPoolExecutor,
)

Run-lifecycle namespace. Stateless across calls; takes the run triple as args.

fail

fail(
    experiment_id: str,
    variation: int,
    run: int,
    *,
    reason: str = "crash",
) -> None

Mark a run failed. reason is crash (worker error) or abandoned (cancel).

list_outputs

list_outputs(
    experiment_id: str, variation: int, run: int
) -> list[dict[str, Any]]

List the output assets produced by a single run (newest-first).

list_variation_outputs

list_variation_outputs(
    experiment_id: str, variation: int
) -> list[dict[str, Any]]

List output assets produced across all runs of a variation (newest-first). The resume-discovery scope: a fresh run finds the prior run's checkpoint here.

start

start(
    experiment_id: str,
    variation: int,
    run: int,
    *,
    wandb_run_id: str | None = None,
    wandb_entity: str | None = None,
    wandb_project: str | None = None,
    wandb_dashboard_url: str | None = None,
) -> None

Mark the run started. Optionally link its W&B run by passing the full triple (wandb_run_id + wandb_entity + wandb_project; project falls back server-side to the experiment's wandb_project). The link is the (exp,var,run) → W&B run pointer distillation reads — agent-side with its own key, or via the backend wandb_fetch_* broker.

methodic.AssetsAPI

AssetsAPI(
    transport: Transport,
    *,
    chronicle: Chronicle | None = None,
    default_organization_id: str | None = None,
)

Asset operations.

Output-of linking (which experiment/variation/run produced this asset) is passed explicitly by callers — Run populates it from its bound context, while researcher-level uploads pass it directly or omit it for shared assets.

approve

approve(asset_id: str) -> dict[str, Any]

Clear review_required from an asset's pending_reasons. Auto-finalizes the asset if that was the last reason (state → ready). Idempotent on already-terminal assets. See design.md § Pending reasons. Requires Write permission on the asset.

bulk_approve

bulk_approve(
    asset_ids: list[str] | None = None,
    *,
    all_in_scope: bool = False,
) -> BulkApproveResponse

Approve review-gated reports in a batch — the attention feed's "approve selected" / "approve all" action.

Exactly one mode (a ValueError is raised before any request otherwise):

  • pass asset_ids to approve the listed assets ("approve selected");
  • pass all_in_scope=True to approve every pending review_required asset the caller can act on in scope ("approve all N").

Partial success is normal: the call returns HTTP 200 with a per-asset result list, and a 404 / permission denial / validation failure on one asset surfaces as an "error" row rather than aborting the batch. Each asset's Write permission is re-checked server-side, so an all_in_scope candidate you can read but not write fails its own row.

Returns a :class:~methodic.types.BulkApproveResponse (results + approved / failed counts).

create_inline

create_inline(
    *,
    asset_type: str,
    content: Any,
    name: str | None = None,
    content_type: str = "application/json",
    output_of: dict[str, Any] | None = None,
    asset_config: dict[str, Any] | None = None,
    pending_reasons: list[str] | None = None,
    organization_id: str | None = None,
    team_id: str | None = None,
    visibility: str | None = None,
) -> dict[str, Any]

Upload a small inline asset. Chronicle auto-finalizes unless pending_reasons is non-empty — see design.md § Pending reasons. Valid reasons: upload_in_progress (not on inline content), compile_pending, review_required.

Pass organization_id (and/or team_id) to create the dataset under an org you belong to, and visibility ("private" | "organization" / "team" | "public") to set who else can read it — same model as experiments.create. Omitting organization_id falls back to the client's configured default org (if any); pass methodic.PERSONAL to force a personal, private dataset.

create_with_presigned

create_with_presigned(
    *,
    asset_type: str,
    components: list[str],
    name: str | None = None,
    content_type: str = "application/octet-stream",
    output_of: dict[str, Any] | None = None,
    asset_config: dict[str, Any] | None = None,
    pending_reasons: list[str] | None = None,
    organization_id: str | None = None,
    team_id: str | None = None,
    visibility: str | None = None,
) -> AssetUploadInfo

Register a new asset and get presigned PUT URLs for each component. Accepts an optional asset_config (stored on the asset — datasets put their provenance record here) and a pending_reasons list (see create_inline). organization_id / team_id / visibility set the dataset's org context + visibility (see create_inline); organization_id falls back to the client's configured default org.

delete

delete(asset_id: str) -> dict[str, Any]

Hard-delete an asset that is not linked to any experiment — no experiment/variation input links and no output links reference it. The cleanup path for orphans (over-uploaded datasets, abandoned pending uploads). Removes the row, its ACLs, inline content, storage bytes, and search document. Returns the server's removal summary. Irreversible.

Refused with ConflictError (409, kind: "asset_linked", per-table counts in links) while the asset is linked anywhere — a linked asset is part of an experiment's record; take it out of use with :meth:deprecate/:meth:invalidate instead. Requires the Delete action on the asset.

deprecate

deprecate(asset_id: str, reason: str) -> dict[str, Any]

Soft-warn on an asset: it stays usable as an input, but the deprecation (with reason) is surfaced as a warning wherever it is linked. The right call for "superseded, but existing results stand". Returns the updated asset.

download

download(asset_id: str, local_dir: Path) -> Path

Download all components of an asset to a local directory.

finalize

finalize(asset_id: str) -> None

Mark a presigned-upload asset as ready (immutable) once all components are up.

get

get(
    asset_id: str, *, include_presigned: bool = False
) -> dict[str, Any]

Fetch asset metadata. With include_presigned=True, includes read URLs.

grant_access

grant_access(
    asset_id: str, principal_id: str, action: str = "read"
) -> dict[str, Any]

Grant a principal (a user sub, team id, org id, or everyone) an action on a dataset/asset. action defaults to read; others: write, delete, administer. Idempotent. Requires Administer on the asset (its creator has it).

invalidate

invalidate(asset_id: str, reason: str) -> dict[str, Any]

Hard-block an asset: it can no longer be linked as an input without allow_invalid_assets. The row, links, and provenance survive (unlike :meth:delete) — the right call for "wrong data, do not build on this" when the asset is part of an experiment's record. Returns the updated asset.

list_access

list_access(asset_id: str) -> dict[str, Any]

List the access-control entries on a dataset/asset — who can read/write it. Requires Read on the asset. Returns the server's object-aces payload.

move

move(
    asset_id: str,
    *,
    organization_id: str,
    team_id: str | None = None,
    visibility: str | None = None,
) -> dict[str, Any]

Transfer a dataset/asset into an organization — the asset analog of experiments.move. Sets its owning org/team so it lists under the org (GET /assets?owner=…) and bills to it; visibility ("private" | "organization" / "team" | "public", default org-wide) sets who else can read it. Requires Administer on the asset and membership of the target org (resolve the id via chronicle.me.scopes()). created_by is unchanged.

presign

presign(
    asset_id: str,
    *,
    operation: str = "read",
    components: list[str] | None = None,
) -> dict[str, Any]

Request presigned URLs for an asset's components.

reject

reject(asset_id: str, reason: str) -> dict[str, Any]

Reject a review-gated asset — clears review_required AND transitions pending → abandoned, recording rejected_at and rejection_reason. Requires Write on the asset; reason is a required free-text explanation surfaced in audit + UI.

revoke_access

revoke_access(
    asset_id: str, principal_id: str, action: str
) -> dict[str, Any]

Revoke a principal's (principal, action) grant on a dataset. Idempotent — the response removed is False if the grant didn't exist. Requires Administer on the asset.

set_visibility

set_visibility(
    asset_id: str, visibility: str
) -> dict[str, Any]

Set an asset's visibility — its broadcast read grant — independent of any experiment it's linked to. visibility is "private" | "org" (a.k.a. "organization" / "team") | "public".

Use this to share a single report/dataset more widely — e.g. make one report "public" — without exposing the whole experiment. "private" removes the broadcast grant; per-person shares from :meth:grant_access and experiment-inherited access are left untouched (visibility controls only the single broadcast grant). Unlike :meth:move, this does not change the asset's owning org. Requires Administer on the asset (its creator has it; an experiment's admins have it on the experiment's reports). Returns the asset.

share_with_scope

share_with_scope(
    asset_id: str, scope_id: str, action: str = "read"
) -> dict[str, Any]

Share a dataset with a whole team or organization: grant the scope (a team/org id — see chronicle.me.scopes()) read (default) on the asset so its members reach it. A thin convenience over :meth:grant_access; requires Administer on the asset and that you belong to the scope.

upload_component

upload_component(
    upload_url: str, local_path: Path, content_type: str
) -> None

PUT one component to its presigned URL.

methodic.SearchAPI

SearchAPI(transport: Transport, chronicle: Chronicle)

Vertex-backed search across research docs, experiment metadata, and arxiv assets.

history

history(
    query: str,
    *,
    experiment_context: list[str] | None = None,
    created_by: str | None = None,
    created_after: str | None = None,
    created_before: str | None = None,
    asset_types: list[str] | None = None,
    page_size: int | None = None,
    page_token: str | None = None,
) -> SearchResponse

Search Chronicle's INTERNAL corpus — your experiment history and internal research documents (hypothesis/takeaways/research reports, experiment metadata).

This is a semantic alias for :meth:query with the common narrowing knobs (author, time window, asset type) lifted into keyword args and assembled into a SearchFilters. It hits the same POST /search endpoint and is RBAC- and storage-prefix-scoped server-side.

NOTE: this does NOT search external literature. arxiv / paper search is served by a SEPARATE external MCP, not by this method or by Chronicle's search API.

asset_types defaults to None — Chronicle already excludes session assets server-side, so the unfiltered corpus is the right default. Pass a list to narrow to specific types (e.g. ["research_report"]).

iter

iter(
    query: str,
    *,
    filters: SearchFilters | dict[str, Any] | None = None,
    experiment_context: list[str] | None = None,
    scope: SearchScope | dict[str, Any] | None = None,
    page_size: int | None = None,
) -> Iterator[SearchResult]

Yield every search hit, paging server-side as needed.

query

query(
    query: str,
    *,
    filters: SearchFilters | dict[str, Any] | None = None,
    experiment_context: list[str] | None = None,
    scope: SearchScope | dict[str, Any] | None = None,
    page_size: int | None = None,
    page_token: str | None = None,
) -> SearchResponse

Run a single search request. Returns one page; use iter to walk pages.

scope is a HARD filter that restricts results to the union of the named collections + experiments (and member experiments' outputs), distinct from experiment_context which only boosts. Pass a :class:~methodic.SearchScope or a raw {collections, experiments} dict; an empty scope paired with experiment_context hard-scopes to that context. See collections.md §"Search integration".

Response types

methodic.ExperimentData dataclass

ExperimentData(
    id: str,
    owner_subject: str,
    hypothesis_summary: str,
    created_at: str,
    created_by: str,
    state: str,
    rationale: str | None = None,
    description: str | None = None,
    committed_at: str | None = None,
    concluded_at: str | None = None,
    retracted_at: str | None = None,
    retraction_reason: str | None = None,
    git_repo_state: str = "pending",
    git_repo_url: str | None = None,
    git_repo_failure_reason: str | None = None,
)

Mirror of the server's Experiment struct.

git_repo_state defaults to "pending" so older server payloads (which may not include the field yet) deserialize cleanly.

methodic.ExperimentDetail dataclass

ExperimentDetail(
    experiment: Experiment,
    parent_ids: list[str],
    variations: list[VariationSummary],
)

GET /experiments/{id} response: experiment + parents + variation summaries.

methodic.ExperimentSummary dataclass

ExperimentSummary(
    id: str,
    hypothesis_summary: str,
    variation_count: int,
    created_at: str,
    created_by: str,
    state: str,
    status: str | None = None,
    committed_at: str | None = None,
    concluded_at: str | None = None,
    retracted_at: str | None = None,
)

One row in the experiments list.

methodic.ExperimentListPage dataclass

ExperimentListPage(
    results: list[ExperimentSummary],
    next_page_token: str | None = None,
)

One page of experiments.list results plus a cursor for the next page.

The current server returns a flat array; we normalize that into a single-page response with next_page_token=None. When the server grows pagination, the same dataclass keeps working.

methodic.VariationData dataclass

VariationData(
    experiment_id: str,
    variation: int,
    config_json: dict[str, Any],
    config_yaml: str,
    created_at: str,
    created_by: str,
    state: str,
    accelerate_config_json: dict[str, Any] | None = None,
    accelerate_config_yaml: str | None = None,
    launch_config: dict[str, Any] | None = None,
    description: str | None = None,
    committed_at: str | None = None,
    retracted_at: str | None = None,
    retraction_reason: str | None = None,
    git_ref: str | None = None,
    git_sha: str | None = None,
    name: str | None = None,
    hypothesis: str | None = None,
    expected_outcome: str | None = None,
)

Mirror of the server's Variation struct.

config_json, accelerate_config_json, and launch_config arrive as arbitrary JSON — kept as dict[str, Any] since the schema is open.

methodic.VariationSummary dataclass

VariationSummary(
    variation: int,
    created_at: str,
    run_count: int,
    state: str,
    description: str | None = None,
    latest_status: str | None = None,
    committed_at: str | None = None,
    retracted_at: str | None = None,
    name: str | None = None,
    hypothesis: str | None = None,
)

One variation as it appears in ExperimentDetail.variations.

methodic.LineageResponse dataclass

LineageResponse(
    experiment_id: str,
    ancestors: list[Experiment],
    descendants: list[Experiment],
)

methodic.UpstreamRetraction dataclass

UpstreamRetraction(
    experiment_id: str,
    retracted_at: str,
    reason: str,
    depth: int,
    variation: int | None = None,
    document_asset_id: str | None = None,
    chain: list[str] | None = None,
)

methodic.UpstreamRetractionsResponse dataclass

UpstreamRetractionsResponse(
    has_retractions: bool,
    retractions: list[UpstreamRetraction],
)

methodic.CreateExperimentResponse dataclass

CreateExperimentResponse(
    experiment_id: str, variation: int, run: int
)

POST /experiments response: the new experiment plus the always-created variation 0 / run 0.

methodic.SearchResult dataclass

SearchResult(
    document_id: str,
    source_type: str,
    relevance_score: float,
    lineage_boost: bool,
    asset_type: str | None = None,
    title: str | None = None,
    snippet: str | None = None,
    experiment_ids: list[str] = list(),
    created_at: str | None = None,
)

One hit from the Vertex-backed search.

methodic.SearchResponse dataclass

SearchResponse(
    results: list[SearchResult],
    total_size: int,
    next_page_token: str | None = None,
)

methodic.SearchFilters dataclass

SearchFilters(
    asset_types: list[str] | None = None,
    organization_id: str | None = None,
    team_id: str | None = None,
    created_after: str | None = None,
    created_before: str | None = None,
    created_by: str | None = None,
    source_type: str | None = None,
    tags: list[str] | None = None,
)

Filters layered on top of the RBAC + namespace filters the server adds.

methodic.AssetUploadInfo dataclass

AssetUploadInfo(
    asset_id: str,
    asset_uri: str,
    upload_urls: dict[str, str],
)

Result of AssetsAPI.create_with_presigned: where to put each component.

Errors

methodic.ChronicleError

Bases: Exception

Base class for every error raised by the methodic client.

methodic.APIError

APIError(
    status_code: int,
    message: str,
    response: Response | None = None,
)

Bases: ChronicleError

HTTP error from the Chronicle API.

methodic.AuthenticationError

AuthenticationError(
    status_code: int,
    message: str,
    response: Response | None = None,
)

Bases: APIError

401 — missing or invalid credentials.

methodic.PermissionDeniedError

PermissionDeniedError(
    status_code: int,
    message: str,
    response: Response | None = None,
)

Bases: APIError

403 — caller lacks the required ACL grants.

methodic.NotFoundError

NotFoundError(
    status_code: int,
    message: str,
    response: Response | None = None,
)

Bases: APIError

404 — resource does not exist or is hidden by RBAC.

methodic.BadRequestError

BadRequestError(
    status_code: int,
    message: str,
    response: Response | None = None,
)

Bases: APIError

400/422 — malformed request body or invalid arguments.

methodic.ConflictError

ConflictError(
    status_code: int,
    message: str,
    response: Response | None = None,
)

Bases: APIError

409 — state conflict (e.g., commit on already-committed experiment).

methodic.ServerError

ServerError(
    status_code: int,
    message: str,
    response: Response | None = None,
)

Bases: APIError

5xx — Chronicle is unreachable, misconfigured, or buggy.