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
/sdkclient access - streaming responses over SSE
- local tool orchestration with automatic continuation
- conversation and message helpers
- widget config, embed, theme, and endpoint catalog accessors
- a
ChangeNotifierchat controller - a starter Flutter chat panel widget
Features
- Simple prompt generation with
generate()andprompt() - 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-keyAuthorization: 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
sessionIdis required for listing conversations and loading resources.apiKeyis required for SDK-authenticated endpoints.widgetKeyis 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.