Docs Getting started
Quickstart: a minimal turn
Nothing below needs an API key, a database or a network. It uses the sample
travel domain, the in-memory stores and a scripted model from the
testing and tasks modules, which is what the
test-kit feature is for. Swap the doubles for a real workflow, a real store and a
real adapter and the rest of the code is unchanged.
use std::sync::Arc; use serde_json::json;use turnframe::effort::Effort;use turnframe::flow::{CaseKey, WorkflowRegistry};use turnframe::ids::{AccountId, CaseId, CaseRevision, ConversationId, TurnId};use turnframe::locale::Locale;use turnframe::provider::provider::ModelProvider;use turnframe::provider::router::ProviderPool;use turnframe::runtime::config::{NarrationConfig, OrchestratorConfig};use turnframe::runtime::orchestrator::{CaseCandidate, Orchestrator, StaticCaseDirectory};use turnframe::store::conversation::ConversationRecord;use turnframe::store::stores::Stores;use turnframe::tasks::testing::ScriptedTasks;use turnframe::testing::workflows::InMemoryExecutor;use turnframe::testing::workflows::trip::{TripWorkflow, incomplete_case};use turnframe::turn::{ActorContext, TurnInput}; let account = AccountId::from("aurora");let conversation = ConversationId::new(); // 1. A domain. The projector and the executor are the two things an adopter// writes; here they come ready-made from the test kit.let trips = Arc::new(InMemoryExecutor::new(TripWorkflow::default()));trips.seed(&account, &CaseId::from("trip-1"), incomplete_case(), CaseRevision(3));let workflows = Arc::new( WorkflowRegistry::builder() .register(TripWorkflow::default(), Arc::clone(&trips)) .build()?,); // 2. A model. Understanding is a few small tasks, each answered by id: split the// message into requests, check none was missed, route the request to an// operation, point at the value in the user's words, verify it.let text = "Set the name of Trip 1 to Lisbon";let tasks = Arc::new( ScriptedTasks::new("scripted", "small") .answer("turn/segment", json!({ "analysis": "One request.", "units": [{"kind": "request", "words": {"from": 1, "to": 8}, "workflow": "trip"}] })) .answer("turn/coverage", json!({"missed": []})) .answer("u1/route", json!({"operations": ["trip.set_name"]})) .answer("u1/extract", json!({"arguments": { "value": {"kind": "words", "text": "Lisbon", "message": "current", "from": 8, "to": 8} }})) .answer("u1/verify", json!({ "reason": "The user said so.", "arguments": {"value": "stated"}, "overall": "confirmed" })),);let providers = Arc::new( ProviderPool::builder() .provider(Arc::clone(&tasks) as Arc<dyn ModelProvider>) .build()?,); // 3. Persistence, and the records this user may address. The model never sees a// record identifier: it sees the label, and the runtime issues an opaque token.let stores = Stores::in_memory();stores .conversations() .create_conversation(ConversationRecord::new(conversation, account.clone(), chrono::Utc::now())) .await?;let directory = StaticCaseDirectory::new() .with_case(CaseCandidate::new(CaseKey::new("trip", "trip-1"), "Trip 1")); // Receipts, notices and cards only: nothing here writes prose.let mut config = OrchestratorConfig::conservative();config.narration = NarrationConfig::conservative().with_enabled(false);let orchestrator = Orchestrator::builder() .workflows(workflows) .providers(providers) .stores(stores) .case_directory(Arc::new(directory)) .config(config) .build()?; // 4. One turn.let answer = orchestrator .handle_turn(TurnInput { turn_id: TurnId::new(), conversation_id: conversation, actor: ActorContext::new(account.clone(), "u1"), text: Some(text.to_owned()), interaction_response: None, attachments: Vec::new(), origin: None, locale: Locale::from("en-GB"), // One reading per task, since the script answers each once; higher levels vote. effort: Some(Effort::Low), }) .await?; // The field really changed, under a new revision.assert_eq!(trips.revision_of(&account, &CaseId::from("trip-1")), CaseRevision(4)); // And the reply says so only because a committed event backs it.let receipts: Vec<&str> = answer.receipts().map(|r| r.status_code.as_str()).collect();assert_eq!(receipts, ["trip.name_set"]);assert!(answer.receipts().all(|receipt| receipt.is_event_backed()));assert!(tasks.unanswered().is_empty());Read that in the order the pipeline runs it. The projector turned the stored
trip into a view. Understanding split the message into one request, routed it
to trip.set_name, and pointed at the user's own words for the value, which
code sliced out of the message. The reducer resolved the one trip in view to a
case and compiled a typed command with an expected revision and an idempotency
key. The executor committed it. The receipt was rendered from the committed event,
not from a model's prose.