Local resource
Output Contracts
_docs/OUTPUT_CONTRACTS.md
Output Contracts
Identity abstention and result ACK
When a V2 identity snapshot contains any abstention, CIS applies a deterministic post-model guard in addition to prompt instructions. For abstained subjects it:
- replaces chat names and sender identity with generic labels and opaque
- forces
assumed_relation="no determinada"andconfidence_pct=0in chat - drops memory updates for abstained chats (all memory updates for
- redacts JIDs and phone-like digit runs from returned structures; and
- omits model debug payloads, recording
payload_omitted=identity_abstention.
subj_* refs;
summaries;
abstain_all);
Broker results carry a sanitized metadata.identity_certainty ACK containing only contract version, policy version, snapshot_ref, attribution mode and allow/abstain counts. It intentionally omits subjects and identity evidence. Core must validate this ACK and its generation fence before materializing any report, alert, memory, artifact or delivery. Invalid V2 input emits a terminal failed result with a recoverable sanitized ACK when the envelope contained a valid snapshot ref; no partial report is returned.
This document defines the outputs returned by Content Intelligence Service (CIS) after a flow run is accepted.
For producer payloads, see _docs/PAYLOAD_CONTRACTS.md. For exact runtime model request construction, see Runtime Prompt Requests.
Where Results Appear
POST /analyze only accepts the run and returns:
{
"run_id": "uuid",
"status": "accepted",
"flow": "initial-ingestion-report",
"recipe": "initial-ingestion-report-text-only"
}
The actual flow result is available through:
GET /runs/{run_id}asRunStatusResponse.result.- Kafka
content-intelligence.flow-results.v1through.v4as - Dev tools that poll the same run status endpoint.
FlowResultEnvelope.result or FlowResultEnvelope.result_ref.
Response Builder
response_builder is the logical final step declared in every recipe. It is not a separate module today. The concrete implementation is the tail of services/recipes/runner.py::run_recipe, executed by activities/text_activities.py::run_flow_pipeline_activity in the Temporal path or by services/workflow_launcher.py::LocalWorkflowLauncher in local mode. For split Daily runs, services/report_split_runtime.py::ReportSplitRuntime.prepare_reduce verifies exact recomposition, global media merge and pre-model budgets, then checkpoints two internal refs. ReportSplitRuntime.reduce revalidates those checkpoints and invokes the same run_recipe tail.
Responsibilities:
- Build the final
FlowRunResult. - Attach the selected
flow, resolvedrecipe,run_id,statusand - Put the primary, parent-facing flow result under
FlowRunResult.output. - Preserve primary model step outputs in
model_outputs. - Preserve shadow model step outputs or shadow failure objects in
- Preserve rendered
conversation_documentsfor debug/traceability. - Preserve media activity outputs in
media_processed. - For
initial-ingestion-report, unwrap the model'sreportobject into - For
daily-summary-and-notifications-report, validate one daily summary, - When entity extraction is enabled for initial/daily, expose additive
- For
interweek-report, return the model'sinterweek_reportobject without - For Daily and InterWeek supported zero payloads, classify
generated_at.
shadow_outputs.
output.report, key chat summaries by chat_id, and expose output.chat_id_memory_updates.
1 to 5 notifications, no priority field, known chat_ids and memory update decisions. Optional payload.user_stats_metadata may be reflected in the daily summary fields only when the model includes it in the existing output shape.
output.entity_updates using wardian-entity-updates.v1. Each candidate is scoped to a real current source_chat_id, cites real message IDs and may match only a catalog item with the same id, entity kind, relationship and pet species. In effective protected mode, source_chat_id and evidence_message_ids are replaced with deterministic domain-separated opaque references before the result leaves CIS; raw JIDs, phone-bearing IDs and upstream message IDs are not returned. Batches validate independently, then merge and deduplicate candidates while preserving evidence. A failed batch is logged and does not erase successful batches or fail the parent-facing report.
requiring conversation documents. Optional payload.user_stats_metadata may inform the existing InterWeek report fields.
none | verified_zero | unverified_zero | planned_pause_full | planned_pause_partial and build the same public output shapes deterministically before model-client construction. Both zero paths have empty model_outputs, shadow_outputs, conversation_documents and media_processed; Daily also returns chat_id_memory_updates: {}.
For a split run, the reducer stores the completed FlowRunResult behind an internal object-store ref so the large result does not enter Temporal history. TemporalWorkflowLauncher.get_run validates and hydrates that ref before returning the ordinary public result. Internal manifest refs, partition refs, wardian-report-split.v2 and workflow-only split_metadata are not fields of FlowRunResult. BROKER_KAFKA_RESULT_REFS_ENABLED controls only whether a large public Kafka FlowResultEnvelope uses its existing result_ref alternative; startup requires it on batch/result-publishing workers before auto-split can be enabled because an exact recomposed result may exceed Kafka.
When wardian.media-results.v2 was negotiated, every media result includes:
{
"identity": {
"contract_version": "wardian.media-identity.v2",
"chat_id": "chat-1",
"message_id": "m1"
},
"chat_id": "chat-1",
"message_id": "m1"
}
media_processed is ordered deterministically by (chat_id, message_id). Activities, artifact hydration and reducers verify the echoed identity. A mismatch or a conflicting duplicate fails closed; reducers never overwrite one chat's media result with another chat's result.
Non-responsibilities:
- It does not persist product state.
- It does not store raw media bytes.
- It does not merge with previous runs.
- It does not invent fallback report fields when model output omits them. The
deterministic supported-zero path is an explicit input contract, not a model fallback. A non-object AI output fails for every AI step; daily notification flows have extra runtime validation, while initial report field completeness is a prompt/doc contract.
RunStatusResponse
GET /runs/{run_id} returns:
{
"run_id": "uuid",
"status": "accepted|processing|completed|failed",
"flow": "initial-ingestion-report",
"recipe": "initial-ingestion-report-text-only",
"result": null,
"error": null
}
Fields:
| Field | Meaning |
|---|---|
run_id | Run id returned by POST /analyze. |
status | accepted, processing, completed or failed. |
flow | Resolved flow id when known. |
recipe | Resolved recipe id when known. |
result | FlowRunResult when completed; otherwise usually null. |
error | Failure reason when failed. |
FlowRunResult
Completed runs return:
{
"run_id": "uuid",
"flow": "initial-ingestion-report",
"recipe": "initial-ingestion-report-text-only",
"status": "completed",
"output": {},
"model_outputs": [
{"step_id": "initial_report", "model_id": "google/gemini-3.5-flash", "payload": {}}
],
"shadow_outputs": [
{"step_id": "initial_report", "model_id": "google/gemini-3.1-flash-lite", "payload": {}}
],
"conversation_documents": [
{
"chat_id": "chat-1",
"chat_type": "direct",
"chat_name": "Sofi",
"rendered_text": "markdown-like prompt document",
"stats": {}
}
],
"media_processed": [],
"generated_at": "2026-06-16T12:00:00Z"
}
Fields:
| Field | Meaning |
|---|---|
run_id | Run id. |
flow | Resolved flow id. |
recipe | Resolved recipe id. |
status | Always completed inside FlowRunResult. |
output | Stable flow-specific output for product consumers. |
model_outputs | Primary model outputs by AI step. These are debug/audit surfaces, not the product API. |
shadow_outputs | Shadow model outputs by AI step, or { "status": "failed", "error": "..." } payloads when shadow fails. Shadow never overrides primary output. |
conversation_documents | Rendered documents produced by conversation_builder; useful for debug and traceability. |
media_processed | Media activity outputs. Shape varies by media processor and should be treated as operational/debug data. Oversized entries can contain a bounded prompt projection, media_processed_compaction, and a sanitized artifact_ref; product consumers must not depend on raw model debug fields here. |
content_trust_assessment | Typed post-output boundary result. Present on runtime results; mandatory in completed broker v5 results. |
generated_at | Result creation timestamp. |
Content trust assessment
Every generated user prompt is one canonical wardian.untrusted-data-envelope.v1 JSON value, and every flow/media system prompt receives the invariant that dialogue, OCR, transcripts, documents, linked text, memories and prior reports are evidence data—not instructions. The production gateway validates the step-specific Pydantic schema before a payload can reach the response builder.
Completed v5 broker results require:
{
"contract_version": "wardian.content-trust-assessment.v1",
"policy_version": "conversations-are-data.v1",
"mode": "enforce",
"decision": "pass|review|block",
"reason_codes": ["evaluator_unavailable"],
"checks": {
"schema_valid": true,
"evidence_grounded": true,
"memory_grounded": true,
"instruction_boundary": true,
"evaluator_consensus": false
},
"evaluator_status": "completed|unavailable|invalid|not_required",
"output_sha256": "64 lowercase hex characters"
}
output_sha256 binds canonical output, not debug surfaces. Reason codes are an allowlist and do not contain copied conversation text. Supported deterministic no-activity reports pass without an evaluator. In enforce, any ordinary generated result without a completed evaluator is at least review; the consumer must retain it but withhold memory and delivery side effects. In observe, the same typed review remains observable without withholding delivery. A pass assessment cannot carry an unavailable/invalid evaluator or negative evaluator consensus. A completed evaluator returning review or block must include reason codes that match at least one failed typed check; otherwise CIS classifies the evaluator response as invalid. Older result contracts keep their original shape and semantics: CIS strips the assessment before publishing v1-v4.
Safe terminal failure extension
FlowRunResult remains a completed-result contract. Failed broker envelopes may add the optional wardian.safe-failure.v1 extension documented in BROKER_INTEGRATION.md. The extension is outside FlowRunResult.output, so it does not change content_trust_assessment.output_sha256, production-minimal audit_summary.output_sha256, report-context seals, evidence payloads, or legacy completed-result digests. A failed result without the extension keeps the exact N-1 shape.
The safe block contains machine keys only. It never contains conversation text, prompts, input payloads, media data, provider messages or a human-facing error. Control Plane resolves safe_message_key locally and treats suggested_action as a bounded hint, not executable instructions.
Broker production-minimal profile
GET /runs/{run_id} and POST /analyze keep the legacy application contract. Only a v2, v4, or v5 broker request may negotiate result_delivery.profile=wardian.cis-result.production-minimal.v1. Its completed result contains the complete output, a bounded wardian.cis-result-audit-summary.v1, and an evidence descriptor. It never contains model_outputs, shadow_outputs, conversation_documents or media_processed inline. Requests without result_delivery and requests for wardian.cis-result.legacy-full.v1 keep the full result shape above.
audit_summary.output_sha256 binds canonical output; lineage groups model calls without prompts or payloads; usage reports primary, shadow, media and total status/tokens/cost, using status=unknown rather than invented zeroes; and degradations exposes at most 32 sanitized groups. The whole non-output summary is capped at 48 KiB. Primary remains authoritative and shadow never changes output.
When evidence is enabled, the four debug surfaces and sanitized execution records are stored as wardian.cis-result-evidence.v1 behind a signed flow_result_evidence ref. When disabled the descriptor is not_requested. After the bounded object-store grace it is unavailable; CIS never falls back to the legacy full result inline. If the minimal projection itself exceeds the Kafka threshold, the outer result_ref contains only that minimal projection.
conversation_documents[].stats can include:
date_start, date_end, date_last_message, msg_count_total,
msg_count_monitored, msg_count_received, pct_monitored, participant_count,
top_participants, media_summary, media_unprocessed, link_count, domain_list,
pct_school, pct_night, sent_received_ratio, monitored_name
Video and animated-media activity results keep the existing operational media_processed shape:
{
"message_id": "video-1",
"media_type": "video",
"duration_seconds": 12.5,
"analysis": {
"descripcion_corta": "Descripcion breve.",
"texto_visible": null
},
"audio_transcription": "Texto audible o null.",
"frame_summaries": [],
"should_inline": true,
"model_traces": [
{
"gateway": "openrouter",
"model_id": "google/gemini-3.1-flash-lite",
"media_resolution": "MEDIA_RESOLUTION_LOW",
"tokens_input": 1000,
"tokens_output": 100,
"costo_usd": 0.0004,
"fallback_used": false
}
]
}
Animated sticker and gif results use the same shape with their original media_type. Static sticker/GIF content retains the vision result shape. No media bytes or data URLs appear in either result.
If a vision provider omits only descripcion_detallada but returns non-empty texto_visible, CIS keeps that text as a typed degraded result instead of discarding the complete media activity. The result has descripcion_detallada="", degraded=true, error_code="vision_text_only_fallback" and should_inline=true. Missing both fields remains a schema failure and follows the normal Temporal retry policy.
Flow Output: smoke-test
smoke-test is a runtime/model health check. Product consumers should not use it for parent-facing state.
Executable fixture: tests/fixtures/contracts/output_smoke_test.json.
{
"output": {
"smoke_test": {
"ok": true,
"summary": "Runtime recibio datos y respondio correctamente."
}
}
}
model_outputs contains one primary entry:
{"step_id": "smoke_test", "model_id": "google/gemini-3.5-flash", "payload": {"ok": true, "summary": "..."}}
Flow Output: initial-ingestion-report
initial-ingestion-report returns one parent report plus one chat summary and one memory update decision set per rendered conversation document.
Executable fixture: tests/fixtures/contracts/output_initial_ingestion_report.json.
{
"output": {
"report": {
"usage_stats": {
"status": "provided|missing",
"groups_most_messages": [],
"groups_most_child_participation_pct": [],
"direct_most_messages": [],
"direct_most_view_once_media": [],
"groups_most_view_once_media": [],
"night_activity": {"summary": "string", "evidence": "string"}
},
"wellbeing_analysis": "string",
"positive_signals": ["string"],
"parent_attention_points": ["string"],
"categories": [
{"id": "bullying", "status": "✅|👀|⚠️", "summary": "string"}
],
"markdown": "string"
},
"chat_id_summaries": {
"chat-1": {
"chat_id": "chat-1",
"chat_name": "1er ano",
"monitored_name": "Felipe",
"assumed_relation": "compania de colegio",
"confidence_pct": 80,
"topics_summary": "string",
"recent_topics": ["string"],
"cross_chat_context": "string"
}
},
"chat_id_memory_updates": {
"chat-1": {
"short_term_memory_recent_topics": {
"should_update": true,
"reason": "Hay temas recientes suficientes para inicializar memoria.",
"next_value": {}
},
"long_term_memory_relationship_and_others": {
"should_update": true,
"reason": "La relacion inicial puede describirse con evidencia del historial.",
"next_value": {}
}
}
}
}
}
Rules:
output.reportis the parent-facing report object.output.chat_id_summariesis keyed by chat ids present in the request.output.chat_id_memory_updatesis required and keyed by chat ids present in- The model-facing
chat_id_summarizationresponse uses arrays - Runtime may execute the logical
chat_id_summarizationstep with batching - Runtime verifies exact
chat_idset equality in both model-facing arrays - Single-chat calls retry up to
CHAT_ID_SUMMARIZATION_MAX_RETRIES(default 2). - OpenRouter's UI
Input tokensvalue counts system prompt, user prompt, categoriesshould include every id inconfig/taxonomies/risk-v1.yaml.- Category status values are defined by
prompts/initial_report.md. usage_statsshould only summarizepayload.user_stats_metadatawhen Corereport.markdownis a mobile-first, one-column progressive reading. With richEstadísticas durasuses onlypayload.user_stats_metadata. Exact v1 remains- These presentation rules add depth inside the existing
markdownstring, sin - For memory update decisions,
should_update=falserequires - When
next_valueis updated, it should use prosa descriptiva, no arrays de tags. - En el slot
long_term_memory_relationship_and_others,summarydescribe de forma
Each summary includes chat_name, the visible chat name used in prompts (group subject, direct contact/display name, or a legible fallback).
the request.
chat_id_summaries[] and chat_id_memory_updates[] with explicit chat_id fields so strict JSON Schema can avoid dynamic keys. The response builder normalizes those arrays to the public dicts shown above. Runtime temporarily accepts legacy dict payloads in tests/mocks.
interno, controlled by CHAT_ID_SUMMARIZATION_BATCH_SIZE (default 1 chat document per model call) and CHAT_ID_SUMMARIZATION_BATCH_MAX_PROMPT_CHARS (default 30000 estimated prompt characters). An acumulador en memoria merges all batch outputs and the contrato publico no cambia: Core still receives dicts keyed by chat_id.
before merging a batch. Mismatched multi-chat batches are split into single-chat retries.
If the model still emits one invented summary id and one invented memory id, runtime normalizes both ids to the requested chat id and records chat_id_verification.status=normalized_after_retries in model_outputs. Non-unequivocal mismatches fail with chat_id_summarization_chat_id_mismatch.
rendered conversations and strict JSON Schema. max_tokens limita output completion only; it is not an input-context cap. Request previews expose prompt_chars and an estimated input token count, while Langfuse/log metadata records provider usage when returned.
provides it; otherwise the model should mark it as missing.
evidence it targets 900 a 1500 palabras; sparse inputs are shorter instead of padded. It starts with Si leés una sola cosa, keeps the essential narrative first, then renders Estadísticas duras, the Semáforo completo with las 18 categorías, follow-up questions, a conversation starter and the scope note.
compatible during rollout; strict wardian-user-stats.v2 is the current contract and includes reconciled global/per-chat counters for every active chat. CIS does not backfill from conversation-document headers, calculate missing aggregates, or turn usage statistics into risk evidence. The public output schema is unchanged and may summarize at most five relevant chats; absent metadata produces a short availability note.
cambiar la forma JSON or any public input/output key.
next_value=null; should_update=true requires next_value as an object.
Recommended shape: {"summary": "En este chat se viene hablando de...", "active_threads": ["El tema principal reciente es..., con evidencia en..."], "relationship_context": "La dinamica parece ser...", "open_questions": ["Conviene seguir mirando si..."], "last_observed_at": "YYYY-MM-DD"}.
durable de que se trata el chat (su naturaleza) y relationship_context el tipo de relacion con un vocabulario guiado en prosa (familiar directo como madre o padre, compañeros de curso, equipo o club, amistad, contacto no agendado, etc.). Esta memoria viaja a runs futuros como contexto; no cambia el contrato de claves.
Primary model outputs:
[
{"step_id": "initial_report", "model_id": "google/gemini-3.5-flash", "payload": {"report": {}}},
{
"step_id": "chat_id_summarization",
"model_id": "google/gemini-3.5-flash",
"batch_index": 1,
"batch_total": 3,
"chat_ids": ["chat-1"],
"is_retry": false,
"payload": {"chat_id_summaries": [], "chat_id_memory_updates": []}
}
]
Flow Output: daily-summary-and-notifications-report
daily-summary-and-notifications-report returns one compact daily summary, 1 to 5 parent-facing notifications and explicit memory update proposals for Core.
Executable fixture: tests/fixtures/contracts/output_daily_summary_and_notifications_report.json.
{
"output": {
"daily_summary": {
"date": "2026-06-18",
"summary": "string",
"taxonomy_status": [
{"id": "bullying", "status": "✅|👀|⚠️", "summary": "string"}
],
"relevant_events": []
},
"notifications": [
{
"icon": "🟢|🟡|🔴|🔵|💛",
"kind": "señal_positiva|para_observar|riesgo|cambio_relevante|sin_novedad_relevante|momento_wardian",
"title": "string",
"area": "string",
"taxonomy_ids": ["bullying"],
"chat_ids": ["chat-1"],
"what_happened": "string",
"why_it_matters": "string",
"suggested_action": "string",
"markdown": "string"
}
],
"chat_id_memory_updates": {
"chat-1": {
"short_term_memory_recent_topics": {
"should_update": true,
"reason": "string",
"next_value": {}
},
"long_term_memory_relationship_and_others": {
"should_update": false,
"reason": "string",
"next_value": null
}
}
}
}
}
Rules:
- Runtime validates
daily_summaryis an object. - If
payload.user_stats_metadatais provided, the prompt may use it inside - Runtime validates
notificationslist length: minimum 1, maximum 5. - Runtime rejects
notifications[].priority. - Runtime validates
notifications[].chat_idsreference request chat ids. - Model-facing
chat_id_memory_updatesis required as an array with explicit - Memory updates follow the shared memory update contract:
- When
next_valueis updated, it should use prosa descriptiva, no arrays de tags.
existing fields such as daily_summary.summary, daily_summary.taxonomy_status[].summary or daily_summary.relevant_events. Notifications do not get a dedicated stats-only output section, and stats alone should not create a parent-facing notification except for the explicit verified/unverified zero paths documented below. Coverage uncertainty creates only the operational yellow copy; it is not risk evidence.
chat_id fields so strict JSON Schema can avoid dynamic keys. The public flow output shown above is still a dict keyed by chat_id; the response builder performs that normalization and temporarily accepts legacy dict payloads in tests/mocks.
should_update=false requires next_value=null; should_update=true requires next_value as an object.
Recommended shape: {"summary": "En este chat se viene hablando de...", "active_threads": ["El tema principal reciente es..., con evidencia en..."], "relationship_context": "La dinamica parece ser...", "open_questions": ["Conviene seguir mirando si..."], "last_observed_at": "YYYY-MM-DD"}.
With negotiated conversation_memory.v2 in enforce, Daily also returns output.conversation_memory_ack. It is generated after identity abstention has removed disallowed updates and before content-trust, audit-summary, output digest and result serialization. The v2-to-legacy prompt projection includes only non-abstained chats with message evidence; prior memory for an abstained chat is never restored from the canonical snapshot. Model output must contain exactly one memory decision object for every eligible chat, and no others:
{
"contract_version": "conversation_memory.v2",
"canonicalization_version": "wardian-json-c14n.v1",
"updates": [
{
"chat_id": "chat-synthetic",
"slot_key": "short_term_memory_recent_topics",
"base_version": 7,
"should_update": true,
"reason": "Apareció un tema escolar reciente.",
"slot_value": {},
"canonical_digest": "64 lowercase hex characters",
"last_observed_at": "2026-08-14T18:05:00Z",
"evidence_through": "2026-08-15T00:00:00Z"
}
]
}
reason is bounded to 120 characters. should_update is true exactly when slot_value is non-null. Missing slots use base_version=0; no-op entries have a null value and the canonical SHA-256 of JSON null. A no-op for an existing slot preserves both current watermarks, so an older Daily can be classified as stale without constructing a regressing pair. An empty ACK is valid for deterministic no-activity Daily. last_observed_at is the latest actual message for that chat, while evidence_through is the Daily evidence/window boundary; they are not interchangeable. Control Plane classifies CAS application as applied, conflict, stale or no-op without aborting the completed report. The ACK contains at most 1024 updates: two slots for each of the at most 512 Daily chats accepted when memory v2 is negotiated. Observe computes/logs only the bounded ACK digest and count; it does not add the ACK to output. Legacy jobs keep the exact pre-v2 output and digest.
Deterministic no-activity output
Daily and InterWeek classify supported stats as none | verified_zero | unverified_zero | planned_pause_full | planned_pause_partial before Temporal/media:
- Exact V1 root JSON integer
total_messages: 0remains temporary N-1 legacy - Valid V2
totals.activity_events: 0plus - The same V2 zero plus
status: unverifiedisunverified_zero. - V2 zero plus
status: intentionally_excludedisplanned_pause_full; the - V2 zero plus
status: intentionally_partialisplanned_pause_partial. unverifiedremains authoritative wheneverplanned_pauseis combined with- V2 non-zero is
noneand follows the existing activity/model path after
verified_zero behavior.
source_coverage.status: verified is verified_zero.
excluded intervals must cover the full window.
a real operational reason such as session_gap.
contract validation. Initial ingestion never activates either zero path.
V2 canonical window, payload metadata and source_coverage.window must describe the same half-open window and IANA timezone, representing the same parsed UTC instants including across daylight-saving folds. Malformed V2, unsupported explicit stats versions, V1 carrying coverage, conflicting conversations, InterWeek period summaries/notifications, sibling counters/breakdowns or top-activity collections fail closed during request normalization, before Temporal media fan-out. source_coverage is technical control data: it is removed from model-facing stats in Initial, Daily and InterWeek (including previews and debug_plaintext) and never becomes risk evidence.
The verified_zero parent-facing body remains deterministic:
Wardian no registró actividad para {primer nombre útil o etiqueta genérica} durante {fecha o período}. No hay conversaciones, señales ni alertas nuevas para resumir. No hace falta realizar ninguna acción; el monitoreo continúa normalmente.
The monitored display name uses the first token only when it is an unambiguous given name; an explicit label such as Menor: may introduce that token. If a title, role, preposition, initial or unlabeled placeholder precedes another token, the copy falls back to el monitoreo rather than risk exposing a surname. Phone/JID-like or otherwise unusable values use the same fallback.
- Verified Daily returns the full existing shape, one
sin_novedad_relevante - Verified InterWeek returns the full existing
interweek_reportshape with every - Verified Markdown starts with
## Sin actividad registrada.
notification, empty taxonomy/events, and chat_id_memory_updates: {}. Its date is the local date of window_start.
analytical list empty. period_start is the local start date and period_end is the local calendar date containing the instant immediately before exclusive window_end.
The unverified_zero copy is unequivocal and does not invent risk evidence:
Wardian no pudo confirmar cobertura completa; esto no demuestra que no haya habido actividad.
- Unverified Daily returns one yellow
para_observarnotification titled - Unverified InterWeek keeps every analytical list empty. Both yellow Markdown
- Neither zero path updates memory, invokes a primary/shadow model, processes
Actividad no verificable, with empty taxonomy_ids, chat_ids, daily_summary.taxonomy_status and relevant_events.
fields start with ## 🟡 Actividad no verificable.
media or returns conversation documents. The public Daily/InterWeek schemas and Kafka envelopes remain unchanged.
Planned-pause output is also deterministic and never represents the interval as verified silence or risk evidence:
- Full Daily returns one required blue informational
cambio_relevanteitem so - Full InterWeek keeps every analytical list empty.
- A partial report with activity analyzes only conversations outside the
- Mixed planned pause plus an operational gap keeps the yellow
- Full and zero-partial paths invoke no model, shadow, media, memory update or
the existing 1..5 notification contract remains intact. It has empty taxonomy_ids and chat_ids, plus empty taxonomy/events/memory updates.
excluded intervals. After model validation, CIS prepends a fixed coverage disclosure to daily_summary.summary, or to InterWeek quick_read and markdown; coverage metadata itself never reaches the model or evidence.
Actividad no verificable behavior and adds only the deterministic coverage disclosure.
risk notification.
Flow Output: interweek-report
interweek-report synthesizes the last 3/4/x days from explicit Core context. It does not require raw conversations and does not update per-chat memory in v1.
Executable fixture: tests/fixtures/contracts/output_interweek_report.json.
{
"output": {
"interweek_report": {
"period_start": "2026-06-15",
"period_end": "2026-06-18",
"quick_read": "string",
"taxonomy_semaphore": [
{"id": "bullying", "status": "✅|👀|⚠️", "summary": "string"}
],
"what_changed": [],
"key_findings": [],
"positive_signals": [],
"watch_points": [],
"context_chats": [],
"suggested_parent_action": [],
"markdown": "string"
}
}
}
Rules:
- Input evidence should come from
payload.context.previous_daily_summaries, - Optional
payload.user_stats_metadatacan inform existing fields such as - Runtime validates the top-level
interweek_reportobject. - No
chat_id_memory_updatesare returned by this flow in v1. conversation_memory.v2is Daily-only; InterWeek remains read-only and emits
payload.context.previous_notifications, payload.context.previous_interweek_reports, payload.context.memories_by_chat_id and payload.context.monitored_profile.
interweek_report.quick_read, interweek_report.key_findings, interweek_report.watch_points and interweek_report.markdown; no separate stats output object is added. Supported exact zero instead uses the deterministic path above; source_coverage itself is never model context.
neither memory updates nor an ACK. A stale proposal converges in the next ordinary Daily; CIS does not create a memory-only job.
Broker Result Envelope
content-intelligence.flow-results.v1
Kafka publishes FlowResultEnvelope to content-intelligence.flow-results.v1:
{
"contract_version": "content-intelligence.flow-results.v1",
"request_id": "request-1",
"tenant_id": "tenant-1",
"run_id": "uuid",
"status": "completed",
"result": {
"run_id": "uuid",
"flow": "daily-summary-and-notifications-report",
"recipe": "daily-summary-and-notifications-report-text-only",
"status": "completed",
"output": {}
},
"trace_id": "trace-1",
"generated_at": "2026-06-16T12:00:00Z",
"metadata": {
"degraded": false,
"media_failure_count": 0,
"diagnostic_event_count": 0
}
}
For status="completed", the envelope carries exactly one of:
result: inlineFlowRunResult.result_ref: object-store reference to the completeFlowRunResult.
Oversized completed results can use result_ref when BROKER_KAFKA_RESULT_REFS_ENABLED=true:
{
"contract_version": "content-intelligence.flow-results.v1",
"request_id": "request-1",
"tenant_id": "tenant-1",
"run_id": "uuid",
"status": "completed",
"result_ref": {
"source": "object_store",
"bucket": "wardian-flow-artifacts",
"object_key": "flow-artifacts/flow_result/2026/07/03/request-1-deadbeefdeadbeef.json.gz",
"content_type": "application/json",
"compression": "gzip",
"size_bytes": 123456,
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"artifact_kind": "flow_result"
},
"trace_id": "trace-1",
"generated_at": "2026-06-16T12:00:00Z",
"metadata": {
"original_payload_bytes": 1200000,
"omitted_payload": true
}
}
Consumers must resolve result_ref, verify sha256, decompress gzip, parse the JSON object as FlowRunResult, and then read the product output from FlowRunResult.output exactly as they do for inline result.
content-intelligence.flow-results.v4
V4 matches flow-run v4 and adds the CIS-owned privacy attestation. A completed v4 result always includes privacy_decision; a privacy admission rejection includes the same decision with decision="rejected", a stable reason_code, and no workflow/model execution:
{
"contract_version": "content-intelligence.flow-results.v4",
"request_id": "request-1",
"tenant_id": "tenant-1",
"run_id": "uuid",
"status": "completed",
"result": {},
"privacy_decision": {
"contract_version": "wardian-cis-privacy-decision.v1",
"requested_mode": "protected",
"effective_mode": "protected",
"decision": "accepted_exact",
"reason_code": null,
"policy_version": "wardian-cis-privacy-policy.v1",
"capabilities_digest": "sha256:<frozen-digest>",
"input_protection_mode": "masked"
}
}
Control Plane must compare the attestation with its frozen request instead of inferring effective privacy from settings.privacy_mode, deployment labels, or the current capabilities response. The decision is operational evidence only; it does not assert legal compliance. V1-v3 results carry no such attestation.
content-intelligence.flow-results.v5
V5 preserves the v4 privacy_decision, echoes the producer-owned intent fingerprint, and requires result.content_trust_assessment on every completed inline result. The same requirement applies after resolving result_ref. observe reports a typed decision without withholding delivery; enforce routes evaluator outages, invalid responses, oversize inputs, and suspicious outputs to review or block according to the pinned policy.
When optional media processing fails, the broker still publishes status="completed" if the report pipeline completed. The failed media item is included in result.media_processed with status="activity_failed" and a sanitized error; consumers can render the report normally and use metadata.degraded=true for operational visibility.
Media intentionally disabled through the recipe or FlowRunSettings.media_policy is not an activity failure: CIS does not schedule that activity, does not add an activity_failed item to media_processed, and does not publish a degradation diagnostic for the omission. The conversation remains available to the text pipeline with the existing unprocessed-media evidence boundary.
An audio/video file that is downloadable but contains no audio stream uses this stable non-retryable degradation result:
{
"message_id": "m1",
"media_type": "audio",
"status": "activity_failed",
"activity": "transcribe_audio",
"error": "no_audio_track",
"error_code": "no_audio_track",
"should_inline": false
}
The Activity itself completes successfully, emits no model trace or cost, and does not call Groq. This is distinct from audio_probe_failed:*, which is a technical exception eligible for Temporal retry and only becomes a generic failed media item if all attempts fail.
Failure envelope:
{
"contract_version": "content-intelligence.flow-results.v1",
"request_id": "request-1",
"run_id": "uuid",
"status": "failed",
"error": "flow_failed"
}
result and result_ref are omitted from failed broker messages because broker publishing uses exclude_none=True; result_ref is never valid for failed envelopes.
content-intelligence.flow-diagnostics.v1
Kafka publishes FlowDiagnosticEnvelope to content-intelligence.flow-diagnostics.v1 for valid runs that were degraded or failed:
{
"contract_version": "content-intelligence.flow-diagnostics.v1",
"event_type": "media_activity_failed",
"severity": "warning",
"request_id": "request-1",
"tenant_id": "tenant-1",
"run_id": "uuid",
"flow": "daily-summary-and-notifications-report",
"recipe": "daily-summary-and-notifications-report-all-media",
"message_id": "m1",
"media_type": "audio",
"activity": "transcribe_audio",
"error_code": "audio_download_failed",
"error": "audio_download_failed:<redacted-url>",
"created_at": "2026-06-16T12:00:00Z"
}
Diagnostic publish is best-effort and never replaces the main result envelope. Invalid JSON and invalid broker envelopes go only to the flow-runs DLQ. A well-formed envelope whose payload_ref is permanently invalid also publishes a flow_run_failed diagnostic after its terminal failed FlowResultEnvelope, so CP can close the correlated request without a Temporal run.
Diagnostic error values are sanitized and bounded. Sanitization redacts base64 data URIs regardless of payload length, URLs, WhatsApp identifiers, phone numbers, email addresses, JWTs, bearer/basic credentials, password and database URL assignments, URLs containing userinfo, common API keys/secrets/tokens, and long opaque tokens. error_code values are normalized to the same safe code format even when an activity supplies an explicit code; safe existing codes such as audio_download_failed remain unchanged.
Consumer Guidance
- Product consumers should read
FlowRunResult.output. - Treat
model_outputs,shadow_outputs,conversation_documentsand - Do not store raw media bytes; they are never part of these output contracts.
- Do not treat shadow outputs as canonical.
- Do not infer state across runs from CIS output; Core owns persistence and
media_processed as debug/audit surfaces unless a product contract explicitly says otherwise.
product state.
FlowRunResult.input_diagnostics.temporal_evidence is an additive sanitized copy of the input temporal summary. It contains only versions, mode, aggregate counts and enum count maps. It never contains content, message/chat IDs, JIDs or raw/exact timestamps.
Raw WAHA results also expose FlowRunResult.input_diagnostics.waha_compatibility. The block contains only the versioned adapter stage plus bounded outcome/family count maps. A raw batch with no analyzable messages returns the same block in the HTTP error detail or, for broker v3, under FlowResultEnvelope.metadata.input_diagnostics, so decode and unsupported evidence survives terminal normalization failure. These diagnostics never include event names, content, IDs/JIDs, URLs or timestamps.