Skip to main content

art_adk 1.0.6 copy "art_adk: ^1.0.6" to clipboard
art_adk: ^1.0.6 copied to clipboard

Flutter SDK for ART realtime communication with WebSocket channels, AI Agents, AI Orchestrators, presence tracking, end-to-end encrypted messaging, and CRDT-backed shared objects.

pub.dev License: MIT Platform

Flutter ADK for ART — A Realtime Tech communication, a realtime communication with WebSocket channels, AI Agents, AI Orchestrators, presence tracking, end-to-end encrypted messaging and CRDT-backed shared objects.

Features #

  • WebSocket connection management — connect, pause, resume, and auto-reconnect with exponential backoff
  • Channel subscriptions — default, targeted, group, secure (encrypted), and shared-object (CRDT) channels
  • Push messages — send structured payloads with optional per-user targeting
  • Event listening — receive every message with listen() or bind to named events via emitter.on()
  • Presence tracking — observe users online on a channel in real time
  • End-to-end encryption — crypto applied transparently on secure channels
  • Interceptors — hook into the message pipeline to resolve or reject payloads
  • Shared objects (CRDT) — collaborative state with automatic conflict resolution
  • AI Agents — interact with ART Agent Builder using threads, runs, typed events, and Human-in-the-Loop (HITL)
  • AI Orchestrators — execute multi-agent workflows with thread-scoped conversations and workflow events
  • Storage — thread-scoped temporary media: upload, list, retrieve and delete files, and attach them to agent runs

Installation #

Add art_adk to your pubspec.yaml:

dependencies:
  art_adk: ^1.0.6

Fetch packages:

flutter pub get

Import the ADK where you need it:

import 'package:art_adk/art_adk.dart';

Configuration #

1. Create assets/adk-services.json #

Generate client credentials from the ART Live Dashboard and save them under assets/adk-services.json:

{
  "Client-ID": "xxxxxxxx",
  "Client-Secret": "xxxxxxxxxxxxxxxx",
  "Org-Title": "YOUR_ORG",
  "ProjectKey": "YOUR_PROJECT_KEY",
  "Environment": "YOUR_ENV"
}

Register the asset in your app's pubspec.yaml:

flutter:
  assets:
    - assets/adk-services.json

See Workspace and Project for how to obtain ProjectKey and Environment.

2. Load the credentials #

import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
import 'package:art_adk/art_adk.dart';

Future<CredentialStore> loadCredentials() async {
  final raw = await rootBundle.loadString('assets/adk-services.json');
  final json = jsonDecode(raw) as Map<String, dynamic>;
  return CredentialStore(
    environment:  json['Environment']   as String,
    projectKey:   json['ProjectKey']    as String,
    orgTitle:     json['Org-Title']     as String,
    clientID:     json['Client-ID']     as String,
    clientSecret: json['Client-Secret'] as String,
  );
}

3. Get a user passcode #

The art_adk package authenticates with ART using a short-lived passcode issued for a single user:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<String> fetchPasscode(CredentialStore creds) async {
  final response = await http.post(
    Uri.parse('https://dev.arealtimetech.com/ws/v1/connect/passcode'),
    headers: <String, String>{
      'Client-Id':     creds.clientID,
      'Client-Secret': creds.clientSecret,
      'X-Org':         creds.orgTitle,
      'Environment':   creds.environment,
      'ProjectKey':    creds.projectKey,
      'Content-Type':  'application/json',
    },
    body: jsonEncode(<String, dynamic>{
      'username':   'jane_doe',
      'first_name': 'Jane',
      'last_name':  'Doe',
    }),
  );

  final decoded = jsonDecode(response.body) as Map<String, dynamic>;
  final data    = decoded['data'] as Map<String, dynamic>;
  return data['passcode'] as String;
}

Quick start #

The package includes a complete Flutter example demonstrating every major ART ADK capability from a single application.

The example covers #

  • Connection management
  • Channel subscriptions
  • Presence tracking
  • Publish / Subscribe messaging
  • Secure messaging
  • CRDT shared objects
  • Message interceptors
  • AI Agent integration
  • AI Orchestrator workflows

The example application is organised into the following sections:

