Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6d4de2c
feat(interactive): M4 host-owned Coordinator, serialized Agent Pendin…
rockyshi1993 Nov 10, 2026
e9ca59e
fix(interactive): M4 canonical public frames, no ephemeral extras, re…
rockyshi1993 Nov 10, 2026
3a3002d
test(interactive): M4 P0-A Program traversal, durable Agent retry, re…
rockyshi1993 Nov 10, 2026
6113341
feat(interactive): M4 explicitly scoped public interactive subpath
rockyshi1993 Nov 10, 2026
0e912ab
types(interactive): M4 host-only Coordinator, Pending, Store and comp…
rockyshi1993 Nov 10, 2026
e2578d3
build(interactive): include M4 Coordinator, SDK entry and declaration…
rockyshi1993 Nov 10, 2026
989e9eb
feat(interactive): expose M4 SDK from isolated ./interactive subpath …
rockyshi1993 Nov 10, 2026
5c00c2d
test(interactive): assert isolated SDK export while preserving Standa…
rockyshi1993 Nov 10, 2026
91114a9
test(interactive): assert isolated SDK export while preserving Standa…
rockyshi1993 Nov 10, 2026
9e7b8d1
test(interactive): consume real M4 SDK from installed private package
rockyshi1993 Nov 10, 2026
0ca3cf3
test(interactive): M4 TypeScript public subpath and forbidden P0-B ca…
rockyshi1993 Nov 10, 2026
85c9af6
test(types): include M4 Interactive compile-time contract checks
rockyshi1993 Nov 10, 2026
cb29e85
fix(interactive): refuse malformed failure receipts and primitive acc…
rockyshi1993 Nov 10, 2026
ab705e6
test(interactive): M4 malformed Host failures, primitive results and …
rockyshi1993 Nov 10, 2026
d088317
test(interactive): SIGKILL fault injection around prepare, publish, r…
rockyshi1993 Nov 10, 2026
ccf3bbe
test(interactive): target real phase cuts without assuming a fixed re…
rockyshi1993 Nov 10, 2026
c2ca175
docs(interactive): M4 Host-only execution contract and crash-recovery…
rockyshi1993 Nov 10, 2026
bbd29e3
docs(interactive): render SDK identifiers as Markdown code spans
rockyshi1993 Nov 10, 2026
815573e
test(interactive): cover SIGKILL before/after enter, prepare and publ…
rockyshi1993 Nov 10, 2026
8259a67
test(interactive): confirm durable receipt Ack after successful CAS r…
rockyshi1993 Nov 10, 2026
4aed90b
test(interactive): race two real Node Host processes on one Agent rec…
rockyshi1993 Nov 10, 2026
2302676
fix(interactive): align Agent Pending id with Standard runAgent fields
rockyshi1993 Nov 10, 2026
71981c9
fix(interactive): align Agent Pending id with Standard runAgent fields
rockyshi1993 Nov 10, 2026
90dbd55
fix(interactive): align Agent Pending id with Standard runAgent fields
rockyshi1993 Nov 10, 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
136 changes: 136 additions & 0 deletions docs/interactive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# FlowKit Interactive — P0-A M4 Host Coordinator

> **Internal/maintainer integration only.** The Standard root API remains exactly
> `run / compile / compileSource / check / evalWhen / isControlSignal`.
> Interactive APIs live under `@devcodex-labs/flowkit/interactive`.
> This does **not** grant a model permission to act as a trusted Host.

## Supported features

- **Implemented**: Condition, Terminal, Agent (no Agent verify/checkStep), immutable compiled JavaScript Program, original Standard analyzer, namespace-scoped SQLite WAL/FULL Checkpoint Store, phase-by-phase CAS, deterministic Agent preparation, persist-before-publish, receipt-accept-before-validate, local sync validation, retry exhaustion, crash resume, final State and error cause.
- **Not implemented in P0-A**: Human, Action, Verification, Loop, Parallel, Agent verify or external checkStep, automatic `runAgent` invocation, trusted Human provenance bridge, background service, P0-B/P1/P2 extensions.
- The Coordinator and the Store are **owned and invoked by the Host**, not by the model. A model must not have direct filesystem/database access or direct `complete()` authority.

## Example: compile, start, resolve pending, resume

The source is a validated `*.workflow.js` or `*.workflow.ts` using
FlowKit's existing DSL; it is **not imported or executed** while compiling.

