agentic_core 0.1.0 copy "agentic_core: ^0.1.0" to clipboard
agentic_core: ^0.1.0 copied to clipboard

Foundation layer of the agentic framework for Dart and Flutter: immutable message and content models, JSON Schema, a structured error hierarchy, cancellation, retry and circuit-breaker policies, a typ [...]

example/agentic_core_example.dart

// Demonstrates the pieces of `agentic_core` an application actually touches:
// a run context, structured logging and tracing, the event bus, schema
// validation, and a retry that survives a flaky dependency.
//
// Run it with:
//
//     dart run example/agentic_core_example.dart
//
// Nothing here reaches the network. The "provider" is a function that fails
// twice and then succeeds, which is enough to show the machinery working.
import 'dart:async';

import 'package:agentic_core/agentic_core.dart';

/// Published when a step of the run finishes.
final class StepCompleted extends AgenticEvent {
  const StepCompleted({
    required super.id,
    required super.timestamp,
    required this.step,
    required this.detail,
    super.runId,
  });

  final String step;
  final String detail;

  @override
  String get type => 'example.step.completed';

  @override
  JsonMap payload() => <String, Object?>{'step': step, 'detail': detail};
}

Future<void> main() async {
  // ---------------------------------------------------------------------------
  // 1. A run context carries identity, logging, events, tracing, time and
  //    cancellation. Everything below receives it rather than reaching for a
  //    global, which is what makes all of it testable.
  // ---------------------------------------------------------------------------
  final events = BroadcastEventBus();
  final spans = InMemorySpanExporter();

  final context = AgenticContext.root(
    logger: StructuredLogger(level: LogLevel.debug),
    events: events,
    tracer: Tracer(exporter: spans),
    timeout: const Duration(seconds: 30),
    metadata: <String, Object?>{'example': 'agentic_core'},
  );

  context.logger.info(
    'Run started',
    fields: <String, Object?>{
      'runId': context.runId,
      // Credential-shaped fields are redacted before they reach any sink.
      'apiKey': 'sk-proj-not-a-real-key-1234',
    },
  );

  // A UI would subscribe here. Note that the bus replays recent events, so a
  // subscriber that arrives a frame late still sees everything.
  final subscription = events.on<StepCompleted>().listen(
    (event) => print('  event: ${event.step} -> ${event.detail}'),
  );

  // ---------------------------------------------------------------------------
  // 2. Schemas are the contract for tool arguments and structured output. They
  //    validate, and they repair the near-misses language models really emit.
  // ---------------------------------------------------------------------------
  final schema = JsonSchema.object(
    description: 'Search the web',
    properties: <String, JsonSchema>{
      'query': JsonSchema.string(
        description: 'What to search for',
        minLength: 1,
      ),
      'limit': JsonSchema.integer(minimum: 1, maximum: 50, defaultValue: 10),
    },
    required: <String>{'query'},
  );

  // A model sent the limit as a string and omitted nothing else. Coercion fixes
  // the unambiguous mistake instead of spending a round trip on it.
  final repaired = schema.coerce(<String, Object?>{
    'query': 'dart 3 records',
    'limit': '5',
  });
  print('coerced arguments : $repaired');
  print('valid             : ${schema.validate(repaired).isValid}');

  // A genuinely wrong argument reports every problem at once, phrased so the
  // text can be handed straight back to a model as a repair instruction.
  final invalid = schema.validate(<String, Object?>{'limit': 500});
  for (final violation in invalid.violations) {
    print('violation         : $violation');
  }

  context.publish(
    StepCompleted(
      id: context.ids.prefixed('evt'),
      timestamp: context.clock.now(),
      step: 'validate',
      detail: '${invalid.violations.length} violation(s)',
      runId: context.runId,
    ),
  );

  // ---------------------------------------------------------------------------
  // 3. A step opens a span and derives a scoped context. The retry policy is
  //    driven by the error's own `isRetryable`, never by parsing messages.
  // ---------------------------------------------------------------------------
  var attempts = 0;

  final answer = await context.step('provider.generate', (scope, span) async {
    span.setAttribute('provider', 'flaky-example');

    return const RetryPolicy(
      maxAttempts: 4,
      backoff: ExponentialBackoff(initial: Duration(milliseconds: 50)),
    ).execute(
      (attempt) async {
        attempts = attempt;
        scope.logger.debug('Calling provider', fields: {'attempt': attempt});

        if (attempt < 3) {
          // A 503 is transient, so the policy will back off and try again.
          throw ProviderException(
            'upstream temporarily unavailable',
            provider: 'flaky-example',
            statusCode: 503,
          );
        }
        return 'Records are a Dart 3 feature for grouping values.';
      },
      operation: 'provider.generate',
      cancellation: scope.cancellation,
      clock: scope.clock,
    );
  });

  print('answer            : $answer');
  print('attempts          : $attempts');

  // ---------------------------------------------------------------------------
  // 4. Messages are immutable and multimodal, and history knows things worth
  //    knowing — such as which tool calls have not been answered yet.
  // ---------------------------------------------------------------------------
  final history = <Message>[
    Message.system('You are a concise assistant.'),
    Message.user('What are Dart records?'),
    Message.assistant(
      answer,
      toolCalls: <ToolCallPart>[
        ToolCallPart(
          id: 'call_1',
          name: 'search_web',
          arguments: repaired! as JsonMap,
        ),
      ],
    ),
  ];

  print('history text      : ${history.last.text}');
  print('pending tool calls: ${history.pendingToolCalls.map((c) => c.name)}');

  // Usage is additive across every call in a run, whatever the provider.
  final usage = <TokenUsage>[
    const TokenUsage(
      promptTokens: 820,
      completionTokens: 90,
      cachedPromptTokens: 640,
    ),
    const TokenUsage(promptTokens: 120, completionTokens: 40),
  ].sum();
  print('tokens            : $usage');
  print(
    'cache hit rate    : ${(usage.cacheHitRate * 100).toStringAsFixed(1)}%',
  );

  // ---------------------------------------------------------------------------
  // 5. The trace is the record of what happened, and it survives the run.
  // ---------------------------------------------------------------------------
  for (final span in spans.spans) {
    print(
      'span              : ${span.name} '
      '(${span.duration.inMilliseconds}ms, ${span.status.name})',
    );
  }

  await subscription.cancel();
  await events.dispose();
  await spans.dispose();
}
2
likes
150
points
200
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

Foundation layer of the agentic framework for Dart and Flutter: immutable message and content models, JSON Schema, a structured error hierarchy, cancellation, retry and circuit-breaker policies, a typed event bus, and structured logging and tracing. Pure Dart, no Flutter dependency.

Repository (GitHub)
View/report issues
Contributing

Topics

#ai #agents #llm #agentic #workflow

License

MIT (license)

Dependencies

collection, meta

More

Packages that depend on agentic_core