Connection
-----------
• Connect
• Subscribe
• Generate KeyPair (for secure channel)
• Disconnect

Realtime Data Communication
----
• Send data (Targeted/Secure channel)
• Receive data (Targeted/Secure channel)
• Add Interceptor

CRDT Operations
---------------
• Set Title
• Array Push
• Array Pop
• Live Object

Agent
-----
• Create Agent
• Start Thread
• Register Listeners
• Run Prompt
• Reset Agent

Orchestrator
------------
• Create Orchestrator
• Start Thread
• Register Listeners
• Run Workflow
• Reset Orchestrator

Before running the example, update the placeholder values in example/main.dart:

const String kServerUri = 'YOUR_WEBSOCKET_URI';
const String kPasscodeEndpoint = 'YOUR_PASSCODE_ENDPOINT';
const String kUsername = 'YOUR_USERNAME';
const String kChannel = 'YOUR_CHANNEL_NAME';
const String kAgentSlug = 'YOUR_AGENT_SLUG';
const String kOrchestratorSlug = 'YOUR_ORCHESTRATOR_SLUG';
import 'package:art_adk/art_adk.dart';

Future<void> main() async {
  final creds    = await loadCredentials();                 // from asset JSON
  final passcode = await fetchPasscode(creds);              // from your auth service
  final updated  = creds.copyWith(accessToken: passcode);

  final adk = Adk(
    adkConfig: AdkConfig(
      uri:            kServerUri,
      authToken:      passcode,
      getCredentials: () => updated,
    ),
  );

  adk.on('connection', (dynamic data) {
    if (data is ConnectionDetail) {
      debugPrint('connected → ${data.connectionId}');
    }
  });
  adk.on('close', (dynamic reason) => debugPrint('closed: $reason'));

  await adk.connect();

  final sub = await adk.subscribe(channel: 'room-42');
  sub.emitter.on('message', (dynamic data) {
    debugPrint('received: $data');
  });

  await sub.push(
    event: 'message',
    data:  <String, dynamic>{'text': 'Hello ART!'},
  );
}

Connecting #

final adk = Adk(
  adkConfig: AdkConfig(
    uri:            kServerUri, 
    authToken:      passcode,
    getCredentials: () => updatedCreds,
  ),
);

await adk.connect();

For short-lived workflows, wrap connect / disconnect in try / finally:

try {
await adk.connect();
// your logic here
} finally {
await adk.disconnect();
}

Read the current state at any time:

adk.getState(); // 'connected' | 'retrying' | 'paused' | 'stopped'

Token renewal #

The ADK renews the access token 30 seconds before it expires and when a reconnect forces it:

  • With a client ID and a client secret, it authenticates again with them (the client-credentials grant). Refresh tokens are not used.
  • Otherwise it exchanges accessToken and then renews with the refresh token.

Concurrent renewals share one request. When the token changes, the ADK renews the connection's authorisation with the server before the next push or subscribe.

If the server rejects the credentials (HTTP 401 or 403), the connection fails with code AUTHENTICATION_FAILED: the ADK emits authenticationFailed and connectionFailed, stops reconnecting, and connect() throws that ConnectionError. Update the credentials and call connect() again. Failed auth requests throw AuthHttpError, which carries the HTTP status; isTerminalAuthError(error) is true for rejected credentials. An auth request that gets no answer within AdkConfig.connectionTimeoutMs fails with an ARTError too.

Subscribing to a channel #

adk.subscribe returns a BaseSubscription. For default / secure / targeted / group channels it is a Subscription; for shared-object channels it is a LiveObjSubscription.

final BaseSubscription sub = await adk.subscribe(channel: 'room-42');

if (sub is LiveObjSubscription) {
  // Shared-object (CRDT) channel — use sub.state(), sub.query(), sub.flush().
}

Unsubscribe when you no longer need the channel:

await sub.unsubscribe();

Pushing messages #

await sub.push(
  event: 'message',
  data:  <String, dynamic>{'text': 'Hello'},
);

Target a specific user (required for secure and targeted channels — exactly one recipient):

await sub.push(
  event:   'message',
  data:    <String, dynamic>{'text': 'Hi Bob'},
  options: const PushConfig(to: <String>['bob']),
);

