Metadata-Version: 2.4
Name: keystone-agent-client
Version: 0.1.4
Summary: Keystone consumption client — discover + invoke DEPLOYED pro-code agents from any app (US-19 / FDP-3442). Speaks only the public gateway surface through Kong; no LangGraph/SDK deps.
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: httpx<1,>=0.27
Provides-Extra: dev
Requires-Dist: pytest<9,>=8; extra == "dev"
Requires-Dist: pytest-cov<7,>=6; extra == "dev"
Requires-Dist: ruff==0.15.15; extra == "dev"

# keystone-agent-client

Consumption client for **deployed** Keystone pro-code agents (US-19 / FDP-3442): discover
agents in your workspace and invoke them from any Python app — sync, streaming (SSE), or
background runs — without learning the gateway's transport quirks.

**Dependency-thin by design (D-M4-G):** `httpx` only. Embedding an agent must not pull the
authoring SDK's LangGraph tree into your app. (Authoring agents? That's
`keystone-agent-sdk` + the `keystone` CLI — not this package.)

## Install

Published with the `keystone-cli` bundle (FDP-4561). Pick the channel that matches the
environment you call — the package **name** carries the channel, the import path does not:

| Environment | Package | Index |
|---|---|---|
| dev | `keystone-agent-client-dev` | GAR `onx-py-public` |
| qc | `keystone-agent-client-qc` | GAR `onx-py-public` |
| staging | `keystone-agent-client-staging` | GAR `onx-py-public` |
| prod | `keystone-agent-client` | Public PyPI |

```bash
# dev / qc / staging — GAR needs Google credentials to READ (401 without them), so uv asks
# `keyring`, whose GAR plugin hands over your gcloud login. One-time setup:
uv tool install keyring --with keyrings.google-artifactregistry-auth
gcloud auth application-default login     # an account with read access to vinid-devops
# then:
uv add keystone-agent-client-dev --keyring-provider subprocess \
  --index https://oauth2accesstoken@asia-east1-python.pkg.dev/vinid-devops/onx-py-public/simple/

# prod — Public PyPI, no credentials
uv add keystone-agent-client
```

GAR is therefore only reachable by people and runners with a Google identity on
`vinid-devops`; a consumer outside the organisation uses the PyPI (prod) package.

Every channel installs the same module: `from keystone.agent_client import …`.

## Quickstart

```python
from keystone.agent_client import AgentPlatformClient

# Config from args or env: KEYSTONE_API_BASE / KEYSTONE_TOKEN / KEYSTONE_WORKSPACE_ID
# (the CI convention, FDP-3441). No stage: that tier is retired (FDP-2071).
with AgentPlatformClient() as client:
    # Every page of the workspace's registry; deployed_only keeps the pro-code agents with a
    # Live version — the only kind the gateway routes (Visual Builder agents are listed too).
    for a in client.list_agents(deployed_only=True):
        print(a.name, a.id, a.status)

    # By registry name or registry UUID (connect() is an alias of agent()):
    agent = client.agent("docs-qa")

    # Sync — blocks until the agent answers (its manifest budget):
    run = agent.invoke({"question": "What is Keystone?"})
    print(run.status, run.result)

    # Multi-turn — pass the previous run's session_id to continue the thread. session_id names
    # the CONVERSATION (same on every turn); run_id names this turn's own run (get_run key):
    follow_up = agent.invoke({"question": "And in Vietnamese?"}, session_id=run.session_id)
    assert follow_up.session_id == run.session_id and follow_up.run_id != run.run_id

    # Streaming (token-level):
    for event in agent.stream({"question": "..."}, stream_mode="messages"):
        if event["type"] == "token":
            print(event["content"], end="")

    # Background run + poll:
    handle = agent.invoke_async({"question": "long job..."})
    done = agent.get_run(handle.run_id, wait=True)   # returns on terminal OR awaiting_human
    print(done.status, done.trace_id)                # trace_id = observability link (FDP-3435)
```

## What the client owns for you

- **Kong path + tenant headers** (`Authorization`, `X-Workspace-ID`) and a
  fresh 32-hex `X-Correlation-ID` per call — the exact literal the platform stamps as the
  Langfuse `trace_id`, so `Run.trace_id` is a working trace link.
