Skip to content

Human intervention: public API usage ​

简体中文 · Runtime · Five workflows and commands

Applications compose public graph, runtime.run, CONTEXT.LOAD, INFER.REASONING.SAMPLE, INTERACTION.ACT.TOOL and INTERACTION.OUTPUT APIs. Application tools persist task state, review policy, artifact versions and effects. No business approval API is added to Core, and delivery receipts do not grant approval.

Integration and complete call ​

Use Node.js 24+. In an application with @codesoul-co/ditto installed, copy examples/control-flow/human/ and examples/_shared/tools/human-review-store.ts, preserving relative paths. Prepare ditto.yaml and .env. These are application sources, not Core package exports. The adapter uses only Node.js standard libraries, including node:sqlite.

This example creates a pending task and defines a continuation handler for an authenticated human decision. Never register onHumanDecision as a model tool. Derive actor from a verified session, not an untrusted username in a request.

ts
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createDitto, loadRuntimeConfigFile } from "@codesoul-co/ditto/runtime";
import { createContextWorker } from "@codesoul-co/ditto/worker/context";
import { createInferWorker } from "@codesoul-co/ditto/worker/infer";
import { createInteractionWorker } from "@codesoul-co/ditto/worker/interaction";
import { HumanReviewStore } from "./examples/_shared/tools/human-review-store.ts";
import { createFixture, reviewers } from "./examples/control-flow/human/fixtures.ts";
import { runApproval } from "./examples/control-flow/human/approval.ts";

const config = loadRuntimeConfigFile("ditto.yaml", process.env);
if (!config.model) throw new Error("Configure a default model");
const input = { id: "task", model: config.model };
const directory = await mkdtemp(join(tmpdir(), "ditto-human-"));
await createFixture(directory, "approval");

function open() {
  const store = new HumanReviewStore(directory, reviewers);
  const runtime = createDitto({
    config,
    sandbox: { ...config.sandbox, tools: store.tools.map(tool => tool.name) },
    workers: [
      createContextWorker(),
      createInferWorker(),
      createInteractionWorker({ tools: store.tools, output: store.output }),
    ],
  });
  return { store, runtime };
}

const first = open();
try {
  await first.store.create(input.id, "approval");
  const pending = await runApproval(first.runtime, input);
  console.log({ directory, request: pending.request, stage: pending.job.stage });
} finally {
  await first.runtime.close();
  first.store.close();
}

// Invoke only from the application's authenticated human-decision handler.
export async function onHumanDecision(decision: {
  requestId: string;
  expectedToken: string;
  choice: "approve" | "reject";
  actor: string;
}) {
  const next = open();
  try {
    next.store.decide(decision);
    return await runApproval(next.runtime, input);
  } finally {
    await next.runtime.close();
    next.store.close();
  }
}

A new process should load the original directory from application configuration, reconstruct the Runtime and Store, and call the same workflow. Do not call create again or recreate inputs. The CLI demonstrates this process boundary. Replace the fixture policy with application-owned reviewer permissions.

Workflow functions ​

Exports are runApproval, runIntermediate, runEditContinue, runReviewPublish and runEscalation. Files and commands are listed in the example guide. All use this signature:

ts
(runtime: Pick<DittoRuntime, "run">,
 input: { id: string; model: ModelConfig },
 options?: { signal?: AbortSignal }) => Promise<HumanResult>

HumanResult contains job, artifact: Version | null and request: ReviewRequest | null. The job mode must match the function. Waiting, rejection, handoff and completion return durable states normally. Tool failures, validation errors and cancellation throw. runEscalation converts failed or unusable model results into handoffs; cancellation and other errors propagate.

StageMeaning and next action
queuedInputs saved; load facts and create Context
draftedVersion saved; create and deliver the review
awaiting-reviewWait for a decision or permitted edit; reentry delivers the same snapshot
approvedNext call executes the reviewed version; intermediate/edit workflows first run downstream inference
rejectedStop this execution and preserve the rejection
executingPending effect persisted; reentry reconciles and resumes that snapshot
completedEffect and result saved; reentry returns the existing result
escalatedHandoff to a human; assignment does not automatically resume execution