Attempting to target zero or more than one recipient on a secure / targeted channel throws ARTError.serverError.

On secure and targeted channels, push completes once the server acknowledges the message, and throws a ConnectionError with code ACK_TIMEOUT if no acknowledgement arrives within 50 seconds. Messages are never queued or resent: when there is no connection, push throws a ConnectionError (see Connection failures).

Receiving messages #

Bind to a specific event on the subscription's emitter:

sub.emitter.on('message', (dynamic data) {
  debugPrint('got: $data');
});

Or, on default channels, cast to Subscription and stream every event:

if (sub is Subscription) {
  sub.listen((Map<String, dynamic> data) {
    debugPrint("event=${data['event']} content=${data['content']}");
  });
}

Unbind a specific listener:

if (sub is Subscription) {
  sub.remove(event: 'message');
}

Presence #

Track who is online on a channel:

final cancelPresence = await sub.fetchPresence(
  callback: (List<String> users) {
    debugPrint('online: $users');
  },
);

// later:
await cancelPresence();

Presence tracking must be enabled on the channel in the ART dashboard.

Encrypted channels #

Secure channels apply end-to-end NaCl-box encryption per message. After connecting, generate (or set) a key pair — the ADK publishes the public half automatically:

await adk.generateKeyPair();          // creates + uploads a fresh KeyPair
// or
await adk.setKeyPair(existingPair);   // adopt an externally-managed KeyPair

Then push as usual. Encryption and decryption happen transparently:

final secure = await adk.subscribe(channel: 'YOUR_SECURE_CHANNEL');

await secure.push(
  event:   'message',
  data:    <String, dynamic>{'text': 'This is private'},
  options: const PushConfig(to: <String>['bob']), // secure requires exactly one recipient
);

secure.emitter.on('message', (dynamic data) {
  debugPrint('decrypted: $data');
});

Interceptors never see encrypted channel traffic — ART preserves message privacy end-to-end.

Shared object channels (CRDT) #

Shared-object channels expose a JSON-like document that every subscriber edits concurrently; conflicts are resolved automatically via the ADK's CRDT engine.

final sub = await adk.subscribe(channel: 'YOUR_CRDT_CHANNEL');

if (sub is LiveObjSubscription) {
  // 1. Write
  sub.state()['document']['title'].set('My Doc');
  await sub.flush();

  // 2. Read once
  final snapshot = await sub.query(path: 'document').execute();
  debugPrint('doc: $snapshot');

  // 3. Observe
  final dispose = await sub.query(path: 'document').listen((dynamic data) {
    debugPrint('document updated: $data');
  });

  // later:
  dispose();
}

Array operations #

final items = sub.state()['items']; // CRDTProxy

items.push('alpha');                // append
items.unshift('zero');              // prepend
items.pop();                        // remove last
items.removeAt(2);                  // remove by index
items.splice(start: 1, deleteCount: 2, insert: <dynamic>['x', 'y']);

await sub.flush();
debugPrint('length: ${items.length}');

Pending local writes are auto-batched and flushed every 50 ms; call flush() to force an immediate merge.

Interceptors #

Interceptors let you plug custom logic into the server-side message pipeline. Each intercepted message must be resolved (forwarded) or rejected.

await adk.intercept(
  interceptor: 'profanity-filter',
  fn: (
    Map<String, dynamic> payload,
    void Function(dynamic data) resolve,
    void Function(String error) reject,
  ) {
    final content = payload['data'];
    if (content is Map && '${content['text']}'.contains('anyword')) {
      reject('blocked by profanity filter');
      return;
    }
    resolve(payload);
  },
);

Encrypted channels are never routed through interceptors.

Connection lifecycle #

// Life-cycle listeners
adk.on('connection', (dynamic data) {
if (data is ConnectionDetail) {
debugPrint('connectionId=${data.connectionId}');
}
});
adk.on('close', (dynamic reason) => debugPrint('closed: $reason'));
adk.on('authenticationFailed', (dynamic data) {
  final detail = data as AuthenticationFailedDetail;
  debugPrint('credentials rejected (${detail.status}): ${detail.error}');
});
adk.on('limitExceeded', (dynamic data) {
  final detail = data as LimitExceededDetail;
  debugPrint('limit reached: ${detail.code}');
});
adk.on('connectionFailed', (dynamic data) {
  final error = data as ConnectionError;
  debugPrint('connection failed (${error.code}): ${error.message}');
});

