Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
81c49df
feat(sdk): compile typed outputs to json_schema
Sep 2, 2026
5ab0fee
fix(sdk): enforce structured output contracts
Sep 2, 2026
24c43d6
ops(review): persist PR #133 swarm transcripts
Sep 2, 2026
54361d7
ops(review): persist PR #133 swarm transcripts
Sep 2, 2026
321b272
feat(sdk): add declared agent model contract
Sep 2, 2026
f774694
kernel: dispatch runnable steps in parallel
Sep 2, 2026
39d535c
fix(sdk): refuse fields outside step verb schemas
Sep 2, 2026
115acf2
feat(sdk): settle data and code gate contract
Sep 2, 2026
78efc0d
docs(review): record PR 136 fresh review
Sep 2, 2026
4888d15
fix(sdk): make model adapters fail closed
Sep 2, 2026
52a112e
fix(kernel): drive complete parallel dispatch batches
Sep 2, 2026
22c7d31
fix(sdk): harden malformed step validation
Sep 2, 2026
3c7d939
feat(cli): run authored flows with direct input
Sep 2, 2026
adae4e2
ops(review): record PR 140 repair findings
Sep 2, 2026
8880122
fix(sdk): fail closed on invalid gates
Sep 2, 2026
688d6a0
fix(sdk): bundle JSON Schema draft metadata
Sep 2, 2026
0987e38
fix(cli): execute direct flows through journal runtime
Sep 2, 2026
060e8b9
fix(sdk): bind declared model execution checks
Sep 2, 2026
15ef367
fix(kernel): preserve parallel assignment lifecycle
Sep 2, 2026
fccc52a
docs(review): record PR 136 integration review
Sep 2, 2026
30f2324
test(sdk): cover public named-agent boundaries
Sep 2, 2026
e50d977
fix(sdk): refuse fields outside step verb schemas
Sep 2, 2026
866d399
fix(sdk): harden malformed step validation
Sep 2, 2026
1c6f8a4
fix(sdk): close timeout and dependency boundaries
Sep 2, 2026
4a6c0cc
fix(surface): fail closed at authored boundary
Sep 2, 2026
64c2d5f
merge: reconcile typed outputs for research integration
Sep 2, 2026
62a647f
fix(sdk): bind named agent preflight provenance
Sep 2, 2026
11ad3e2
fix(sdk): fail closed at gate boundaries
Sep 2, 2026
b3643a6
merge: reconcile declared model runtime for research integration
Sep 2, 2026
51e7b22
merge: reconcile production parallel dispatch for research integration
Sep 2, 2026
3b5fbcd
merge: update declared model to exact PR 136 head 62a647f
Sep 2, 2026
4c5ec33
merge: reconcile exact PR 138 head 1c6f8a4
Sep 2, 2026
6527fd3
fix(sdk): compose typed outputs with closed step fields
Sep 2, 2026
0c85927
fix(kernel): validate deep dependency graphs iteratively
Sep 2, 2026
9b45fdc
merge: reconcile exact PR 139 head 11ad3e2
Sep 2, 2026
f5b8437
fix(runtime): close gate boundary execution holes
Sep 2, 2026
b65b8d9
fix(sdk): compose gate snapshots with declared models
Sep 2, 2026
e164e42
fix(sdk): bound dependency cycle diagnostics
Sep 2, 2026
e6210a2
fix(sdk): reject proxies at exported boundaries
Sep 2, 2026
64beb20
merge: reconcile exact PR 134 lifecycle repair for integration
Sep 2, 2026
a089bc5
merge: reconcile exact PR 138 dependency diagnostics repair
Sep 2, 2026
9404a4e
merge: reconcile exact PR 139 hostile-boundary repair
Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/cloud-runtime-artifact.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ jobs:
working-directory: kernel
run: cargo build --locked --release -p relayflowd

- name: Install SDK dependencies
run: npm ci --prefix sdk

- name: Test SDK and type-level authoring contracts
working-directory: sdk
run: |
npm run build
npm run typecheck:tests
./node_modules/.bin/vitest run \
tests/typed-output.test.ts \
tests/validate.test.ts \
tests/spec-parity.test.ts \
tests/deterministic-llm.test.ts