- **Cold-start (warming) retries**: a scaled-to-zero version wakes on first call; the
  gateway answers `202 warming` *without forwarding* — the client honours `Retry-After`
  and re-POSTs safely.
- **SSE parsing** for `stream()`; **polling** for `get_run(wait=True)` — which returns an
  `awaiting_human` run immediately (approvals can take days; your app decides what to do).
- **Typed errors** decoded from the platform envelope (`RT_*`/`GW_*`):
  `AgentNotFoundError`, `AgentConflictError` (e.g. cancel on a terminal run),
  `AgentAuthError`, base `AgentClientError` with `.code`.
- **Version pinning**: `invoke(..., version="v3")` sends `X-Agent-Version`.
- **Name or UUID**: the gateway routes on the agent **name**, so `agent(<uuid>)` looks the name
  up in agent-hub first (one GET). A not-found there falls back to using the string as the name —
  agent-hub's naming rule admits a UUID that starts with `a`-`f`. "Not-found" is a 404 **or** a
  403 whose `error.details.reason` is `resource_registry_miss` / `resource_inactive` (agent-hub's
  authz gate refuses an unknown or deleted UUID before it can 404); any other 403 still raises
  `AgentAuthError`, whose `.reason` carries that deny reason.

## Versioning and compatibility

[SemVer](https://semver.org), on the client's own `pyproject.toml` version — independent of
`keystone-cli`'s, even though both ship in one pipeline.

- **patch** — a fix; no public signature changes.
- **minor** — additive only: a new method, a new keyword-only argument, a new `AgentInfo` /
  `Run` field. Existing calls keep working.
- **major** — anything that breaks a caller: a removed or renamed method, a changed return shape,
  a new required argument. While on `0.x`, a breaking change bumps the minor instead.
- ⚠️ **Bump on every content change.** GAR is immutable and the pipeline tolerates a duplicate
  upload, so an unbumped change is **silently not published** — `-dev` keeps serving the old code
  under the same version.

The client speaks two public surfaces, both `v1`: agent-gateway `/api/agent-service/v1/agents/…`
(invoke, runs, threads) and agent-hub `/api/agent-hub/v1/agents` (discovery). What else a call
needs lives in the **agent's own image**, not in the environment: the runtime is baked in when
the agent is built, so an agent built on an older runtime base lacks the newer routes even on an
up-to-date environment.

| Client | Gateway / hub API | Needs the agent built on | For |
|---|---|---|---|
| `0.1.x` | `v1` | any agent-runtime | `invoke`, `invoke_async`, `stream`, `get_run`, `cancel`, `list_agents`, `agent`/`connect` |
| `0.1.x` | `v1` | agent-runtime `≥ v2026.09.03.1` | `threads`, `thread_state`, `thread_history`, `delete_thread`, `resume`, `multitask_strategy` |
| `≥ 0.1.3` | `v1` | agent-runtime `> v2026.09.27.1` (FDP-5539) | `Run.trace_id` / `Run.agent_uuid` on **sync** `invoke` (older agents: `None`; `invoke_async` / `get_run` always had them) |
| `≥ 0.1.4` | `v1` | agent-runtime `> v2026.09.27.1` (FDP-5538) | a distinct `Run.run_id` per turn on **sync** `invoke` (older agents: the conversation root, i.e. turn 1's id) |

An agent built before that runtime answers those calls `404` (`AgentNotFoundError`); rebuild it
to pick them up (see [api.md → io_schema](../../../docs/architecture/pro-code-agent/api.md) for
why a republish alone does not rebuild).

Not in scope (v1): HITL approve/reject (approver tooling lives in the `keystone` CLI and
the portal), authoring/deploying agents, async (`await`) client — the surface is small on
purpose; an async twin can be added when demand shows up.

## Example

`examples/consume_docs_qa.py` is a runnable end-to-end script. Export the four
`KEYSTONE_*` variables above (API base = the Kong edge; token = your login token or a
service token), then:

```bash
python examples/consume_docs_qa.py [agent-name]
```