// Manual controls
adk.pause();               // tear the socket down, keep subscriptions
await adk.resume();        // bring it back
await adk.disconnect();    // full teardown

close reports that the transport closed; the ADK then tries to reconnect, and if the connection does not come back it fails with connectionFailed. connectionFailed means the connection is gone until you call connect() again — see below.

Connection failures #

A ConnectionError is a failure of the client's connection, not of a server execution. It carries a readable message, a code, and the HTTP status when there was one. The connection fails, emitting connectionFailed, when:

Code Cause
AUTHENTICATION_FAILED Credentials are missing, the server rejects them, or a token renewal fails
CONNECTION_TIMEOUT The connection is not ready within AdkConfig.connectionTimeoutMs
CONNECTION_LOST A control message could not be sent, or 5 reconnect attempts failed
CONNECTION_PAUSED / CONNECTION_CLOSED pause(), closeWebSocket() or disconnect() was called
CONNECTION_MINUTES_LIMIT_EXCEEDED / CONCURRENT_LIMIT_EXCEEDED A billing or concurrency limit was reached (limitExceeded fires too)

After a failure:

  • Automatic reconnection stops. Waits, sends and subscribes throw the same ConnectionError until you call connect(), which clears it and starts a new attempt. Subscribes that failed can then be retried.
  • Active agent runs end: done() throws an AgentError with the same code, whose details have source: connection and execution_status: unknown. Listeners receive the same agent_error, and the thread state becomes failed with source client and message Connection unavailable. Orchestrator threads with a request in progress report the same. A disconnected client cannot tell whether the server finished the execution.

Some failures concern one request only and leave the connection up:

  • CONNECTION_TIMEOUT when a control request (such as a subscribe) gets no reply within AdkConfig.connectionTimeoutMs.
  • CONNECTION_LOST when a channel message has no connection to go out on.
  • ACK_TIMEOUT when the server does not acknowledge a message on a secure or targeted channel.
  • CONNECTION_AUTHORIZATION_FAILED when the connection's authorisation cannot be renewed.

Failed messages are never queued for replay. Check whether a request took effect before you send it again, so it does not run twice.

AdkConfig.connectionTimeoutMs (default 30 000 ms) bounds connection set-up, authentication requests and control replies — not how long an execution may run.

Dispose agent and orchestrator threads when their screens close, and call disconnect() when the Adk instance is no longer used; a later connect() attaches its listeners again.

Common codes: notConnected, authenticationFailed, serverError, encryptionError, decryptionError, timeout, channelNotFound, forbidden.

Agent Lab #

ART ADK provides two AI integrations:

  • Agent — interact with a single AI agent.
  • Orchestrator — execute multi-agent workflows coordinated by an orchestrator.

Agents and orchestrators are addressed by their slug, optionally pinned to a named build: my-agent or my-agent@release2. Other values throw an ArgumentError; validateExecutionReference checks one up front.

Agent #

final agent = adk.agent('your-agent-slug');

final thread = agent.thread();

await thread.listen((AgentEventEnvelope event) {
  debugPrint(event.event);
});

final run = await thread.run(
  'Plan a 3-day trip to Goa',
);

final output = await run.done();

debugPrint(output.message);

// Requests started from the ART playground are marked as such:
await thread.run(
  'Plan a 3-day trip to Goa',
  deps: const RunDeps(executionOrigin: ExecutionOrigin.playground),
);

// Stop a conversation: ends its active run and removes only this thread's
// listeners. The agent's channel stays open for other threads.
thread.dispose();

Orchestrator #

final orchestrator =
    adk.orchestrator('your-orchestrator-slug');

final thread = await orchestrator.thread();

thread.listen((Map<String, dynamic> data) {
  debugPrint(data.toString());
});

await thread.push(
  event: 'user_input',
  data: <String, dynamic>{
    'user_input': 'Plan a 3-day trip to Goa',
  },
);

Storage #

