Tuul SDK for Flutter

A Flutter and Dart SDK for consuming Tuul agent endpoints with a clean, developer-friendly API.

This package mirrors the Tuul TypeScript SDK surface for Dart and Flutter projects, including:

  • typed /sdk client access
  • streaming responses over SSE
  • local tool orchestration with automatic continuation
  • conversation and message helpers
  • widget config, embed, theme, and endpoint catalog accessors
  • a ChangeNotifier chat controller
  • a starter Flutter chat panel widget

Features

  • Simple prompt generation with generate() and prompt()
  • Streaming support with stream()
  • Client-side local tools that automatically continue the same Tuul run
  • Conversation helpers for listing, renaming, deleting, and auto-titling chats
  • Message helpers for reading conversation or run messages
  • Widget accessors for public widget configuration and embed data
  • Flutter-ready UI state management through TuulChatController
  • Starter widget via TuulAgentPanel

Installation

Add the package to your pubspec.yaml:

dependencies:
  tuul_sdk: ^0.1.0

Then run:

flutter pub get

Imports

import 'package:tuul_sdk/client.dart';
import 'package:tuul_sdk/models.dart';
import 'package:tuul_sdk/chat_controller.dart';
import 'package:tuul_sdk/widgets/tuul_agent_panel.dart';

If you export these through a barrel file, you can also use:

import 'package:tuul_sdk/tuul_sdk.dart';

Quick start

import 'package:tuul_sdk/client.dart';
import 'package:tuul_sdk/models.dart';

final client = TuulClient(
  const TuulClientConfig(
    agentId: 'agent-id',
    apiKey: 'sdk_key_here',
    defaultSessionId: 'web-session-123',
  ),
);

final response = await client.generate(
  const TuulPromptInput(
    input: 'Summarize the latest support backlog.',
  ),
);

print(response.text);

Streaming

await for (final event in client.stream(
  const TuulPromptInput(
    input: 'Write a release summary.',
  ),
)) {
  if (event.type == 'delta' && event.text != null) {
    print(event.text);
  }
}

Local tools

When the model emits a call for one of your registered local tools, the SDK executes it on the client, posts the tool result back to Tuul, and continues the same response in context.

final response = await client.generate(
  TuulPromptInput(
    input: 'Use the local calculator tool to add 4 and 9.',
    localTools: [
      TuulLocalTool(
        name: 'calculator_add',
        description: 'Adds two numbers locally on the client.',
        inputSchema: {
          'type': 'object',
          'properties': {
            'a': {'type': 'number'},
            'b': {'type': 'number'},
          },
          'required': ['a', 'b'],
        },
        execute: (input, context) async {
          final args = Map<String, dynamic>.from(input as Map);
          final a = (args['a'] as num).toDouble();
          final b = (args['b'] as num).toDouble();
          return {'sum': a + b};
        },
      ),
    ],
  ),
);

print(response.text);

Using prompt()

prompt() gives you a single entry point for both blocking and streaming flows.

Non-streaming

final result = await client.prompt(
  const TuulPromptInput(
    input: 'Explain the support trend for this week.',
    stream: false,
  ),
);

print(result.text);

Streaming

final result = await client.prompt(
  const TuulPromptInput(
    input: 'Draft a release summary.',
    stream: true,
  ),
);

print(result.text);
print(result.metadata.finishReason);

SDK manifest

Load the remote SDK manifest:

final manifest = await client.manifest();
print(manifest.packageName);
print(manifest.endpoints);

Or without constructing a client first:

final manifest = await TuulClient.getManifest();

Public widget endpoints

The same client can load widget-related public resources.

Widget config

final config = await client.getWidgetConfig();
print(config.agentId);
print(config.theme.launcherLabel);

Widget theme

final theme = await client.getWidgetTheme();
print(theme.primaryAccentToken);

Widget embed

final embed = await client.getWidgetEmbed();
print(embed.snippet);

Endpoint catalog

final endpoints = await client.getEndpointCatalog();
for (final endpoint in endpoints) {
  print('${endpoint.method} ${endpoint.path}');
}

Conversation helpers

List conversations

final conversations = await client.listConversations();

Rename a conversation

await client.renameConversation('conversation-id', 'Quarterly planning');

Delete a conversation

await client.deleteConversation('conversation-id');

Generate a conversation title

await client.generateConversationTitle('conversation-id');

Message helpers

Get messages in a conversation

final messages = await client.getMessages('conversation-id');

Get messages for a run

final messages = await client.getRunMessages('run-id');

Resources

final resources = await client.getResources();

Flutter chat controller

TuulChatController wraps the SDK in a Flutter-friendly state layer using ChangeNotifier.

final controller = TuulChatController(
  client: client,
  autoLoadConversations: true,
  autoLoadResources: false,
);

Initialize

await controller.initialize();

Send a message

await controller.send('Hello from Flutter');

Load a conversation

await controller.loadMessages('conversation-id');

Starter widget

TuulAgentPanel provides a ready-made Flutter chat panel for quick integration.

import 'package:flutter/material.dart';
import 'package:tuul_sdk/client.dart';
import 'package:tuul_sdk/models.dart';
import 'package:tuul_sdk/chat_controller.dart';
import 'package:tuul_sdk/widgets/tuul_agent_panel.dart';

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  late final TuulChatController controller;

  @override
  void initState() {
    super.initState();
    final client = TuulClient(
      const TuulClientConfig(
        agentId: 'agent-id',
        apiKey: 'sdk_key_here',
        defaultSessionId: 'flutter-session-1',
      ),
    );
    controller = TuulChatController(client: client);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF030712),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: TuulAgentPanel(controller: controller),
      ),
    );
  }
}

Authentication behavior

The client supports two auth modes:

  • SDK auth using apiKey
  • Widget auth using widgetKey

For SDK-protected endpoints, the client automatically sends:

  • x-api-key
  • Authorization: Bearer <apiKey>

For widget endpoints, the client sends x-widget-key when configured.

Error handling

The SDK throws TuulSdkError when a request fails.

try {
  final response = await client.generate(
    const TuulPromptInput(input: 'Hello'),
  );
  print(response.text);
} on TuulSdkError catch (error) {
  print(error.message);
  print(error.statusCode);
}

Dependencies

This SDK currently relies on:

dependencies:
  flutter:
    sdk: flutter
  http: ^1.0.0

Notes

  • sessionId is required for listing conversations and loading resources.
  • apiKey is required for SDK-authenticated endpoints.
  • widgetKey is optional unless your widget endpoints require it.
  • SSE parsing is handled internally by the client.
  • Local tool execution works in both blocking and streaming modes.

License

This project is licensed under the MIT License. See LICENSE for details.

Libraries

tuul_sdk_flutter