Skip to main content

probe-research (probe SDK/CLI + probe-research plugin)

License & repo status (2026-08-12). This project is open source under Apache-2.0. The source of truth now lives in Probe's monorepo, and this repository is transitioning into a CI-generated distribution mirror carrying exactly what installations pull from here: the Claude Code / Codex plugin marketplace (probe-research, probe-research-tap), client-version.json, and the CHANGELOG. Nothing changes for users — installed plugins keep updating from this repo, and the CLI/launcher keep shipping via PyPI and npm (npx probe-research). Until the cutover completes, the full source remains here and development continues in this repo.

CLI + SDK client for Probe Research, Probe's experiment-tracking platform. It is a thin client over the v3 ingestion contract (CONTRACT.md in the Probe Research backend). Implemented experiment calls map onto real endpoints.

Two client surfaces

Probe Research tracks the team's ML work — experiments and training runs, but also surveys, design decisions, data processing and provisioning — through two separate surfaces over the same backend, for two different workflows:

  • probe — SDK + CLI (non-agent). A Python library (import probe) and the probe command-line tool for integrating with existing setups and manual experimentation. Drop it into a training script or pipeline to record runs, metrics, spans, and artifacts. No agent required.
  • probe-research — plugin: skills + MCP (agent-centric). Installed into a coding agent (Claude Code or Codex). Its skills teach the agent the experiment workflow, its read-only MCP server lets the agent query experiment state, and writes flow through the probe CLI. This is the surface for agent-driven research loops such as Anthrogen.

Same backend, two entry points: humans-in-code reach for the SDK/CLI; agents-in-the-loop use the plugin.

Package boundaries

src/probe/
├── sdk/       # typed client, uploads, local capture, session adapter ABI
│   ├── fluent.py    # probe.init/log/finish — ambient run, contextvar-bound
│   └── analysis.py  # client.compare(): N runs aligned on step
├── cli/       # `probe`: thin shell over the SDK
└── mcp/       # `probe-research-mcp`: strictly read-only tools and resources
skills/
├── track-work/
├── show-research-status/
└── write-overview/

The SDK is the implementation. The CLI, MCP source adapter, future hooks, Python experiments, and passive platform integrations all use it. CLI and SDK therefore have capability parity; they differ only in ergonomics.

Codex packaging, transcript capture, and the three-layer release gate are documented in docs/codex-integration.md.

Surface SDK CLI Intended caller
Experiment upload Client.run, Run.log/span/log_artifact/snapshot/link/execute, Client.events, Client.promote run, log, span, artifact, snapshot, link, exec, notes, promote Researchers, agents, notebooks, training/platform code
Ambient upload probe.init/log/log_hw/log_artifact/span/finish, probe.active_run n/a (a CLI call has no ambient run) Training scripts, and library code with no handle to pass
Session adapter Client.sessions.attach/checkpoint/detach probe hook session ... Future deterministic hooks/broker only
Artifact reuse check Client.list_anchored(Anchor.SHARED, prefix=…) + list_artifact_versions, normally behind MCP probe artifact versions Agent through read-only MCP
Passive ingestion Client.ingest No convenience command yet Install-once platform integration
Read plane SDK reads used by probe.mcp; Client.compare for N-run analysis get/bundle diagnostics MCP for agents; CLI for humans/scripts

Session commands do not upload metrics or experiment outputs. They correlate a coding-agent session with a run and checkpoint redacted transcript metadata. No hooks are installed in this release.

Install

pip install -e ".[dev]"     # from this directory

Auth

probe login       # browser device flow (RFC 8628 + PKCE) — the default; nothing to paste

Air-gap paste path: probe login --token probe_pat_xxxxxxxx (verified via GET /v1/me); probe login --endpoint-only --base-url … saves the endpoint without minting a token. Both write ~/.config/probe/config.json.

Import existing work

Use Import existing work in probe wizard, or run:

probe backfill /path/to/work --project existing-project
probe backfill --transcripts-only --no-digest

In interactive onboarding, session and folder imports run in the background after review. The session picker detects saved histories independently of which coding agents you installed and selects all detected sources by default; only the histories you keep selected are imported.

The final onboarding page shows live progress for both kinds of import. You can exit while they run, then use npx probe-researchExisting imports to check results or resume work that needs attention. Workers survive a terminal disconnect and retry temporary network failures automatically. After a machine restart, reopening the installer resumes interrupted jobs from their saved checkpoints. Recovery keeps the original reviewed files, session versions and destination; changed files need another review. Direct probe backfill commands remain synchronous.

Folder imports review placement before writing and report current-file coverage separately from pending delivery. Rerun to resume; use the reported --source-id when a source folder moves within the same authenticated destination. Changed files and unresolved legacy matches require review through --import-changed or --import-unverified. References remain distinguished from uploaded bytes.

If the folder is a GitHub checkout, backfill checks the existing integration's access and uses bounded commit history as context. It reuses or attaches the corresponding Code source after placement review. File/Git reconstruction drafts are saved for explicit publication review; AI Summary refresh is a separate step.

Historical conversations import as standalone sessions. Backfill verifies their native session identity and preserves immutable retries, but does not infer or create project, experiment, run or other entity links. Protocol-2 transcript imports require the compatible engine/API and tap release.

probe wizardSign in or switch account is the same thing with a screen. It signs in (again, on a device that already holds a credential — that is how a wrong-account install is corrected), switches between accounts already saved here (the named contexts of probe context list), and signs out: revoking this device's token, clearing the active context, and stopping session capture so it cannot keep uploading to the account you just left. The plugins stay installed — removing those is Uninstall. Scripts use probe wizard --action login / --action logout, which never prompt. Or set env: PROBE_BASE_URL, PROBE_TOKEN (user token, /v1), PROBE_INGEST_TOKEN (ingest token, /ingest), PROBE_HMAC_SECRET (optional body-signature secret). SDK-created runs heartbeat every 60s so the server can reap crashed ones; PROBE_HEARTBEAT_SECONDS tunes the interval (<=0 disables).