Uploaded files are temporary media that belong to a conversation thread. Each file is owned by a configuration (an agent, an orchestrator, or your project by default) and is read through a short-lived URL.

Give an agent a file #

final thread = adk.agent('your-agent-slug').thread();

final fileRef = await thread.upload(
  bytes,
  opts: const UploadOptions(
    filename: 'itinerary.pdf',
    contentType: 'application/pdf',
  ),
);

final run = await thread.run(
  'Summarise this itinerary',
  deps: RunDeps(
    fileMeta: <Map<String, dynamic>>[MediaRef.fromFileRef(fileRef).toJson()],
  ),
);

thread.upload stores the file for that thread, owned by the agent. OrchestratorThread.upload does the same for orchestrators, and thread.listFiles() lists a thread's files.

Upload a file #

final bytes = await File('path/to/file.jpg').readAsBytes();

final fileRef = await adk.upload(
  bytes,
  opts: UploadOptions(
    threadId: 'thread-123',        // required
    filename: 'my_image.jpg',
    contentType: 'image/jpeg',
    ttlSeconds: 3600,
    allowedMimeTypes: const <String>['image/jpeg', 'image/png'],
    onProgress: (double progress) {
      print('Upload progress: ${progress * 100}%');
    },
  ),
);

print('File ID: ${fileRef.fileId}');
print('Read URL: ${fileRef.readUrl}');

adk.upload makes the project the owner unless you pass configId. onProgress reports 0.0 when the upload starts and 1.0 once the server has stored the file.

List files #

final result = await adk.listFiles(
  opts: const ListOptions(
    threadId: 'thread-123',        // required
    limit: 10,
  ),
);

final List<StorageFile> files = result['files'];
for (var file in files) {
  print('File: ${file.name} (${file.size} bytes)');
}

Get a single file #

final file = await adk.getFile('file_id_123', threadId: 'thread-123');
print('Name: ${file.name}');
print('Read URL: ${file.readUrl}');

Delete a file #

await adk.deleteFile('file_id_123');

Storage calls throw UploadError; its step is validate, upload, list, get or delete.

API reference #

Class / type Purpose
Adk Top-level facade — connect, subscribe, intercept, key management
AdkConfig Connection configuration (URI, credentials, auth token, connectionTimeoutMs)
CredentialStore Immutable holder for org / project / client credentials
BaseSubscription Base type for all channel subscriptions (push, fetchPresence, unsubscribe)
Subscription Default / secure / targeted / group channel — listen(), bind(), emitter.on()
LiveObjSubscription CRDT-backed shared-object channel — state(), query(), flush()
CRDTProxy Chainable accessor for reading / mutating CRDT state
PushConfig Push options — to for targeted delivery, fileMeta, executionOrigin
UploadOptions / ListOptions Storage options — threadId is required
MediaRef A file_meta entry built from an upload's FileRef
AuthenticationFailedDetail Payload of 'authenticationFailed' — HTTP status and error
AuthHttpError Auth request rejected by the server — carries the HTTP status
ConnectionError Payload of 'connectionFailed' and error of failed waits and sends — code, message, optional status
KeyPairType KeyPairType for secure channels
ConnectionDetail Emitted on 'connection' — contains connectionId, instanceId, tenant info

Documentation #

Full documentation is available at docs.arealtimetech.com/docs/adk.

Topic Link
Overview ADK Overview
Installation Flutter Installation
Publish & Subscribe Pub/Sub Docs
Connection Management Connection Docs
User Presence Presence Docs
Encrypted Channels Encryption Docs
Shared Object Channels Shared Object Docs
Interceptors Interceptor Docs
Agents Agents Docs
Orchestrator Orchestrator Docs

License #

Released under the MIT License.

4
likes
160
points
181
downloads

Documentation

Documentation
API reference

Publisher

verified publisherarealtimetech.com

Weekly Downloads

Flutter SDK for ART realtime communication with WebSocket channels, AI Agents, AI Orchestrators, presence tracking, end-to-end encrypted messaging, and CRDT-backed shared objects.

Homepage

Topics

#websocket #realtime #messaging #pubsub #flutter

License

MIT (license)

Dependencies

flutter, http, http_parser, pinenacl, web_socket_channel

More

Packages that depend on art_adk