Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 61 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,43 @@

---

## What Is AXME?

AXME is a coordination infrastructure for durable execution of long-running intents across distributed systems.

It provides a model for executing **intents** — requests that may take minutes, hours, or longer to complete — across services, agents, and human participants.

## AXP — the Intent Protocol

At the core of AXME is **AXP (Intent Protocol)** — an open protocol that defines contracts and lifecycle rules for intent processing.

AXP can be implemented independently.
The open part of the platform includes:

- the protocol specification and schemas
- SDKs and CLI for integration
- conformance tests
- implementation and integration documentation

## AXME Cloud

**AXME Cloud** is the managed service that runs AXP in production together with **The Registry** (identity and routing).

It removes operational complexity by providing:

- reliable intent delivery and retries
- lifecycle management for long-running operations
- handling of timeouts, waits, reminders, and escalation
- observability of intent status and execution history

State and events can be accessed through:

- API and SDKs
- event streams and webhooks
- the cloud console

---

## What You Can Do With This SDK

- **Send intents** — create typed, durable actions with delivery guarantees
Expand All @@ -19,16 +56,26 @@

## Install

Add to your `pom.xml`:
Build and install from source (local Maven repository):

```bash
git clone https://github.com/AxmeAI/axme-sdk-java.git
cd axme-sdk-java
mvn -q -DskipTests install
```

Then add to your `pom.xml`:

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

Maven Central publication target: `ai.axme:axme`.

---