Public Context is saved before inference, retaining evidence even when inference fails. Review snapshots include the full proposal, version, artifact and source digests, operation type and target. Activation also binds the target's initial digest. The output sink marks the request delivered only after writing the exact snapshot. Its accepted receipt still requires a separate human decision.

Application review controller ​

HumanReviewStore(directory, reviewers) opens SQLite in an existing directory. ReviewerPolicy is Readonly<Record<string, readonly Purpose[]>>; purposes are activate, intermediate, edit, publish and handoff.

MethodContract
create(id, mode)Read <id>.source.json and <id>.deployment.json; return Promise<HumanJob>; new tasks only
job(id)Read the durable task; reject unknown IDs or unsupported schema
version(id, version?)Read a specific or latest immutable version and verify its digest
request(requestId)Read a review request and verify its snapshot token
decide({ requestId, expectedToken, choice, actor, note? })Approve or reject a delivered, pending, current-version request with the required role; return the updated job
edit({ requestId, expectedToken, replacement, actor, note })For edit/publication tasks awaiting review or approved before execution; create a full version, supersede the old review and return a drafted job
claim({ requestId, expectedToken, actor, note })Assign a pending handoff and record the actor and note
close()Close SQLite after Runtime work finishes

decide, edit and claim are trusted controller methods, absent from store.tools. The token is a SHA-256 digest of the review snapshot, binding content and operation scope. It is not an authentication secret. Authenticate and authorize the human before accepting decisions.

After editing, invoke the workflow to deliver the new request, approve its new request/token, then invoke the workflow again. Editing never auto-approves. Old tokens cannot authorize replacements. A replacement is a full { releaseId, date, title, body } object and cannot change releaseId. Downstream calendar inference receives only the approved version; the effect tool rechecks version, digest and output fields to reject stale results after concurrent edits.

IDs contain 1–64 letters, digits, underscores or hyphens. Input accepts 1–8 unique source IDs. Dates must be valid YYYY-MM-DD; titles allow 200 characters, bodies 4000, and the input change sentence 1000. Edit and assignment notes must be nonempty and at most 1000 characters.

Tools and delivery boundaries ​

Application toolResponsibility
human_readRead job, artifact and request
human_sourceRead actual source files and verify their digest
human_save_contextPersist public Context and source digest before inference
human_save_draftCheck source facts; save the first generated version and Context
human_request_reviewPersist review/handoff requests without granting execution rights
human_applyCheck approval and exact version, persist a pending effect, then write actual files
human_reportPersist and return the complete task result

Inject store.output as an OutputSink with createInteractionWorker({ tools: store.tools, output: store.output }). Tools implement RegisteredTool and are explicitly allowed in the sandbox. Preserve configured network rules for model access. Tool metadata does not replace authorization within human_apply.

Publication writes the body from the reviewed snapshot. Calendar delivery verifies that model-generated { releaseId, date, title } exactly matches the approved artifact. A persisted pending effect resumes its original version after failure or process termination without regenerating content. Identical files may be reused; conflicting files are not overwritten. Target drift, partial file writes and cancellation require application reconciliation, not an assumption of rollback. The local file adapter assumes application ownership of its directory. External systems must implement conditional writes or transactions and idempotency; a file digest check is not a distributed transaction.

Handoffs include the task, sources, public Context, events and any available draft. Reasons include CONFLICTING_DATES, MODEL_FAILED, MODEL_INVALID and HUMAN_REVIEW_REQUIRED. Assignment records the actor and note while preserving escalated. Conflicting sources also route ordinary approval workflows to handoff rather than executing on the first date.

Task acceptance ​

The task suite verifies 18 real scenarios, including actual inbox failures, restart after SIGKILL, edits across processes and during inference, rejected/stale reviews, exact-version publication and target drift. A test controller represents human decisions; model calls use real providers. Failed model outputs are additionally covered by offline regressions. The package suite installs an npm pack tarball outside the repository, typechecks without source aliases, and runs the complete task suite. See the example guide for commands, directories and reports.

Ditto · @codesoul-co/ditto · Node.js 24+