~~~js
import {readFile} from 'node:fs/promises';
import {
compileInteractive, createSqliteCheckpointStore, createInteractiveCoordinator,
} from '@devcodex-labs/flowkit/interactive';

const compiled = await compileInteractive('tasks/review.workflow.js', {
output: 'memory',
featureStage: 'P0-A',
});
if (compiled.diagnostics.some(d => d.severity === 'error')) {
throw new Error('Interactive compile rejected the workflow');
}

const store = createSqliteCheckpointStore('.host-private/review.db', {
namespace: 'host-tenant-001',
});
try {
const coordinator = createInteractiveCoordinator({
store,
policy: {bindingVersion: 1, hostPolicyId: 'host-policy-v1'},
runtime: {
// Trusted-read: return the instruction; do not make model calls here.
skillResolver: async ({agent, skill, nodeId}) => {
if (!agent || skill) throw new Error('Unsupported skill mapping in this example');
return readFile(new URL('skills/' + agent + '.md', import.meta.url), 'utf8');
},
// Pure, synchronous local output schema validator.
validateResult: (_outputSchema, result) =>
typeof result?.answer === 'string' ? [] : ['answer is required'],
},
});

let frame = await coordinator.start({
programBytes: Buffer.from(compiled.code, 'utf8'),
programIdentity: compiled.programIdentity,
startKey: 'review-2026-11-10',
state: {ready: true, message: 'Review the supplied draft'},
});

while (frame.status === 'waiting') {
const task = frame.pending; // Only published after durable CAS.
// modelResult = await HOST_OWNED_AGENT_TRANSPORT(task);
const modelResult = {answer: 'Reviewed'}; // demonstration only
frame = await coordinator.complete({
runId: task.runId,
requestId: task.requestId,
nodeId: task.nodeId,
attempt: task.attempt,
kind: 'agent-result',
result: modelResult,
});
}
while (frame.status === 'paused' && frame.pauseReason === 'stage-budget') {
frame = await coordinator.resume({runId: frame.runId});
}
if (frame.status === 'errored') throw new Error(frame.terminal?.causeCode);
console.log(frame.status, frame.state);
} finally {
store.close();
}
~~~

## Invariants and recovery

1. **Program is the routing authority.** The generated `advance(view,event)`
and its verified immutable `programTable` supply legal stage transitions.
The Coordinator does not reconstruct DSL routing or let model output choose
`toBlockId`.
2. **A durable Pending precedes delivery.** The Host may dispatch only the
`frame.pending` returned from a successful `waiting` CAS. A restart with
that run ID returns the same `requestId` and persisted task.
3. **Receipt is stored before validation.** `complete()` atomically stores the
result/failure, its canonical digest, `acceptedRevision` and the `validate`
Checkpoint. The local validator runs only afterward. If the process stops
between the two steps, `resume()` re-validates the **same** candidate,
without publishing the previous request again.
4. **Receipt retransmission is idempotent.** The same canonical body with the
same `requestId/nodeId/attempt` returns the original
`acceptedRevision` even if several later revisions exist; different
content returns `FK_I_RECEIPT_CONFLICT`.
5. **Only a validated result may modify State.** Validation failure retries
with a new deterministic request ID up to `retries+1`; exhaustion becomes
`errored(FK_R004)`. A malformed pure validator becomes
`errored(FK_I_VALIDATOR_CONTRACT)`. No candidate is merged before a
successful validation and commit-stage CAS.
6. **Identity is fixed.** The run pins `namespace`, Program artifact bytes,
start intent, and policy fingerprint. Rebinding or changing policy requires
a compatible, explicitly managed procedure; it is not inferred from source
or a new Program file.
7. **Budget is explicit.** A finite `maxStagesPerCall` (1–128, default 128)
produces a `paused(stage-budget)` checkpoint, which `resume()` can
advance. It is never a fake `waiting` frame.

## Security / operational boundary

