# Java SDK

Record agent actions, push facts from systems of record, read verdicts and draft contracts. The runtime has
zero dependencies (JDK HTTP client, its own JSON codec); Java 17+.

```xml
<dependency>
  <groupId>ai.commitlayer</groupId>
  <artifactId>commitlayer</artifactId>
  <version>0.1.0</version>
</dependency>
```

```kotlin
implementation("ai.commitlayer:commitlayer:0.1.0")   // Gradle
```

```bash
cd sdk/java && mvn install        # from this repository, into the local Maven repository
export COMMITLAYER_API_KEY=spk_...          # the agent runtime's key: a *recorder* key (records actions, nothing else)
export COMMITLAYER_EVIDENCE_KEY=spk_...     # the pipeline's key: an *evidence* key bound to the sources it pushes for
# COMMITLAYER_API_URL defaults to https://api.commitlayer.ai. One credential per side of the line: the key that
# records an action can never also supply the facts that verify it.
```

Every method returns the API's JSON answer as a `Map<String,Object>` (nested maps, lists, strings, `Long`,
`Double`, booleans) rather than typed classes: the response shapes evolve with the API (see `docs/API.md`), and a
map keeps an older SDK compatible with new fields.

## Record an action where it happens

```java
import ai.commitlayer.ActionOptions;
import ai.commitlayer.ActionRecorder;
import ai.commitlayer.Client;

Client cl = Client.builder().agentId("support-bot").build();   // key and url from the environment

Map<String, Object> refund(String orderId, double amount) {
    ActionRecorder rec = cl.action("refund_workflow", orderId, new ActionOptions()
            .params(Map.of("amount", amount))
            .subjectKind("order")
            .actionId("refund:" + orderId));   // retries update one claim instead of recording two
    return rec.run(() -> payments.refund(orderId, amount));   // the agent's own work, unchanged
}
```

The action is recorded **before** the function runs, so its request time precedes every effect. The function's
return value is never changed and never consulted: verification reads the systems of record. If the API cannot be
reached the function still runs and a warning is logged through `System.Logger` (`failOpen(true)`, the
default); call `rec.failOpen(false)` to rethrow instead and skip the function. `rec.lastRecorded()` holds the
API's answer to the latest recording (`action`, `verification`, `allowance`), and `rec.onRecorded(...)` receives
it as it arrives:

```java
rec.onRecorded(r -> {
    Map<String, Object> verification = (Map<String, Object>) r.get("verification");
    Map<String, Object> summary = (Map<String, Object>) verification.get("summary");
    System.out.println(summary.get("headline"));
});
```

`cl.recordAction(contractId, subjectId, opts)` records without wrapping anything. An action that corrects an
earlier one names it with `.compensates("refund:order_10231")`.

## Push facts, with a watermark

Use an **evidence** key here, bound to the sources it pushes for (Settings → API keys). The key your agent runtime records actions with is a **recorder** key and cannot push facts: the credential that records an action never also supplies the facts that verify it.

```java
import ai.commitlayer.FactsOptions;

Client evidence = Client.builder().apiKey(System.getenv("COMMITLAYER_EVIDENCE_KEY")).build();  // the pipeline's own client
evidence.pushFacts(
        List.of(Map.of(
                "source", "crm",
                "subject_id", "order_10231",
                "fact_type", "note_added",
                "occurred_at", "2026-10-13T10:42:09Z")),
        new FactsOptions().watermarks(Map.of("crm", "2026-10-13T10:45:00Z")));  // everything up to here has been sent for crm
```

A verdict that is not yet a PASS settles only when its window has closed **and** every source it reads has a
watermark past the end of the window. Declare one when your feed lags; a source that never declares one is taken
as current. `.links(...)` on the same options records that two record ids are the same case.

## Draft a contract from what was recorded

```java
import ai.commitlayer.DraftOptions;

Map<String, Object> d = cl.draftContract("refund_order", new DraftOptions().lookbackDays(30));
String yaml = (String) d.get("yaml");       // expected effects, window, preconditions, limits, from the sample
System.out.println(((Map<?, ?>) d.get("stats")).get("effects"));
Map<String, Object> v = cl.validateContract(yaml);   // errors, and weakening warnings against the version it would replace
cl.saveContract(yaml);                                // the response carries the same warnings
```

Every value in a draft is what the sample showed, not what the policy says. Review it before saving.

## Read a verdict

```java
Map<String, Object> v = cl.getAction("refund:order_10231");
v.get("status");                                          // pending | settled
((Map<?, ?>) v.get("settlement")).get("waiting_on");      // sources still behind the window, if any
v.get("reversal");                                        // the later record that undid the action, if any
v.get("sources");                                         // trust level and watermark per source the contract read
```

`cl.verify(new VerifyOptions().actionId("refund:order_10231"))` verifies now; with `.action(...)`, `.facts(...)`
and `.dryRun(true)` an action and its facts are evaluated inline and nothing is stored.

## Errors

An answer with status 400 or above throws `CommitLayerException` with `status()`, `code()`, `message()` and
`details()` taken from the API's `{error: {code, message, details}}` body (for instance `entitlement_limit` with
`details.upgrade_to`). Network failures throw `java.io.UncheckedIOException`.

## Tests

The tests run offline: they inject a `Transport` (a functional interface, `Response send(method, path, body)`)
and, for the default HTTP transport, a loopback `HttpServer`.

```bash
cd sdk/java && mvn test
```