## Quickstart
Expand All @@ -42,9 +89,16 @@ import java.util.Map;
public class Quickstart {
public static void main(String[] args) throws Exception {
AxmeClient client = new AxmeClient(
new AxmeClientConfig("https://gateway.axme.ai", "YOUR_API_KEY")
new AxmeClientConfig(
"https://gateway.axme.ai",
"YOUR_PLATFORM_API_KEY", // sent as x-api-key
"OPTIONAL_USER_OR_SESSION_TOKEN" // sent as Authorization: Bearer
)
);

// Check connectivity / discover available capabilities
System.out.println(client.getCapabilities(RequestOptions.none()));

// Send an intent
Map<String, Object> intent = client.createIntent(
Map.of(
Expand All @@ -71,9 +125,9 @@ The SDK covers the full public API surface:

---

## Pagination, Filtering, and Sorting
## Pagination and Cursor Flows

List endpoints return paginated results. The SDK handles cursor-based pagination:
Cursor-based list endpoints are available for inbox change streams:

![Pagination, Filtering, and Sorting Patterns](docs/diagrams/03-pagination-filtering-sorting-patterns.svg)

Expand Down Expand Up @@ -155,12 +209,12 @@ Map<String, Object> sa = client.createServiceAccount(
// Issue a key
Map<String, Object> key = client.createServiceAccountKey(
(String) sa.get("id"),
Map.of("name", "ci-key"),
Map.of(),
new RequestOptions(null, null)
);

// List service accounts
client.listServiceAccounts("org_abc", null, RequestOptions.none());
client.listServiceAccounts("org_abc", "", RequestOptions.none());

// Revoke a key
client.revokeServiceAccountKey(
Expand Down
16 changes: 11 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@

<groupId>ai.axme</groupId>
<artifactId>axme</artifactId>
<version>0.1.0</version>
<name>Axme Java SDK</name>
<version>0.1.1</version>
<name>axme</name>
<description>Official Java SDK for Axme APIs and workflows.</description>
<url>https://github.com/AxmeAI/axme-sdk-java</url>

<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/license/mit</url>
<url>https://opensource.org/licenses/MIT</url>
<distribution>repo</distribution>
</license>
</licenses>
Expand Down Expand Up @@ -89,7 +89,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.7.0</version>
<version>3.10.0</version>
<configuration>
<source>11</source>
<encoding>UTF-8</encoding>
Expand All @@ -108,6 +108,12 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.7</version>
<configuration>
<gpgArguments>
<arg>--pinentry-mode</arg>
<arg>loopback</arg>
</gpgArguments>
</configuration>
<executions>
<execution>
<id>sign-artifacts</id>
Expand All @@ -128,7 +134,7 @@
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
<version>0.8.0</version>
<version>0.9.0</version>
<extensions>true</extensions>
<configuration>
<publishingServerId>central</publishingServerId>
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/dev/axme/sdk/AxmeClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
public final class AxmeClient {
private final String baseUrl;
private final String apiKey;
private final String actorToken;
private final HttpClient httpClient;
private final ObjectMapper objectMapper = new ObjectMapper();

Expand All @@ -25,6 +26,7 @@ public AxmeClient(AxmeClientConfig config) {
public AxmeClient(AxmeClientConfig config, HttpClient httpClient) {
this.baseUrl = config.getBaseUrl();
this.apiKey = config.getApiKey();
this.actorToken = config.getActorToken();
this.httpClient = httpClient;
}

Expand Down Expand Up @@ -648,8 +650,14 @@ private Map<String, Object> requestJson(
.method(method, payload == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(payload)))
.header("Accept", "application/json");

String resolvedAuthorization = isBlank(options.getAuthorization()) ? "Bearer " + apiKey : options.getAuthorization();
builder.header("Authorization", resolvedAuthorization);
builder.header("x-api-key", apiKey);
String resolvedAuthorization = options.getAuthorization();
if (isBlank(resolvedAuthorization) && !isBlank(actorToken)) {
resolvedAuthorization = "Bearer " + actorToken;
}
if (!isBlank(resolvedAuthorization)) {
builder.header("Authorization", resolvedAuthorization);
}

if (payload != null) {
builder.header("Content-Type", "application/json");
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/dev/axme/sdk/AxmeClientConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,33 @@
public final class AxmeClientConfig {
private final String baseUrl;
private final String apiKey;
private final String actorToken;

public AxmeClientConfig(String baseUrl, String apiKey) {
this(baseUrl, apiKey, null, null);
}

public AxmeClientConfig(String baseUrl, String apiKey, String actorToken) {
this(baseUrl, apiKey, actorToken, null);
}

public AxmeClientConfig(String baseUrl, String apiKey, String actorToken, String bearerToken) {
if (baseUrl == null || baseUrl.trim().isEmpty()) {
throw new IllegalArgumentException("baseUrl is required");
}
if (apiKey == null || apiKey.trim().isEmpty()) {
throw new IllegalArgumentException("apiKey is required");
}
String normalizedActorToken = actorToken == null ? null : actorToken.trim();
String normalizedBearerToken = bearerToken == null ? null : bearerToken.trim();
if (isNonBlank(normalizedActorToken)
&& isNonBlank(normalizedBearerToken)
&& !normalizedActorToken.equals(normalizedBearerToken)) {
throw new IllegalArgumentException("actorToken and bearerToken must match when both are provided");
}
this.baseUrl = trimTrailingSlash(baseUrl.trim());
this.apiKey = apiKey.trim();
this.actorToken = isNonBlank(normalizedActorToken) ? normalizedActorToken : normalizedBearerToken;
}

public String getBaseUrl() {
Expand All @@ -23,6 +40,14 @@ public String getApiKey() {
return apiKey;
}

public String getActorToken() {
return actorToken;
}

private static boolean isNonBlank(String value) {
return value != null && !value.isBlank();
}

private static String trimTrailingSlash(String value) {
if (value.endsWith("/")) {
return value.substring(0, value.length() - 1);
Expand Down
47 changes: 44 additions & 3 deletions src/test/java/dev/axme/sdk/AxmeClientTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.axme.sdk;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.fasterxml.jackson.core.type.TypeReference;
Expand Down Expand Up @@ -42,7 +43,7 @@ void registerNickSendsPayloadAndHeaders() throws Exception {
RecordedRequest request = server.takeRequest();
assertEquals("POST", request.getMethod());
assertEquals("/v1/users/register-nick", request.getPath());
assertEquals("Bearer token", request.getHeader("Authorization"));
assertEquals("token", request.getHeader("x-api-key"));
assertEquals("register-1", request.getHeader("Idempotency-Key"));

Map<String, Object> body =
Expand All @@ -51,6 +52,31 @@ void registerNickSendsPayloadAndHeaders() throws Exception {
assertTrue((Boolean) response.get("ok"));
}

@Test
void clientSendsConfiguredActorToken() throws Exception {
server.enqueue(new MockResponse().setResponseCode(200).setBody("{\"ok\":true,\"available\":true}"));

AxmeClient actorClient =
new AxmeClient(new AxmeClientConfig(server.url("/").toString(), "platform-token", "actor-token"));
actorClient.checkNick("@partner.user", RequestOptions.none());

RecordedRequest request = server.takeRequest();
assertEquals("platform-token", request.getHeader("x-api-key"));
assertEquals("Bearer actor-token", request.getHeader("Authorization"));
}

@Test
void configRejectsConflictingActorTokenAliases() {
assertThrows(
IllegalArgumentException.class,
() ->
new AxmeClientConfig(
"https://api.axme.test",
"platform-token",
"actor-a",
"actor-b"));
}

@Test
void checkNickSendsQueryParameter() throws Exception {
server.enqueue(
Expand Down Expand Up @@ -190,7 +216,13 @@ void intentLifecycleAndControlEndpointsAreReachable() throws Exception {
"it_123",
Map.of("controls_patch", Map.of("timeout_seconds", 120), "expected_policy_generation", 5),
RequestOptions.none());
assertEquals("/v1/intents/it_123/controls", server.takeRequest().getPath());
RecordedRequest controlsRequest = server.takeRequest();
assertEquals("/v1/intents/it_123/controls", controlsRequest.getPath());
Map<String, Object> controlsBody =
objectMapper.readValue(controlsRequest.getBody().readUtf8(), new TypeReference<Map<String, Object>>() {});
Map<String, Object> controlsPatch = (Map<String, Object>) controlsBody.get("controls_patch");
assertEquals(120, ((Number) controlsPatch.get("timeout_seconds")).intValue());
assertEquals(5, ((Number) controlsBody.get("expected_policy_generation")).intValue());

client.updateIntentPolicy(
"it_123",
Expand All @@ -200,7 +232,16 @@ void intentLifecycleAndControlEndpointsAreReachable() throws Exception {
"envelope_patch",
Map.of("max_retry_count", 10)),
new RequestOptions(null, null, "agent://creator", null, null));
assertEquals("/v1/intents/it_123/policy?owner_agent=agent%3A%2F%2Fcreator", server.takeRequest().getPath());
RecordedRequest policyRequest = server.takeRequest();
assertEquals("/v1/intents/it_123/policy?owner_agent=agent%3A%2F%2Fcreator", policyRequest.getPath());
Map<String, Object> policyBody =
objectMapper.readValue(policyRequest.getBody().readUtf8(), new TypeReference<Map<String, Object>>() {});
Map<String, Object> grantsPatch = (Map<String, Object>) policyBody.get("grants_patch");
Map<String, Object> delegateGrant = (Map<String, Object>) grantsPatch.get("delegate:agent://ops");
assertTrue(((java.util.List<?>) delegateGrant.get("allow")).contains("resume"));
assertTrue(((java.util.List<?>) delegateGrant.get("allow")).contains("update_controls"));
Map<String, Object> envelopePatch = (Map<String, Object>) policyBody.get("envelope_patch");
assertEquals(10, ((Number) envelopePatch.get("max_retry_count")).intValue());
}

@Test
Expand Down