-
Notifications
You must be signed in to change notification settings - Fork 10
feat(nodes): approval node kind + ApprovalProvider capability #52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+3,448
−524
Merged
Changes from all commits
Commits
Show all changes
139 commits
Select commit
Hold shift + click to select a range
ed9aa54
chore: files changed src/caps/approval.rs
senamakel a895d94
chore: files changed src/caps/mod.rs
senamakel 5e15218
chore: files changed src/caps/mod.rs
senamakel 5f08086
chore: files changed src/caps/mock.rs
senamakel 37ef0df
chore: files changed src/caps/mock.rs
senamakel fc97a0d
chore: files changed src/model/node_kind.rs,src/nodes/execution.rs,sr…
senamakel 72a8a18
chore: files changed src/nodes/integration/approval.rs
senamakel 2d4d4e9
chore: files changed src/nodes/integration/mod.rs
senamakel f84a6a0
chore: files changed src/caps/mod.rs
senamakel f00b31d
chore: files changed src/caps/mod.rs,src/main.rs
senamakel 6205547
chore: files changed src/nodes/integration/approval_tests.rs
senamakel 2af344e
chore: files changed src/catalog.rs,src/catalog/contracts/group_03.rs…
senamakel a9f3a71
chore: files changed src/validate.rs
senamakel e794410
chore: files changed src/validate_tests/validate_tests_part_03_tests.rs
senamakel 523a930
chore: files changed src/visualization.rs,tests/smoke_all_nodes.rs
senamakel fdfbc4c
chore: files changed src/caps/mock.rs,src/nodes/integration/approval.…
senamakel 3fd5c3d
chore: files changed CHANGELOG.md,wiki/Capability-Traits.md,wiki/Node…
senamakel 1f4a0ae
chore: files changed examples/hitl_review.rs
senamakel 28af480
chore: files changed examples/hitl_review.rs
senamakel 27d1470
chore: files changed CHANGELOG.md,examples/hitl_review.rs
senamakel 3b7b2f5
feat(nodes): add an approval node kind and the ApprovalProvider capab…
senamakel cfec2ea
Merge remote-tracking branch 'upstream/main' into hitl-approval
senamakel d9e820b
chore: files changed tests/fuzz_interception.proptest-regressions
senamakel 776f19c
chore: files changed src/validate.rs
senamakel bc345c5
chore: files changed src/nodes/integration/approval.rs
senamakel 2af7dbd
chore: files changed src/nodes/integration/approval.rs
senamakel 0f1f733
chore: files changed src/nodes/integration/approval.rs
senamakel 0be092d
chore: files changed src/nodes/integration/approval.rs
senamakel 7e0587d
chore: files changed examples/hitl_review.rs
senamakel dbba788
chore: files changed README.md
senamakel 35546de
chore: files changed wiki/Capability-Traits.md
senamakel 1530b69
chore: files changed wiki/Node-Catalog.md
senamakel 0c00033
chore: files changed src/validate_tests/validate_tests_part_03_tests.rs
senamakel ddb5381
chore: files changed src/caps/mock_approvals.rs
senamakel de1948b
chore: files changed src/caps/mock.rs
senamakel df2ef83
chore: files changed src/caps/mock.rs
senamakel 74af005
chore: files changed src/caps/mock.rs
senamakel 0ca98c2
chore: files changed src/caps/mock.rs
senamakel 7541236
chore: files changed src/nodes/integration/approval.rs
senamakel 282ff5c
chore: files changed src/nodes/integration/approval_tests.rs
senamakel b059f0d
chore: files changed src/nodes/integration/approval_tests.rs
senamakel 29f935c
chore: files changed src/nodes/integration/approval_tests.rs
senamakel 6165a8f
chore: files changed src/nodes/integration/approval_tests.rs
senamakel a4f5bc2
chore: files changed tests/smoke_all_nodes.rs
senamakel c69f66f
chore: files changed examples/hitl_review.rs
senamakel ffb86af
chore: files changed src/catalog/contracts/group_03.rs
senamakel cc7ec6e
chore: files changed src/catalog/contracts/group_03.rs
senamakel 01f262a
chore: files changed src/caps/mock.rs,src/nodes/integration/approval_…
senamakel 351640b
chore: files changed src/testkit/mocks_double.rs
senamakel 2075f30
chore: files changed src/testkit/mocks_double.rs
senamakel 50a0755
chore: files changed src/testkit/mocks.rs
senamakel 1b0563d
chore: files changed src/testkit/mocks.rs
senamakel 2ef13a6
chore: files changed src/testkit/mocks.rs
senamakel 1846cd6
chore: files changed src/testkit/mocks.rs
senamakel d6c0c47
chore: files changed src/testkit/mocks.rs
senamakel 19557f6
chore: files changed src/testkit/mocks.rs
senamakel bf9eea7
chore: files changed src/testkit/mocks_log.rs
senamakel bcb5e9a
chore: files changed src/testkit/mocks_double.rs
senamakel 1429a17
chore: files changed src/testkit/mocks.rs
senamakel 153bb66
chore: files changed src/testkit/mocks_log.rs
senamakel b55800a
chore: files changed src/testkit/mocks_log.rs
senamakel debba44
chore: files changed src/testkit/mocks.rs
senamakel bc8376d
chore: files changed src/testkit/mocks_tests.rs
senamakel 2fec804
chore: files changed src/testkit/mocks_tests.rs
senamakel 62a5ef2
chore: files changed src/caps/mock_builders.rs
senamakel 1f93e21
chore: files changed src/caps/mock.rs
senamakel fbbdef4
chore: files changed src/caps/mock.rs
senamakel 5e669d1
chore: files changed src/caps/mock_builders.rs
senamakel 311555e
chore: files changed tests/zz_repro.rs
senamakel 33f30f4
chore: files changed tests/zz_repro.rs
senamakel a9e53a0
chore: files changed tests/zz_repro.rs
senamakel 79ec050
chore: files changed src/nodes/integration/gate.rs
senamakel 545140f
chore: files changed tests/zz_repro.rs
senamakel 23064a3
chore: files changed src/nodes/integration/gate.rs
senamakel ea01a35
chore: files changed src/caps/tasks.rs
senamakel 476b20c
chore: files changed src/engine/build/outcome.rs
senamakel 4251ad6
chore: files changed src/engine/build/outcome.rs
senamakel 1783d30
chore: files changed src/caps/tasks.rs,src/engine/build/outcome.rs,sr…
senamakel c10de5f
chore: files changed src/caps/tasks.rs,src/engine/build/outcome.rs,sr…
senamakel 779d977
chore: files changed src/engine/build/outcome.rs
senamakel 0a5627f
chore: files changed src/engine/build/backoff.rs
senamakel 997469e
chore: files changed src/engine/build/outcome.rs
senamakel 32d5897
chore: files changed src/engine/build/activation.rs
senamakel 5bb65cf
chore: files changed src/engine/build.rs
senamakel 3445891
chore: files changed src/engine/build/backoff_tests.rs
senamakel 4bb2ad0
chore: files changed src/nodes/integration/gate.rs
senamakel a266bb6
chore: files changed src/caps/tasks.rs
senamakel 0e169f1
chore: files changed src/lib.rs
senamakel 792dd62
chore: files changed tests/fuzz_resume.rs
senamakel 6929e81
chore: files changed src/engine/build/backoff.rs
senamakel 38c428f
chore: files changed src/engine/build/backoff.rs
senamakel d11ea3d
chore: files changed src/engine/build/backoff_tests.rs
senamakel e3dbdf2
chore: files changed src/engine/build/backoff_tests.rs
senamakel 5e73697
chore: files changed tests/zz_repro.rs
senamakel f2c2fd7
chore: files changed src/nodes/integration/approval_request.rs
senamakel 138c757
chore: files changed src/nodes/integration/approval.rs
senamakel 9c8bb31
chore: files changed src/nodes/integration/approval.rs
senamakel 5d0d218
chore: files changed src/nodes/integration/approval.rs
senamakel 7b16473
chore: files changed src/nodes/integration/approval.rs
senamakel b789cfb
chore: files changed src/nodes/integration/approval_tests.rs
senamakel 6bcbf7b
chore: files changed src/nodes/integration/approval_tests.rs
senamakel b5b8eb8
chore: files changed src/nodes/integration/approval_tests.rs,src/node…
senamakel 8d5dec2
chore: files changed src/nodes/integration/approval_request.rs,src/no…
senamakel b61e8dd
chore: files changed src/validate.rs
senamakel 784d046
chore: files changed src/validate_tests/validate_tests_part_03_tests.rs
senamakel af2b10b
chore: files changed src/nodes/integration/approval_tests/approval_te…
senamakel 772d3df
chore: files changed src/validate.rs
senamakel f67a794
chore: files changed src/nodes/integration/approval_request.rs
senamakel 2498212
chore: files changed src/nodes/integration/approval_request.rs
senamakel 7c1acb7
chore: files changed src/testkit/mocks.rs
senamakel c7af224
chore: files changed src/testkit/mocks.rs
senamakel 620c6f8
chore: files changed src/testkit/mocks_double.rs
senamakel 354ef7d
chore: files changed src/testkit/mocks_double.rs
senamakel 5bb9f98
chore: files changed src/testkit/mocks_double.rs
senamakel 4765a70
chore: files changed src/testkit/mocks_double.rs
senamakel a516b5e
chore: files changed src/nodes/integration/approval_request.rs
senamakel c45d27f
chore: files changed src/testkit/mocks_double.rs
senamakel 731a462
chore: files changed src/testkit/mocks_tests.rs
senamakel 780b687
chore: files changed src/nodes/integration/approval_tests/approval_te…
senamakel 709e99d
chore: files changed src/nodes/integration/approval_tests/approval_te…
senamakel 0111b88
chore: files changed src/testkit/mocks_tests.rs
senamakel 9ce8174
chore: files changed src/nodes/integration/approval.rs
senamakel 86186ef
chore: files changed src/nodes/integration/approval_request.rs
senamakel 207d897
chore: files changed src/nodes/integration/approval_request.rs
senamakel f77ec77
chore: files changed src/catalog/contracts/group_03.rs
senamakel f83bed9
chore: files changed src/nodes/integration/approval_tests/approval_te…
senamakel fca976c
chore: files changed src/nodes/integration/approval_request.rs
senamakel 051d17c
chore: files changed src/catalog/contracts/group_03.rs,src/nodes/inte…
senamakel fda43c0
chore: files changed src/nodes/integration/approval_tests/approval_te…
senamakel 5790907
chore: files changed src/nodes/integration/approval_request.rs
senamakel ca8c656
chore: files changed src/nodes/integration/approval_request.rs
senamakel a5f79e3
chore: files changed src/nodes/integration/approval_tests/approval_te…
senamakel 231a454
chore: files changed src/nodes/integration/approval_tests/approval_te…
senamakel e73f268
chore: files changed src/nodes/integration/approval_tests/approval_te…
senamakel e505dfa
chore: files changed src/nodes/integration/approval_request.rs
senamakel 2717ef8
chore: files changed src/engine/run_state.rs
senamakel 480958c
chore: files changed src/engine/run_state.rs
senamakel f13ff32
chore: files changed src/nodes/integration/approval_tests/approval_te…
senamakel 4173473
docs(changelog): record the approval-provenance behaviour change
senamakel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| #![forbid(unsafe_code)] | ||
|
|
||
| //! Human review as a **step in the graph**: an `approval` node hands a URL to a | ||
| //! host-implemented review surface, the run pauses while nobody has answered, | ||
| //! and the branch it takes afterwards depends on what the human said. | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| //! | ||
| //! `DeskReview` below stands in for whatever real surface a host has — a Slack | ||
| //! card, an inbox row, a web queue. It shows the two things the | ||
| //! [`ApprovalProvider`](tinyflows::caps::ApprovalProvider) contract asks for: | ||
| //! **create-or-fetch** on `request_id`, so re-asking never notifies the reviewer | ||
| //! twice, and a decision that can carry the human's own edit. | ||
| //! | ||
| //! Run: cargo run --example hitl_review --features mock | ||
| #[cfg(feature = "mock")] | ||
| #[tokio::main(flavor = "current_thread")] | ||
| async fn main() { | ||
| use std::collections::HashMap; | ||
| use std::sync::{Arc, Mutex}; | ||
|
|
||
| use async_trait::async_trait; | ||
| use serde_json::{Value, json}; | ||
| use tinyflows::caps::mock::mock_capabilities; | ||
| use tinyflows::caps::{ | ||
| ApprovalDecision, ApprovalOutcome, ApprovalProvider, ApprovalRequest, Capabilities, | ||
| }; | ||
| use tinyflows::compiler::compile; | ||
| use tinyflows::engine::{resume, run}; | ||
| use tinyflows::model::{Edge, Node, NodeKind, WorkflowGraph}; | ||
|
|
||
| /// A host's review desk: one row per `request_id`, holding the verdict once | ||
| /// a human has left one. | ||
| #[derive(Default)] | ||
| struct DeskReview { | ||
| rows: Mutex<HashMap<String, Option<ApprovalDecision>>>, | ||
| } | ||
|
|
||
| impl DeskReview { | ||
| /// What a human does later, from the host's own UI. | ||
| fn answer(&self, request_id: &str, decision: ApprovalDecision) { | ||
| self.rows | ||
| .lock() | ||
| .expect("lock") | ||
| .insert(request_id.to_string(), Some(decision)); | ||
| } | ||
|
|
||
| /// Every review this desk has been asked to run, decided or not. | ||
| fn queue(&self) -> Vec<String> { | ||
| let mut ids: Vec<String> = self.rows.lock().expect("lock").keys().cloned().collect(); | ||
| ids.sort(); | ||
| ids | ||
| } | ||
| } | ||
|
|
||
| #[async_trait] | ||
| impl ApprovalProvider for DeskReview { | ||
| async fn decide( | ||
| &self, | ||
| request: &ApprovalRequest, | ||
| ) -> tinyflows::error::Result<ApprovalOutcome> { | ||
| let mut rows = self.rows.lock().expect("lock"); | ||
| // Create-or-fetch: the row is keyed on `request_id`, so the run | ||
| // asking again after a resume finds THIS review rather than opening | ||
| // a second one and pinging the reviewer twice. | ||
| let row = rows.entry(request.request_id.clone()).or_insert_with(|| { | ||
| println!( | ||
| "[desk] new review {:?}: {} -> {}", | ||
| request.request_id, | ||
| request.title.as_deref().unwrap_or("(untitled)"), | ||
| request.subject.value | ||
| ); | ||
| None | ||
| }); | ||
| Ok(match row.clone() { | ||
| Some(decision) => ApprovalOutcome::Decided(decision), | ||
| None => ApprovalOutcome::Pending, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| fn node(id: &str, kind: NodeKind, config: Value) -> Node { | ||
| Node { | ||
| id: id.into(), | ||
| kind, | ||
| type_version: 1, | ||
| name: id.into(), | ||
| config, | ||
| ports: vec![], | ||
| position: None, | ||
| } | ||
| } | ||
| fn edge(from: &str, port: &str, to: &str) -> Edge { | ||
| Edge { | ||
| from_node: from.into(), | ||
| from_port: port.into(), | ||
| to_node: to.into(), | ||
| to_port: "main".into(), | ||
| } | ||
| } | ||
|
|
||
| // trigger -> review -> publish (on `approved`) / revise (on `rejected`). | ||
| let graph = WorkflowGraph { | ||
| nodes: vec![ | ||
| node("trigger", NodeKind::Trigger, Value::Null), | ||
| node( | ||
| "review", | ||
| NodeKind::Approval, | ||
| json!({ | ||
| // A real host would key this on the run id (e.g. | ||
| // `"=run.id"`) rather than a literal, so two runs of this | ||
| // graph never collide on the same review. | ||
| "request_id": "hitl-review-example", | ||
| "title": "Publish this post?", | ||
| "prompt": "Approving publishes it to the public feed.", | ||
| "subject_kind": "url", | ||
| "subject": "=item.url", | ||
| "assignees": ["editor@example.com"], | ||
| }), | ||
| ), | ||
| node( | ||
| "publish", | ||
| NodeKind::Transform, | ||
| json!({ "set": { "published": "=item.subject" } }), | ||
| ), | ||
| node( | ||
| "revise", | ||
| NodeKind::Transform, | ||
| json!({ "set": { "revise_because": "=item.comment" } }), | ||
| ), | ||
| ], | ||
| edges: vec![ | ||
| edge("trigger", "main", "review"), | ||
| edge("review", "approved", "publish"), | ||
| edge("review", "rejected", "revise"), | ||
| ], | ||
| ..Default::default() | ||
| }; | ||
|
|
||
| let compiled = compile(&graph).expect("compile"); | ||
| let desk = Arc::new(DeskReview::default()); | ||
| let caps = Capabilities { | ||
| approvals: Some(desk.clone()), | ||
| ..mock_capabilities() | ||
| }; | ||
| let trigger = json!({ "url": "https://example.com/drafts/42" }); | ||
|
|
||
| // 1) Nobody has answered, so the run suspends at the review. Nothing is | ||
| // burned while the card sits in someone's queue. | ||
| let paused = run(&compiled, trigger.clone(), &caps).await.expect("run"); | ||
| println!("--- before the human answers ---"); | ||
| println!("pending_approvals: {:?}", paused.pending_approvals); | ||
| println!("desk queue: {:?}", desk.queue()); | ||
|
|
||
| // 2) The human approves — and edits the URL on the way through, which the | ||
| // host reports as the decision's payload. | ||
| let request_id = desk.queue().first().cloned().expect("one open review"); | ||
| desk.answer( | ||
| &request_id, | ||
| ApprovalDecision { | ||
| approved: true, | ||
| decided_by: Some("editor@example.com".into()), | ||
| comment: Some("fixed the slug".into()), | ||
| payload: Some(json!("https://example.com/drafts/42?utm=newsletter")), | ||
| }, | ||
| ); | ||
|
|
||
| // 3) Resuming re-asks the desk, which now has the verdict. Note the review | ||
| // id is unchanged, so the reviewer is never asked a second time. | ||
| let done = resume(&compiled, trigger, vec![], &caps) | ||
| .await | ||
| .expect("resume"); | ||
| println!("--- after the human answers ---"); | ||
| println!("pending_approvals: {:?}", done.pending_approvals); | ||
| println!( | ||
| "review port: {}", | ||
| done.output["nodes"]["review"]["port"] | ||
| ); | ||
| println!( | ||
| "published: {}", | ||
| done.output["nodes"]["publish"]["items"][0]["json"]["published"] | ||
| ); | ||
| println!("desk queue: {:?}", desk.queue()); | ||
| } | ||
|
|
||
| #[cfg(not(feature = "mock"))] | ||
| fn main() { | ||
| eprintln!( | ||
| "this example needs the mock capabilities: cargo run --example hitl_review --features mock" | ||
| ); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.