- name: Build standalone flows CLI
run: |
cd surface
Expand Down
171 changes: 162 additions & 9 deletions docs/SURFACE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion kernel/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ Minimal verb set for gate 1:
| verb | params → result | purpose |
|---|---|---|
| `hello` | `{protocol: 0, client}` → `{protocol: 0, server}` | handshake; version mismatch is a hard error |
| `run.start` | `{spec}` → `{run_id}` | validate spec (zero-agent flows are legal), create run file, append `run.spawned`, begin scheduling |
| `run.start` | `{spec}` → `{run_id}` | validate spec (zero-agent flows are legal; invalid declarations return `invalid_spec` before storage), create run file, append `run.spawned`, begin scheduling |
| `run.resume` | `{run_id}` → `{run_id, state}` | §3 memoized resume |
| `run.get` | `{run_id}` → `{status, steps, budget}` | snapshot for legibility |
| `run.watch` | `{run_id}` → stream of `{event: "entry", data: Entry}` | every appended entry, pushed |
Expand Down
1 change: 1 addition & 0 deletions kernel/relayflowd-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod event;
pub mod journal;
pub mod machine;
pub mod retry;
mod schema;
pub mod spec;
pub mod state;
pub mod verify;
Expand Down
69 changes: 55 additions & 14 deletions kernel/relayflowd-core/src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ pub fn next_actions(state: &RunState, now_ms: i64) -> Vec<Action> {
return Vec::new();
}
if let Some(failed_step_id) = state.failed_step() {
if state
.steps
.values()
.any(|runtime| matches!(runtime.state, StepState::Running { .. }))
{
// Drain already-started siblings before making run.completed
// terminal. No fresh work is elected once failure is inevitable.
return Vec::new();
}
return complete_run_actions(
state,
RunCompletionReason::StepFailed,
Expand All @@ -99,15 +108,25 @@ pub fn next_actions(state: &RunState, now_ms: i64) -> Vec<Action> {
return complete_run_actions(state, RunCompletionReason::Success, None, now_ms);
}

let mut timers = Vec::new();
for spec in &state.spec.steps {
let runtime = &state.steps[&spec.id];
match runtime.state {
StepState::Backoff {
// Wake every retry whose deterministic timer is due before starting work.
// Recovery can put several crashed parallel lanes into the same zero-delay
// backoff; waking just one would let an already-runnable peer start and park
// the run while the other due lane remained asleep.
let due_waits = state
.spec
.steps
.iter()
.filter_map(|spec| {
let runtime = &state.steps[&spec.id];
let StepState::Backoff {
attempt,
wake_at_ms,
} if wake_at_ms <= now_ms => {
return vec![Action::Append(JournalEntry::new(
} = runtime.state
else {
return None;
};
(wake_at_ms <= now_ms).then(|| {
Action::Append(JournalEntry::new(
EntryType::WaitCompleted,
state.run_id.clone(),
Some(spec.id.clone()),
Expand All @@ -118,13 +137,31 @@ pub fn next_actions(state: &RunState, now_ms: i64) -> Vec<Action> {
completion_reason: WaitCompletionReason::TimerFired,
result: Value::Null,
},
))];
}
StepState::Backoff { wake_at_ms, .. } => {
timers.push(Action::ArmTimer { at_ms: wake_at_ms });
}
StepState::Runnable => return start_actions(state, spec, runtime.attempts + 1, now_ms),
_ => {}
))
})
})
.collect::<Vec<_>>();
if !due_waits.is_empty() {
return due_waits;
}

// A fold marks every dependency-free step Runnable at once. Preserve the
// authored spec order while emitting every journal-first start pair from
// that one state snapshot; dependencies unlocked by these executions are
// considered only after their completions are folded on the next pass.
let starts = parallel::runnable_batch(state)
.into_iter()
.flat_map(|(spec, runtime)| start_actions(state, spec, runtime.attempts + 1, now_ms))
.collect::<Vec<_>>();
if !starts.is_empty() {
return starts;
}

let mut timers = Vec::new();
for spec in &state.spec.steps {
let runtime = &state.steps[&spec.id];
if let StepState::Backoff { wake_at_ms, .. } = runtime.state {
timers.push(Action::ArmTimer { at_ms: wake_at_ms });
}
}
timers.sort_by_key(|action| match action {
Expand Down Expand Up @@ -416,5 +453,9 @@ fn deterministic_ulid(
mod recovery;
pub use recovery::{abandonment_actions, recovery_actions, recovery_actions_filtered};

mod parallel;

#[cfg(test)]
mod parallel_tests;
#[cfg(test)]
mod tests;
70 changes: 70 additions & 0 deletions kernel/relayflowd-core/src/machine/parallel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! Deterministic parallel batch selection.
//!
//! DAG independence is not enough for agent work: two steps can be topology-
//! independent while mutating the same declared surface. The authored-order
//! maximal batch therefore reserves each mutable surface once. An unfinished
//! attempt in backoff or waiting retains its reservation just like a live
//! lease, preventing a crash/retry from turning into a last-write-wins fork.

use std::collections::BTreeSet;

use crate::{
spec::{StepKind, StepSpec},
state::{RunState, StepRuntime, StepState},
};

pub(super) fn runnable_batch(state: &RunState) -> Vec<(&StepSpec, &StepRuntime)> {
let mut occupied = state
.spec
.steps
.iter()
.filter(|step| {
matches!(
state.steps[&step.id].state,
StepState::Running { .. }
| StepState::Backoff { .. }
| StepState::Waiting { .. }
| StepState::NeedsHuman { .. }
)
})
.flat_map(surface_keys)
.collect::<BTreeSet<_>>();
let mut selected = Vec::new();
for step in &state.spec.steps {
let runtime = &state.steps[&step.id];
if runtime.state != StepState::Runnable {
continue;
}
let surfaces = surface_keys(step).collect::<Vec<_>>();
if surfaces.iter().any(|surface| occupied.contains(surface)) {
continue;
}
occupied.extend(surfaces);
selected.push((step, runtime));
}
selected
}

fn surface_keys(step: &StepSpec) -> impl Iterator<Item = String> + '_ {
let StepKind::Agent { surfaces, .. } = &step.kind else {
return Vec::new().into_iter();
};
surfaces
.workspace
.iter()
.map(|surface| format!("workspace:{}", surface.surface))
.chain(
surfaces
.streams
.iter()
.map(|surface| format!("stream:{}", surface.stream)),
)
.chain(
surfaces
.external
.iter()
.map(|surface| format!("external:{surface}")),
)
.collect::<Vec<_>>()
.into_iter()
}
Loading
Loading