- Treat `runtime.skillResolver` as **trusted read-only code**: it may be
invoked again after a conflict or crash. Do not charge models or execute
external side effects in it.
- `runtime.validateResult` must be **synchronous, deterministic and pure**.
FlowKit cannot establish purity of arbitrary Host callbacks.
- Agent input contains only declared input keys plus a `missing` list;
absent keys are not synthesized as null. The Host still owns disclosure
rules for confidential state and instructions.
- `complete()` is **not a model-exposed tool**. M5 must enforce actual Host
provenance and identity outside the model-controlled process.
- Store data is written with `node:sqlite` in WAL mode and
`synchronous=FULL`, with every transition in one SQLite transaction.
Do not place this reference Store on an unreliable/shared filesystem.
Node 22's built-in SQLite is experimental; the pinned GitHub Actions
Ubuntu/Windows Node 22/24 matrix is mandatory evidence, not a generic
claim of compatibility for arbitrary host environments.
- External Agent calls are **not exactly-once**: the Host must provide
idempotent delivery/reconciliation if it has externally billed or side
effecting operations. FlowKit guarantees a unique durable pending identity,
not exactly-once provider execution.
- No npm publication, deployment, untrusted code sandbox, or background
service is part of this milestone.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
"import": "./dist/integrations/capability-graph.mjs",
"default": "./dist/integrations/capability-graph.mjs"
},
"./interactive": {
"types": "./dist/interactive.d.ts",
"import": "./dist/interactive/index.mjs",
"default": "./dist/interactive/index.mjs"
},
"./package.json": "./package.json"
},
"files": [
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export async function validatePackage(root, packedPaths) {
assert.deepEqual(lock.packages[''].dependencies, pkg.dependencies); assert.equal(lock.version, pkg.version);
assert.equal(lock.packages[''].name, pkg.name); assert.equal(lock.packages[''].version, pkg.version);
assert.deepEqual(pkg.files, ['dist', 'README.md', 'LICENSE']);
assert.deepEqual(Object.keys(pkg.exports), ['.', './dsl', './capability-graph', './package.json']);
for (const [key, types, module] of [['.', './dist/compiler-api.d.ts', './dist/index.mjs'], ['./dsl', './dist/dsl.d.ts', './dist/dsl.mjs'], ['./capability-graph', './dist/capability-graph.d.ts', './dist/integrations/capability-graph.mjs']]) {
assert.deepEqual(Object.keys(pkg.exports), ['.', './dsl', './capability-graph', './interactive', './package.json']);
for (const [key, types, module] of [['.', './dist/compiler-api.d.ts', './dist/index.mjs'], ['./dsl', './dist/dsl.d.ts', './dist/dsl.mjs'], ['./capability-graph', './dist/capability-graph.d.ts', './dist/integrations/capability-graph.mjs'], ['./interactive', './dist/interactive.d.ts', './dist/interactive/index.mjs']]) {
assert.deepEqual(Object.keys(pkg.exports[key]), ['types', 'import', 'default']);
assert.deepEqual(pkg.exports[key], { types, import: module, default: module });
}
Expand Down
4 changes: 2 additions & 2 deletions scripts/package-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export const packageFiles = [
...['source', 'extract', 'validate', 'skill-check', 'analyze', 'graph-algorithms', 'graph', 'dataflow', 'values', 'paths', 'mermaid', 'emit-plan', 'helpers', 'emit', 'compile', 'api', 'output'].map(name => [`src/compiler/${name}.mjs`, `dist/compiler/${name}.mjs`]),
...['feature-gate', 'lower', 'validate-ir'].map(name => [`src/compiler/interactive/${name}.mjs`, `dist/compiler/interactive/${name}.mjs`]),
['src/compiler/interactive/emit.mjs', 'dist/compiler/interactive/emit.mjs'],
...['compile', 'identity', 'load', 'canonical', 'sqlite-store'].map(name => [`src/interactive/${name}.mjs`, `dist/interactive/${name}.mjs`]),
...['compile', 'identity', 'load', 'canonical', 'sqlite-store', 'coordinator', 'index'].map(name => [`src/interactive/${name}.mjs`, `dist/interactive/${name}.mjs`]),
...['parser', 'evaluate'].map(name => [`src/when/${name}.mjs`, `dist/when/${name}.mjs`]),
...['context', 'execute', 'load', 'source', 'skills', 'skill-remote', 'run', 'aot', 'dump', 'dump-channel', 'dump-hook'].map(name => [`src/runtime/${name}.mjs`, `dist/runtime/${name}.mjs`]),
...['dsl', 'index', 'compiler-api'].map(name => [`types/${name}.d.ts`, `dist/${name}.d.ts`]),
...['dsl', 'index', 'compiler-api', 'interactive'].map(name => [`types/${name}.d.ts`, `dist/${name}.d.ts`]),
];
Loading