You can also skip probe login entirely: the first client.run() / probe run start with no token triggers the same browser approval inline (TTY only) and persists the result. Disable with PROBE_AUTO_LOGIN=0; headless/CI keeps the crisp AuthError and should set PROBE_TOKEN.

The MCP server prefers PROBE_MCP_TOKEN, which should be a separately minted read-only token. It falls back to PROBE_TOKEN for local development, but exposes no mutation tools.

On rented compute (RunPod) with no standing config, the /track-work skill seeds PROBE_TOKEN at session start.

SDK (agent-driven / interactive)

Two forms, same implementation. The module-level one when you want to log from code that has no handle to pass around:

import probe

probe.init(
    project="folding",
    experiment="dockq-sweep",
    question="temp 0.7 wins",
)
probe.log({"loss": 0.42, "dockq": 0.71}, step=42)    # from anywhere, any thread
probe.finish()

probe.init() takes everything client.run() does and returns the same Run handle, so with probe.init(...) as run: works and the rest of the API is one attribute away. The binding is a contextvar backed by a process default: a worker thread finds the run (a bare contextvar would not — threads start with an empty context), while a second init() inside a thread or block shadows rather than hijacking the outer one. That is the part of wandb.init()'s global that silently corrupts concurrent runs. probe.active_run() returns the current binding, and a script that exits without finish() is closed at exit as completed / failed / canceled rather than left to the crash reaper.

The explicit form has no ambient state at all:

import probe

client = probe.Client()  # resolves creds from env / `probe login`

# run() resolves by default. `question=` is the one opt-in to creation, so the
# FIRST run in a new experiment says what you expect to see:
run = client.run(experiment="dockq-sweep", question="temp 0.7 wins",
                 name="run-1", project="folding",
                 description="DockQ baseline at temperature 0.7",
                 source="runpod", external_id="rp-9931")

# …and every run after that is bare — the experiment already exists, and its
# question is first-write-wins so reopening never rewrites it:
run = client.run(experiment="dockq-sweep")

# No question and no experiment? That is a project-direct run, which is the honest
# home for work with none — better than an experiment named after your cwd:
run = client.run(project="folding")

# Omit `name` and the server names the run after its slug — or after the petname
# short_id it mints when there is none — then replaces that with a generated title
# once the run reaches a terminal status. A name you supply is yours and is never
# overwritten. A slug that is a near-miss of an existing one is REFUSED, not
# created: a warning is invisible from a training loop. Creation is SDK-only —
# `probe run start` never creates, because on the CLI the slug is hand-typed every
# time, which is where typos come from.

client.update_run(run.id, name="DockQ baseline",
                  description="Stable reference run")
child = run.child("retry-1", relation="retry",
                  description="Retry after fixing the data loader")
run.snapshot()                                   # non-disruptive git + deps + GPU capture
run.link(wandb_run_id="abc123", s3_prefix="s3://x/y")

for step in range(100):
    run.log({"loss": ..., "dockq": ...}, step=step)     # POST /v1/runs/{id}/metrics

# Omit `step` and it auto-increments per metric kind, so the bare loop shape
# still produces a curve. `step=None` explicitly means no step at all
# (wall-clock axis) — that is what the CLI and the passive importers pass.
for batch in loader:
    run.log({"loss": loss})                             # steps 0, 1, 2, …

# Values of any type are accepted. Numbers (and bools, numpy scalars, 0-d
# tensors) become metric points and plot; strings, dicts, lists and None go into
# that step's record and read back through the trajectory view.
run.log({"loss": 0.4, "phase": "eval", "cfg": {"lr": 3e-4}})

# Below-run coordinates: `coords` are bounded grouping axes (series identity:
# rank/split/..., never a per-sample id), `labels` per-sample drill-down ids
# (point identity only). Everything logged inside the unit carries them, and
# nested units merge (child wins per key).
with run.unit(coords={"rank": 0}, labels={"sample": 3}):
    run.log({"reward": 0.71}, step=12)   # -> dimensions={"rank": 0}, labels={"sample": 3}

with run.span("rollout", name="rollout-0", step_index=1) as span:  # trajectory span
    span.attributes["reward"] = 0.8      # closes with ended_at + a terminal status,
    ...                                  # `failed` if the body raises; nests inside
run.log_artifact("final.sif", uri="r2://bucket/final.sif", kind="artifact")
run.finish()                                     # flushes spool, sets status+ended_at

Structured knowledge and local process capture use the same SDK:

run.execute(["python", "train.py", "--config", "dockq.yaml"])
client.events.add(run.id, "decision", "Use DockQ scorer v3", evidence_refs=["tool:91"])
report = client.check_run(run.id)

Data writes are fail-open by default: on failure they spool to disk (~/.local/state/probe/spool) and return, never blocking the training loop. run.finish() (or probe flush) replays the spool. Appends and queue rewrites are fsync'd and atomic. On rented compute, put the queue on durable storage with PROBE_SPOOL_DIR=/shared/probe/spool or probe --spool-dir /shared/probe/spool …. Pass strict=True to make a write raise.

