Skip to content

Client reference

Top-level

methodic.Chronicle

Chronicle(
    server_url: str,
    api_key: str,
    timeout: int = 30,
    max_upload_workers: int = 2,
)

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_...")

# 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.

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).

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.

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.

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.

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.

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.

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,
) -> Experiment

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

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. Cursor-aware (when server supports it).

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.

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).

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,
) -> Variation

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

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.

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).

methodic.AssetsAPI

AssetsAPI(transport: Transport)

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.

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,
) -> dict[str, Any]

Upload a small inline asset. Chronicle auto-finalizes.

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,
) -> AssetUploadInfo

Register a new asset and get presigned PUT URLs for each component.

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.

presign

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

Request presigned URLs for an asset's components.

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.

iter

iter(
    query: str,
    *,
    filters: SearchFilters | dict[str, Any] | None = None,
    experiment_context: list[str] | 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,
    page_size: int | None = None,
    page_token: str | None = None,
) -> SearchResponse

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

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,
)

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,
)

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,
)

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.