Semantic Synthesis Engine avatar

Semantic Synthesis Engine

Pricing

from $0.15 / actor start

Go to Apify Store
Semantic Synthesis Engine

Semantic Synthesis Engine

RAG Pipeline, Agentic Infrastructure, Idempotent Ingestion — deterministic-first LLM synthesis over Delta-Sync Sentinel manifests, gated by a separated Critic/canary assertion step.

Pricing

from $0.15 / actor start

Rating

0.0

(0)

Developer

Nathan Carter

Nathan Carter

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

5 days ago

Last modified

Categories

Share

Actor 2 of 3. Takes a committed manifest from ../delta-sync-sentinel and produces a synthesis result gated by a deterministic Critic — a "canary" step that decides ok vs critic_rejected before an agent ever sees the payload.

Status: implemented (Task 2). synthesize() calls Claude (temperature 0) to produce {summary, keyFacts, citedChunkIds}. runCritic() runs four real deterministic checks. main.ts checks the KV idempotency cache before calling either. See "Task 2 implementation notes" below for where this deviates from the original brief, and why.

Task 2 implementation notes

Three places where Task 2's instructions conflicted with what Task 1 already shipped. Resolved, not silently picked:

  1. Status vocabulary. The brief said status is 'completed' or 'needs_human_review'. The already-published contract (types.ts, both output schemas) says 'ok' / 'critic_rejected' / 'error'. Kept the original enum values — completed and needs_human_review are the human-readable meaning of ok and critic_rejected, not a field rename, so nothing that already reads this contract breaks.
  2. determinism_self_test redefined. Originally specified (Task 1) as: re-run synthesize() and hash-compare, fail on any diff. Task 2 makes synthesize() a real LLM call — temperature 0 reduces but does not guarantee bit-exact output, so a strict re-run-and-diff would reject a large share of good runs and doubles LLM cost per verification. It now checks internal self-consistency of the single payload instead: no duplicate citedChunkIds, and zero citations despite available source chunks is treated as a failure (a vacuous-result guard), not a valid "nothing to say."
  3. Error shape. Task 1's own infra rule banned prose errors. Task 2 rule 4 asks for { error: "CODE", message: "..." }. errorCode stays the field to branch on (closed enum, matches the published schema); message was added as a supplementary string alongside it, not a replacement for the code.

Verification gap, stated plainly: this environment has no ANTHROPIC_API_KEY — only Claude Code's own OAuth session, not a usable API key for a direct SDK call. synthesize() is implemented against the real Anthropic SDK and typechecks, but the live LLM call itself has not been executed end-to-end. What has been verified for real:

  • npm run harness — 8/8 passing, covering all four critic checks against fixture data (good payload, hallucinated citation, low coverage, duplicate citations, oversized output, vacuous zero-citation result).
  • The cache-key fix (analysis-<synthesisId>, not analysis-<manifestId> as literally written in the brief) — confirmed the same manifestId produces different ids per mode, and the same id across repeats of the same mode, so a narrative_summary request can no longer incorrectly hit a structured_extract cache entry.
  • npx tsc --noEmit — clean across the full implementation.

Infrastructure-grade guarantees

Idempotency. synthesisId = sha256(manifestId + synthesisMode + engineVersion). Same three inputs, same id — call this actor twice on the same manifest and you get the same identity back, so an agent can treat repeat calls as free.

Versioning. engineVersion is on every result, not just in a changelog. A cached synthesisId is only trustworthy alongside the engineVersion it was produced under — if the engine bumps, the id space is allowed to change, and callers are expected to check this rather than assume forever-stability.

Canary verification. Synthesis and verification are two separate modules (synthesis.ts, critic.ts) called from two separate steps in main.ts, on purpose — the Critic never trusts synthesis's own opinion of itself. Its checks are deterministic by design (set/schema operations, not model calls), so a narrative_summary synthesis can be non-deterministic while its verification stays reproducible. status: "critic_rejected" is a first-class outcome, not an error — the pipeline is expected to reject some fraction of runs, and that's the point of having it.

Structured errors only. Every failure path returns { status: "error", errorCode: <enum>, message?: <string> }. errorCode — drawn from a closed enum (MANIFEST_NOT_FOUND, SENTINEL_UNREACHABLE, COST_CEILING_EXCEEDED, VALIDATION_ERROR, SYNTHESIS_TIMEOUT) — is the field to branch on, matching the Sentinel's own typed-error convention (ValidationError, CostCeilingError, etc. in errors.ts) one layer up the stack. message is a supplementary human-readable detail string; treat it as a log line, not a contract.

Integration: how an agent triggers this from a Sentinel manifest

The Sentinel emits committed manifests with a manifestId (see its README, "Lifecycle example"). This actor's whole input surface exists to consume exactly that value:

{
"sentinelDatasetId": "<the Sentinel run's defaultDatasetId>",
"manifestId": "9697071a4e2c...",
"synthesisMode": "structured_extract",
"maxCostUsd": "1.00"
}

Recommended agent loop:

  1. Poll the Sentinel's health endpoint (or watch its dataset) for last_cursor to advance.
  2. Read the new manifest's manifestId and the Sentinel run's defaultDatasetId.
  3. Call this actor with both, plus a cost ceiling.
  4. Branch only on status: ok → use result; critic_rejected → inspect criticVerdict.checks to see which assertion failed, do not use a partial result (there isn't one); error → branch on errorCode, never on message text.

Agent-native schemas

  • .actor/input_schema.json — every field's description is written to explain intent, not just type, so an agent building the call payload from the schema alone (function-calling style) has enough context to fill it in correctly.
  • output_schema.json (project root) — the full function-calling return contract. Kept separate from .actor/output_schema.json, which is Apify's own link-template convention and isn't meant to carry field-level documentation.
  • .actor/dataset_schema.json — same field descriptions as the root schema, in the shape Apify's Console actually renders as a table.

Module boundaries

  • types.ts — shared contract. No logic.
  • synthesis.ts — candidate generation only. Must never decide acceptability.
  • critic.ts — deterministic acceptance checks only. Must never generate content.
  • main.ts — orchestration: fetch Sentinel chunks → synthesize → critic → structured outcome. Mirrors the Sentinel's Actor.init()/log/KV-first conventions.

Local development

npm install
npm run typecheck
npm run harness # critic.ts invariants against fixtures — no API key needed
ANTHROPIC_API_KEY=sk-... npm run start:dev # real end-to-end run, requires a real key