run.snapshot() / probe snapshot RUN records git state, dependencies and hardware, and stores the files git cannot supply. Each file becomes its own artifact row on the run (visible in the artifact explorer, deduped across runs and users by content, uploaded in windows of 256 with every byte verified against the manifest before it leaves the machine); PROBE_CODE_STORAGE=archive keeps the older one-code-bytes-archive-per-run behaviour, and --max-upload-mb caps both storages. probe snapshot-show RUN lists every captured file and where its bytes are (captured row, code-bytes archive, or needs-upload); probe snapshot-restore RUN DEST rebuilds the tree, taking each file from the run's capture rows first and the archive second; probe artifact tree RUN --prefix P prints one folder level of the run's artifacts.

Miles tracking backend (probe.connectors.miles)

Drop-in wandb-parity backend for Miles' TrackingManager. Zero miles commits: the registry is a plain dict checked against args flags at init, so activation is two lines in your launcher, before init_tracking(args):

from probe.connectors.miles import register
register(args)          # registers the backend + sets args.use_probe

Every TrackingManager.log() lands with its own step counter (train/step and rollout/step map to step_index per key, the counter entry itself is stripped), values arrive after Miles' DP-rank reduction — exactly what wandb sees — and the run declares its labeled-point plan (num_rollout x rollout_batch_size x n_samples_per_prompt) so later per-sample capture never trips the server's default budget mid-training. Config: PROBE_BASE_URL / PROBE_TOKEN, optional args.probe_experiment / args.probe_run_name (fall back to the wandb names). Fail-open end to end: a broken tracker never costs a training step; non-finite values are dropped per-point. Per-rank and per-sample detail is enabled without replacing aggregate logging by passing:

--custom-rollout-log-function-path probe.connectors.miles.per_sample_rollout_log

The hook logs labeled sample points through the same durable queue as Miles' aggregate tracking.log() calls. Every point carries metric_scope=sample, the Miles sample id, an optional group id, and—when the sample carries Harbor's returned capture external_key—the exact deterministic rollout span. The dashboard can therefore separate aggregate and sample points while resolving sample → trial → trajectory/sandbox without a Miles-core change.

Reward and effective response length are captured by default. Applications can publish arbitrary numeric sample measurements without connector changes by putting an inline dictionary on the sample:

sample.metadata["probe_metrics"] = {
    "agent/input_tokens": input_tokens,
    "agent/output_tokens": output_tokens,
    "quality/custom_score": score,
}

If values already live elsewhere on the Miles sample, declare metric-name to dotted-path mappings on the same args object Miles sends to RolloutManager:

args.probe_sample_metrics = {
    "agent/observed_tokens": "metadata.agent_metrics.observed_tokens",
    "agent/turns": "metadata.agent_metrics.turns",
    "agent/tool_calls": "metadata.agent_metrics.tool_calls",
}

Stock Miles launchers that do not expose arbitrary rollout args can put the same inline mapping in a tiny importable hook module instead:

# my_project/probe_metrics.py
from probe.connectors.miles import make_per_sample_rollout_log

per_sample_rollout_log = make_per_sample_rollout_log({
    "agent/observed_tokens": "metadata.agent_metrics.observed_tokens",
    "agent/turns": "metadata.agent_metrics.turns",
    "agent/tool_calls": "metadata.agent_metrics.tool_calls",
})

Then set --custom-rollout-log-function-path to my_project.probe_metrics.per_sample_rollout_log. This stays entirely outside Miles source while preserving the shipped hook's durable queue, sample labels, and Harbor span linkage.

Missing, non-numeric, boolean, and non-finite values are omitted; an explicit numeric zero is retained. Configured paths override a same-named metadata["probe_metrics"] entry. The run reserves 1,024 sample metric points per sample by default; set args.probe_sample_metric_budget higher when a sample schema intentionally exceeds that.

Per-rank detail otherwise rides the capture-at-source arc (run.unit + capture_trial). Upstreaming a native --use-probe flag into a miles fork is optional polish (registry docstring's own recipe).

SDK (install-once / passive push)

client.ingest(
    project_slug="protein-folding",
    experiment_slug="dockq",
    run={"name": "r1", "source": "temporal", "external_id": "wf-1", "status": "running"},
    metrics=[{"kind": "model", "key": "loss", "value": 0.5, "step_index": 1}],
    batch_id="deadbeef",          # idempotent redelivery
)

One idempotent push (bearer ingest token + optional HMAC), keyed on (customer_id, source, external_id).

SDK (reading runs back for comparison)

comparison = client.compare(experiment_id=exp_id, keys=["dockq"])
aligned = comparison.aligned("dockq")

for label, values in aligned.values.items():
    plot(aligned.steps, values, label=label)     # or aligned.to_pandas()

Name the runs with run_ids=[...] or select them with the filters list_runs takes (experiment_id=, group_id=). One POST /v1/series/query per 50 runs — more than that batches rather than truncating, because silently dropping runs 51+ reads as "these are all of them". Columns are labelled by the server's petname short_id; runs of differing length keep None holes rather than being cut to the shortest, since differing length is usually what is being compared. pandas is optional and only touched by .to_pandas().

There is no separate read client. wandb.Api() is a distinct object because W&B has two transports (a service process for writes, GraphQL for reads); one REST transport does not need the split, so this is a shaping layer on Client.

CLI (probe)

probe project create folding
probe experiment create dockq --question "temp 0.7 wins" --project folding
RUN=$(probe run start --experiment dockq --name run-1 \
        --project folding --source runpod --external-id rp-9931 \
        --description "DockQ baseline at temperature 0.7")
probe project set folding --name "Protein folding" --description "DockQ studies"
probe project get folding | jq -r '.summary_markdown // ""' > PROJECT.md
# Edit the whole visible project document, retaining useful existing sections.
probe project set folding --summary @PROJECT.md
probe project get folding | jq -r '.summary_markdown // ""'  # verify what landed
probe experiment set EXPERIMENT_ID --name "DockQ sweep" --description "Temperature sweep"
probe experiment set EXPERIMENT_ID --summary @EXPERIMENT.md
probe run set $RUN --name "DockQ baseline" --description "Stable reference run" \
  --summary @RUN.md
probe snapshot $RUN
probe link $RUN --set wandb_run_id=abc --set gpu_job=rp-9931
probe log $RUN loss=0.42 dockq=0.71 --step 42
probe span add $RUN --type rollout --name rollout-0 --step 1
probe artifact add $RUN ./final.sif --kind artifact
probe notes show                      # the project's notes
probe notes write --append ./note.md  # free-text markdown, one file per project
probe exec $RUN -- python train.py --config dockq.yaml
probe run check $RUN
probe run end $RUN --status completed
probe bundle $RUN            # read: run + series + artifacts
probe artifact tree $RUN     # read: one folder level of the run's artifacts (--prefix P --limit N)

Project, experiment, and run prose each has three homes: description is the short identity; summary_markdown is the durable teammate-facing document shown immediately below the AI Summary in Overview; and probe notes is the private operational briefing for agents. Authored Markdown is stored independently, whole-document and last-write-wins, so use a read-edit-write-read loop. Private notes support --append when concurrent handoffs must not overwrite one another. A line containing only [README](https://github.com/owner/repo) embeds that repository's README at that point in any of those visible documents.

Curves in the terminal (probe metrics plot)

The same read probe metrics wide does, drawn instead of dumped.

probe metrics plot $RUN                                  # board: every series, one spark each
probe metrics plot $RUN --key train/loss                 # one panel, braille, with axes
probe metrics plot $RUN --key train/loss --key eval/loss # a panel each
probe metrics plot $RUN --key rollout/response_len/{mean,median} --overlay
probe metrics plot $RUN --key train/loss --ascii --no-color   # for a pipe or a dumb terminal

Bare, it prints the board — every key the run logged with its last/min/max, which is the overview to read before choosing what to look at. --key promotes those series to full panels; several keys get a panel each, because two metrics on different scales do not share an axis. --overlay puts them on one canvas and therefore one y-axis, drawing each series with its own glyph (identity that survives a pipe or a monochrome terminal) and saying in the footer when the scales are far enough apart that the smaller curve has flattened.

Anything the picture cannot show is printed to stderr before it: a read the window cut short, a --key that matched nothing, a series dropped from a full overlay, a non-finite point no scale can hold. On the canvas, % marks a cell more than one curve reached.

Unlike its siblings in probe metrics, this one takes a petname short_id as well as a uuid. Color is off when stdout is not a TTY and whenever NO_COLOR is set; braille and box-drawing degrade to ASCII when the stream's encoding cannot carry them.

Harbor trial capture (probe trial)

Capture a Harbor trial directory into a run, keyed to the training step — the sandbox↔step join (see docs/2026-07-15-harbor-native-ownership-plan.md for status: what's shipped vs parked):

# rollout span + reward metric + labeled CAS file uploads + kind=harbor_trial
# manifest; a recognized trajectory format (ATIF v1.x built in) also expands
# into turn/tool_call spans under the rollout span
probe trial add $RUN jobs/my-job/trials/swe-fix__x1 --step 600 --env-type skypilot-fork
probe trial add $RUN <dir> --step 601 --no-expand      # raw-only capture
# Copy/checksum a host trial tree onto a durable volume without touching the network.
probe trial stage <host-trial-dir> --to /shared/probe/trial-601 \
  --expect result.json --expect lock.json
# Retry one or every Miles bridge request. The descriptor supplies run/step/correlation.
probe trial export /shared/probe/trial-601/export-request.json
probe trial drain /shared/probe/captures
probe trial watch /shared/probe/captures --interval 5
# Bind descriptors produced during an offline run initialization:
probe trial drain /shared/probe/captures --run "$PROBE_RUN_ID"
# retroactively expand a stored trajectory (e.g. after a fork's parser ships);
# deterministic span ids make this idempotent — re-runs upsert, never duplicate
probe trial expand $RUN <manifest-artifact-id> --max-spans 0

Query it back: client.list_run_artifacts(run_id, kind="harbor_trial", step_from=599, step_to=601). Fork parsers plug in via probe.connectors.atif.register_trajectory_parser("their-format", fn); unknown formats are captured raw (never rejected) and expanded later.

probe trial stage and the probe-harbor-export/1 consumer keep an atomic .probe-capture.json beside the durable trial bytes. Its collection status is separate from remote-upload status, so an exporter outage leaves a precise, retryable list of unconfirmed files instead of losing their paths. Stable external keys and span IDs make retries update the same rollout; arbitrary Miles/Osmosis correlation fields are preserved under the harbor_trial manifest's source.context. The exporter additionally promotes sample_id and group_id to the Probe point labels sample and group, respectively, on the reward and harbor_trial manifest. Those labels preserve distinct same-step samples without creating a separate metric series.

Trajectory sources — Harbor-first, by decision. Today trajectories enter Probe through Harbor's on-disk contract: ATIF-supporting agents write trajectory.json into their logs dir, the harness delivers it at <trial_dir>/agent/trajectory.json (the location Harbor's own viewer reads), and parse_trial/capture_trial pick it up from there — raw bytes always stored, recognized formats expanded into turn/tool_call spans, unknown formats expandable retroactively via probe trial expand. Emission is per-agent opt-in upstream (SUPPORTS_ATIF; e.g. the oracle agent emits nothing), so absence is a normal, captured state. "Traditional" trajectory tracking — live SDK/OTel-style span streaming from instrumented agent code, the W&B Weave/MLflow-Tracing model, with no file contract at all — is planned as the second door: the server-side turn/tool_call span rails it needs already exist, so it is an SDK-instrumentation arc, not a schema change. For now the scope is deliberately the Harbor framework.

The completeness claim is intentionally bounded: it covers declared regular files in the host Harbor trial directory. Public Harbor tears down the sandbox before Trial.run() returns, so a post-run SDK consumer cannot know about undeclared state Harbor never materialized. The ledger reports that state as unknown, inventories explicitly declared missing files, and treats hidden files/symlinks as visible skips. A true pre-teardown guarantee requires the producer or environment implementation to invoke durable collection from its lifecycle hook.

The following commands are reserved for future hook configuration and are not part of the normal researcher workflow:

probe hook session attach RUN --session-id SESSION --transcript-path PATH --cwd DIR
probe hook session checkpoint RUN --session-id SESSION --transcript-path PATH --reason pre_compact
probe hook session detach RUN --session-id SESSION --reason session_end

They currently encode session links in run.metadata.agent.sessions[] and transcript checkpoints as redacted local-reference artifacts. Until managed artifact upload exists, transcript portability remains explicitly false.

Read-only MCP server

Run the stdio server with probe-research-mcp. It exposes six tools:

Tool Answers
browse "What exists here?" — the structured project → experiment → run tree
search_knowledge "Find things about X" — one-index exact+semantic search with per-result provenance
entity "Show me this thing" — one entity through a purpose-shaped view
metrics "What are the numbers?" — one run's metrics at the mode you ask for
probe_procedures "What are this team's rules for what I'm about to do?" — workflow memory
find_papers "What does the literature report?" — 40M+ abstracts, and reading the papers

Use the host agent's web tools for general web searches and opening URLs. find_papers retains its search, read, and similar modes for research papers.

Every tool accepts token_budget and cursor. The budget applies to the complete response, including its envelope, errors, verbose output, and all items in an entity batch. It defaults to 2,000 reference tokens and accepts integers from 512 through 8,000; the UTF-8 byte ceiling is eight times that value. The frozen o200k_base encoding ships in the package and works offline. These reference counts describe tool text, not model-specific billing or total conversation usage. The MCP emits one compact text payload without a duplicate structured payload.

A fitting result retains its view shape. Larger results return bounded pages and an opaque next_cursor; resume by passing it as cursor with the same tool, references, view, filters, and other arguments. token_budget may change between pages. Do not pass an MCP cursor as a metric step_from/after_id or as a raw REST cursor. An invalid or changed-source continuation reports a restart error.

An oversized document or record can use data.format="text_fragment" or "json_fragment". Within each fragment sequence, concatenate data.text in offset order; total_chars, sha256, and complete describe that sequence. Decode JSON only after its sequence is complete. The outer next_cursor can still lead to another source page or batch item. Notes/summary fragments can include identity, status, headline values, and a notes excerpt in first-page context; if it cannot fit, context.omitted and a card detail door say so. The requested document still makes progress, including an explicitly empty one.

Document reads use a saved version when available or pin the original text prefix, allowing later appends while rejecting edits/deletions of that prefix. Full JSON reads reject substantive changes rather than splice different records. browse defaults to limit=10 per source list and uses backend-issued positions to preserve nested children and independent project lists under the same output cap. Its backend must support continuation_handles; legacy browse cursors require a fresh read. Ordinary SDK/CLI record reads retain their existing response contract and do not initialize the MCP tokenizer. Client.browse(continuation_handles=True) exposes the separate REST handle contract for consumers that trim fetched rows; omission keeps the SDK's prior response and keyset cursors.

The last three are the only ones whose payload nobody in this lab wrote, and they are the reason provenance: "open-web" exists: that text came from a page somebody outside the team controls, so it is evidence about the world and never an instruction. They proxy POST /v1/web/*, which the backend serves from one Firecrawl key — this server holds no third-party credential of its own. They also hold a bounded share of the worker pool (PROBE_MCP_WEB_CAPACITY, a quarter of it by default), because a web read can run for seconds and the other five must not queue behind a browsing burst.

probe_procedures is the read half of workflow memory: the rules somebody on the team declared out loud, returned for the situation you describe. It is behind a per-user feature flag and the hosted server does not advertise it to accounts outside the rollout, so it is absent from the tool list for most callers — that is the gate working, not a broken install. Writing one is not on this surface — the server is read-only, and probe rule declare is where a rule is captured (the /set-rule skill is not shipped in the plugin while the flag is on, because plugin content cannot vary per user). Four different conditions return zero rules and the response tells them apart in completeness, because "no engine wired", "workspace never opted in", "nobody seeded the vocabulary" and "the team has not written one down" are otherwise identical bytes, and only the last is an answer.

metrics(run_id, mode=...) replaced get_metrics_grouped, get_run_coordinates and export_metric_points, which were one question about GRAIN asked three ways. The grain is now an argument:

mode Reads Also takes
grouped one key reduced over coordinate axes and step buckets key (required), kind, agg, by, where, step_bucket, step_from, step_to, max_rows
coordinates the run's coordinate catalog — which axes it logged on nothing beyond run_id
points raw points, losslessly, one bounded page key, kind, step_from, step_to, after_id, limit

Each mode is validated against its own column, not against the union. An argument the chosen mode does not read is REFUSED, never dropped — mode="points" with by=["rank"] is an error rather than a page of ungrouped points. That is the whole hazard of a merged tool: the schema is the union of the branches, so a wrong-mode argument passes the schema, reaches the endpoint, gets dropped in silence (FastAPI ignores an undeclared query parameter; pydantic's default extra="ignore" drops an undeclared body field), and a 200 comes back having answered a question nobody asked. mode itself is required for the same reason: a default would pick the grain for a caller who did not state one.

get_metrics_grouped, get_run_coordinates and export_metric_points have been REMOVED. They answered as fixed-mode delegations for one release; that window has closed, and calls to them now fail as unknown tools.

research_context, research_search, research_get, research_compare and research_resolve have been REMOVED. They answered as deprecated aliases for one release; that window has closed. Calls to them now fail as unknown tools.

Thin harness, fat skills. Coverage grows through entity's view and filters parameters, never through more tools. browse is the one addition that cleared that bar: it answers a question the others structurally cannot, because search ranks by relevance to a query and therefore needs you to already know what to search for.

entity(refs=[...], view=..., filters=..., token_budget=..., cursor=...) accepts 1–20 references, each at most 256 characters. Keep the kind: prefix on the slug or UUID returned by browse/search; team-note addresses the shared team document.

Kind Views
run card · record · notes · summary · trajectory · trials · metrics · artifacts · reproduce · handoff · lineage · events · code
experiment card · record · notes · summary · artifacts · lineage · groups · versions · reproduce · code
artifact card · record · versions
project card · record · artifacts · notes · summary · code · papers
group card · record · notes
trial card · record · trajectory
session card · record · transcript · digest
team note card · record

card (the default) returns available_views for that entity, so one call tells you what else you can ask for — the matrix above is documentation, not something to memorise. An unrecognised ref kind is rejected outright rather than guessed at: the resolver used to try every getter in turn, so a retired kind surfaced whichever backend parse error came first instead of saying the kind was gone.

Cards carry a compact selection of identity, status, headline values, and authored caveats. view="record" returns the resolved source record through bounded pages. Select one field with filters={"field":"metadata.summary"}; use a path list such as filters={"field":["metadata","metric.with.dots"]} for a literal dotted key. For example, these are arguments to the MCP entity tool:

{"refs":["run:example-run"],"view":"record","filters":{"field":"config"},"token_budget":2000}

The continuation request retains those arguments and adds "cursor":"<next_cursor>". notes reads operational notes; summary reads authored Overview Markdown and includes a bounded notes caveat alongside existing status/headline context. An excerpt explicitly reports truncated:false when complete, or truncated:true with its detail door. Artifact notes use view="record", filters={"field":"notes"} because artifacts have no MCP notes view.

Artifacts resolve by NAME, because the reuse check has a name and not an id — and because no GET /v1/artifacts/{id} route exists. The lookup runs against the SHARED, lab-wide level, which is where an official artifact is promoted to and the nearest thing to the tenant-unique names the retired asset registry enforced. entity(refs=["artifact:<name>"], view="versions", filters={"requirement": ">=2"}) is where research_resolve went. A name that does not exist raises not-found; a name that exists with no satisfying version returns state="no_match" with the versions that do exist, so you can see the real ceiling. Requirements match monotonic integers and labels, not semver — ">=2.0" is rejected rather than silently matching nothing. A name carried by more than one shared artifact is a 422 naming both ids: the duplication this check exists to prevent has already happened, and picking one silently would compound it. An artifact:<uuid> also resolves: versions work for any anchor, while a non-shared ID has limited by-ID metadata. Its card reports shared:false, a resolution note, and the versions detail door; record cannot supply fields the source did not return.

trajectory reads a run's spans (the run bundle carries span_type counts only). metrics returns series summaries, and filters={"key": "<key>"} drills to raw points. reproduce resolves env_ref through its execution record. These reads all use the complete response budget above; a large reproduction manifest remains reconstructable through bounded fragments and next_cursor.

There is no trace-file tool: no backend trace index has ever existed, so it answered matches: [] to every query, which agents read as "this file has no lineage". To trace a path/URI/hash, use search_knowledge (its exact channel matches artifacts) and follow entity(refs=[...], view="lineage") on a kind that supports that view.

MCP reads through the Probe Research API—never directly from Postgres or R2. Its logical sources are control identity/tenant scope, the structured experiment store, the artifact/manifest registry, the one-index search door (POST /v1/search: exact SQL channel + the KB engine's semantic channel; search capabilities are discovered against the live backend with one cached probe), and object-store resource pointers returned by the API. W&B, RunPod, Kubernetes, Git, and local transcript paths are not live MCP sources; adapters upload their identifiers and evidence first.

Hosted deployment and tokenizer health checks are described in the hosting reference.

Skills

One skill carries the whole arc. probe run start opens a run in an experiment that already exists, or project-direct with no experiment at all; it never creates. Creation on the CLI is always probe project create / probe experiment create.

  • track-work is the tracking switch (explicit off/on for this conversation, and typed bare it toggles; an agent's own bare invocation only loads guidance) plus everything recorded while it is on: orient against what exists, create the project and experiment explicitly — first, before the scaffold — route what the work produces (files to artifacts on the right anchor, numbers to metrics, decisions to notes), get inputs into the run snapshot, read back what actually landed, and close with the real lifecycle outcome. It is re-entered through the session rather than run once. Its reference.md holds the capture-call and artifact command syntax, the publication sequence, and project admin.
  • show-research-status renders where the work stands: the tracked state, the gaps, and the arc as one timeline with the next action named.
  • write-overview writes the first version of a project's or experiment's Overview page through the agent door (probe overview write): an artifact someone else reads to understand what is going on, under the same contract the dashboard's own lane obeys and keeps current afterwards.

Reuse hooks are deliberately deferred. track-work contains the reuse-before-create rule; deterministic enforcement can be added later without changing the SDK, CLI, MCP, or skill contracts.

What maps to what (v3 endpoints)

Client call Endpoint
client.run() / run.child() POST /v1/experiments, POST /v1/experiments/{id}/runs
run.log() / run.log_hw() POST /v1/runs/{id}/metrics, and /steps for non-numeric values
run.span() / run.step() POST /v1/runs/{id}/spans | /steps
run.log_artifact() POST /v1/runs/{id}/artifacts
run.snapshot() POST /v1/execution-records, then one row per file via POST /v1/runs/{id}/artifacts/uploads/batch + POST /v1/artifacts/confirm/batch — or, with PROBE_CODE_STORAGE=archive, the code-bytes archive via POST /v1/runs/{id}/artifacts
client.list_run_artifact_tree() (probe artifact tree) GET /v1/runs/{id}/artifacts/tree?prefix=&limit=
client.presign_download_batch() (used by probe snapshot-restore) POST /v1/artifacts/download/batch
run.link() PATCH /v1/runs/{id} (merges metadata.foreign_keys)
run.finish() PATCH /v1/runs/{id}
client.events.add() POST /v1/runs/{id}/artifacts (kind=research_event, v3 encoding)
client.sessions.* PATCH /v1/runs/{id} + transcript artifact metadata (hook ABI)
client.ingest() POST /ingest/v1/runs
client.run_bundle() / run_lineage() GET /v1/runs/{id}/bundle | /lineage
client.search() (used by research_search) POST /v1/search (exact+semantic, sectioned)

v0.4.0.0 ingestion fold-in (Phase 1)

Most earlier gaps are closed by Probe Research v0.4 (PR #13). Now wired:

  • Real metric dimensions. log_hw(..., device=3, host="n1") sends dimensions (fold #9); log(..., dimensions={...}). No more key-encoding.
  • Presign artifact upload. log_artifact(path=...) runs presign → PUT to R2 → confirm (fold #16), carrying kind/meta so byte uploads are labeled like reference artifacts (Harbor-ownership Phase 0). Fails open to a reference on error.
  • Execution records. snapshot() posts a content-addressed execution-record (fold #7); client.execution_record(...).
  • Artifact versions. client.list_artifact_versions() + create_artifact_version(). The separate asset registry that used to sit here was folded into artifacts (research-os #143/#144): an artifact is a named thing in a container with a chain of immutable versions, which is what an asset was.
  • Experiment versions. client.experiment_version() mints the immutable manifest (fold #6). This replaces the removed run-level promote.
  • Lineage edges. client.add_edge() / run.edges() (fold #2).
  • foreign_keys. first-class on the ingest path (run['foreign_keys'], fold #8) and surfaced on reads (run.foreign_keys, run.short_id).
  • Events read. client.events.list() / for_run() (server-emitted lifecycle log).

Remaining

  • MCP semantic/KB search is now wired to POST /v1/search (workspaces+kb fold-in) with an honest keyword fallback on older backends; transcript evidence is not indexed yet. Session hooks remain later work.
  • Harbor-native ownership Phases 1–3 (trial capture connector, capture-at-source, platform surface): see docs/2026-07-15-harbor-native-ownership-plan.md.

(Previously listed here and since shipped: RunPatch foreign_keys/env_ref parity, asset materialize, upload kind/meta, and server-side artifact list filters ?kind=&step_from=&step_to=.)

Typed models (generated from the OpenAPI contract)

Request/response models are generated from the backend's OpenAPI schema, not hand-written, so the client cannot silently drift from the contract. The write paths (log/span/log_artifact/ingest/edges/execution-records) build their payloads through the generated models, so a renamed or removed field fails client-side instead of as a server 422. /ingest/v1/runs is now declared in the schema too (Probe Research PR #12), so the passive push is generated and validated like every other path.

  • schema/openapi.json - a snapshot of Probe Research's FastAPI schema.
  • src/probe/_generated/models.py - generated, never hand-edited.
  • src/probe/models.py - the stable import seam the SDK uses.

Refresh when the contract moves:

make regen        # dump-openapi (RESEARCH_OS=../../research-os) + gen-models
# or step by step:
RESEARCH_OS=/path/to/research-os python scripts/dump_openapi.py
python scripts/gen_models.py

RESEARCH_OS points at a local checkout of the Probe Research backend source repo (directory name research-os); it is only used to regenerate the schema snapshot.

CLI grammar note

The CLI is built on typer. Connection flags are global and go before the command: probe --token probe_pat_x log RUN loss=0.1. probe login also accepts them directly (probe login --token ...).

Tests

pytest        # mocked/unit tests plus real-git snapshot tests; no live server

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

probe_research-0.154.0.tar.gz (6.5 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

probe_research-0.154.0-py3-none-any.whl (5.3 MB view details)

Uploaded Python 3

File details

Details for the file probe_research-0.154.0.tar.gz.

File metadata

  • Download URL: probe_research-0.154.0.tar.gz
  • Upload date:
  • Size: 6.5 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for probe_research-0.154.0.tar.gz
Algorithm Hash digest
SHA256 cc2d0dcc081f81d3ea69d58a2ea449ef9f2851cd94c4bbb33df312767cdca6d7
MD5 c13136c9fe878b65c02811df109f6ffb
BLAKE2b-256 dea77f3e2961f32a89869861b51d6652bcd8dd7bc0482674777dffbe42877d5b

See more details on using hashes here.

Provenance

The following attestation bundles were made for probe_research-0.154.0.tar.gz:

Publisher: release.yml on prbe-ai/research-os

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file probe_research-0.154.0-py3-none-any.whl.

File metadata

File hashes

Hashes for probe_research-0.154.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f947a787c6e86a9f4a3e36de3c767223d67bf25d53fa93829ed11425d1f12666
MD5 abcab02a3e389f25e0644f713590ea71
BLAKE2b-256 43d8ebcaf83f189c1b772d48da1af0b1b1b46477e886aa366fd354d52c124b3c

See more details on using hashes here.

Provenance

The following attestation bundles were made for probe_research-0.154.0-py3-none-any.whl:

Publisher: release.yml on prbe-ai/research-os

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.162.0

2 files

0.161.0

2 files

0.160.3

2 files

0.160.2

2 files

0.160.1

2 files

0.160.0

2 files

0.159.1

2 files

0.159.0

2 files

0.158.10

2 files

0.158.9

2 files

0.158.8

2 files

0.158.7

2 files

0.158.6

2 files

0.158.5

2 files

0.158.4

2 files

0.158.3

2 files

0.158.2

2 files

0.158.1

2 files

0.158.0

2 files

0.157.0

2 files

0.156.2

2 files

0.156.1

2 files

0.156.0

2 files

0.155.4

2 files

0.155.3

2 files

0.155.2

2 files

0.155.1

2 files

0.155.0

2 files

0.154.4

2 files

0.154.3

2 files

0.154.2

2 files

0.154.1

2 files

This release

0.154.0 This release

2 files

0.153.0

2 files

0.152.0

2 files

0.151.2

2 files

0.151.1

2 files

0.151.0

2 files

0.150.3

2 files

0.150.2

2 files

0.150.1

2 files

0.150.0

2 files

0.149.0

2 files

0.148.2

2 files

0.148.1

2 files

0.148.0

2 files

0.147.2

2 files

0.147.1

2 files

0.147.0

2 files

0.146.0

2 files

0.145.0

2 files

0.144.0

2 files

0.143.0

2 files

0.142.0

2 files

0.141.1

2 files

0.141.0

2 files

0.140.0

2 files

0.139.0

2 files

0.138.0

2 files

0.137.0

2 files

0.136.0

2 files

0.135.0

2 files

0.134.0

2 files

0.133.0

2 files

0.132.0

2 files

0.131.1

2 files

0.131.0

2 files

0.130.0

2 files

0.129.0

2 files

0.128.0

2 files

0.127.0

2 files

0.126.0

2 files

0.125.0

2 files

0.124.0

2 files

0.123.0

2 files

0.122.0

2 files

0.121.0

2 files

0.120.0

2 files

0.119.0

2 files

0.118.0

2 files

0.117.0

2 files

0.116.0

2 files

0.115.0

2 files

0.114.1

2 files

0.114.0

2 files

0.113.0

2 files

0.112.0

2 files

0.111.0

2 files

0.110.0

2 files

0.109.0

2 files

0.108.0

2 files

0.107.0

2 files

0.106.0

2 files

0.105.2

2 files

0.105.1

2 files

0.105.0

2 files

0.104.0

2 files

0.103.1

2 files

0.103.0

2 files

0.102.0

2 files

0.101.0

2 files

0.100.0

2 files

0.99.1

2 files

0.99.0

2 files

0.98.0

2 files

0.97.0

2 files

0.96.0

2 files

0.95.1

2 files

0.95.0

2 files

0.94.2

2 files

0.94.1

2 files

0.94.0

2 files

0.93.0

2 files

0.92.0

2 files

0.91.0

2 files

0.90.0

2 files

0.89.0

2 files

0.88.0

2 files

0.87.0

2 files

0.86.0

2 files

0.85.0

2 files

0.84.0

2 files

0.83.0

2 files

0.82.0

2 files

0.81.0

2 files

0.80.0

2 files

0.79.0

2 files

0.78.0

2 files

0.77.0

2 files

0.76.0

2 files

0.75.0

2 files

0.74.0

2 files

0.73.1

2 files

0.73.0

2 files

0.72.1

2 files

0.72.0

2 files

0.71.0

2 files

0.70.3

2 files

0.70.1

2 files

0.70.0

2 files

0.69.0

2 files

0.68.3

2 files

0.68.2

2 files

0.68.1

2 files

0.68.0

2 files

0.67.0

2 files

0.66.0

2 files

0.65.0

2 files

0.64.0

2 files

0.63.0

2 files

0.62.0

2 files

0.61.0

2 files

0.60.0

2 files

0.59.0

2 files

0.58.0

2 files

0.57.1

2 files

0.57.0

2 files

0.56.1

2 files

0.56.0

2 files

0.55.0

2 files

0.54.0

2 files

0.53.0

2 files

0.52.0

2 files

0.51.0

2 files

0.50.1

2 files

0.50.0

2 files

0.49.1

2 files

0.49.0

2 files

0.48.4

2 files

0.48.3

2 files

0.48.2

2 files

0.48.1

2 files

0.48.0

2 files

0.47.0

2 files

0.46.0

2 files

0.45.0

2 files

0.44.0

2 files

0.43.0

2 files

0.42.0

2 files

0.41.0

2 files

0.40.0

2 files

0.39.0

2 files

0.38.0

2 files

0.37.0

2 files

0.36.0

2 files

0.35.0

2 files

0.34.0

2 files

0.33.0

2 files

0.32.0

2 files

0.31.0

2 files

0.30.0

2 files

0.29.0

2 files

0.28.0

2 files

0.27.1

2 files

0.27.0

2 files

0.26.4

2 files

0.26.3

2 files

0.26.0

2 files

0.25.0

2 files

0.24.0

2 files

0.22.0

2 files

0.15.0

2 files

0.14.5

2 files

0.14.4

2 files

0.14.3

2 files

0.14.2

2 files

0.14.1

2 files

0.14.0

2 files

0.13.2

2 files

0.13.1

2 files

0.13.0

2 files

0.12.0

2 files

0.11.0

2 files

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

0.8.2

2 files

0.8.1

2 files

0.8.0

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page