mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-02 21:34:30 +08:00
609 lines
26 KiB
Markdown
609 lines
26 KiB
Markdown
# DC Pipeline Contract
|
|
|
|
The DC pipeline for a single Agentforce session runs as three stages in
|
|
sequence:
|
|
|
|
```
|
|
fetch_dc.py → 24 dc.<name>.json artifacts + dc._session_manifest.json
|
|
assemble_dc.py → dc._session_tree.json (pure in-memory join)
|
|
render_dc.py → dc._session_summary.md (pure tree reader)
|
|
```
|
|
|
|
Each stage is independently runnable. `fetch_dc.py` chains the two
|
|
downstream stages by default; `--no-assemble` / `--no-render` opt out of
|
|
each. The renderer can also run standalone against a tree produced by a
|
|
prior invocation.
|
|
|
|
This document owns the shape + invariants of stages 2 and 3. For per-DMO
|
|
field reference of what stage 1 produces, see
|
|
[`dc_dmo_fields.md`](./dc_dmo_fields.md).
|
|
|
|
---
|
|
|
|
## 1. Pipeline overview
|
|
|
|
| Stage | Script | Input | Output | Scope |
|
|
|---|---|---|---|---|
|
|
| Fetch | `scripts/fetch_dc.py` | org + session id | 24 `dc.<name>.json` + `dc._session_manifest.json` | DC Query REST API, 5-wave waterfall |
|
|
| Assemble | `scripts/assemble_dc.py` | fetch outputs | `dc._session_tree.json` | pure in-memory join, no fetches |
|
|
| Render | `scripts/render_dc.py` | tree + manifest | `dc._session_summary.md` | pure tree reader, no DMO loads |
|
|
|
|
Nothing downstream of fetch issues DC queries. Everything after fetch is
|
|
pure compute over files on disk, which means the renderer can iterate
|
|
on markdown format without re-fetching.
|
|
|
|
---
|
|
|
|
## 2. Assembly stage
|
|
|
|
`scripts/assemble_dc.py` joins the fetched DC rows into a hierarchical
|
|
tree.
|
|
|
|
### 2.1 Purpose + scope
|
|
|
|
**Inputs:** 24 `dc.<name>.json` artifacts + `dc._session_manifest.json`
|
|
produced by `scripts/fetch_dc.py`, under `DATA_ROOT/<sid>/`.
|
|
|
|
**Output:** `dc._session_tree.json` — session-rooted hierarchical view.
|
|
Interaction → Step → Generation → GatewayRequest; audit rows (Tag,
|
|
ObjRecord, Metadata, LLM, Quality, Category, Feedback, FeedbackDetail)
|
|
nested under the right parent.
|
|
|
|
**Contract:** pure in-memory compute, no new DMO queries, forward-only
|
|
joins on already-fetched rows. Chain-orphan GW calls fall through to a
|
|
timestamp-window rule with an explicit `binding_method` flag.
|
|
|
|
### 2.2 Inputs
|
|
|
|
The assembler does **not** hard-code the 24 template names. It reads
|
|
`dc._session_manifest.json` first, then iterates
|
|
`manifest["queries"][*]["name"]` to populate the row map. When
|
|
`fetch_dc.py` adds a 25th template, the row loader picks it up
|
|
automatically — the tree logic only references names it knows how to
|
|
place, so a new DMO is loaded but not rendered until the assembler is
|
|
taught what to do with it.
|
|
|
|
Per-field schema for every DMO lives in
|
|
[`dc_dmo_fields.md`](./dc_dmo_fields.md).
|
|
|
|
### 2.3 Declared binding chain
|
|
|
|
The "normal" path that nests a `GatewayRequest` under its owning
|
|
`LLM_STEP` in the tree. Every edge is a platform-sanctioned forward FK:
|
|
|
|
```
|
|
Step.ssot__GenerationId__c → Generation.generationId__c
|
|
Generation.generationResponseId__c → GatewayResponse.generationResponseId__c
|
|
GatewayResponse.generationRequestId__c → GatewayRequest.gatewayRequestId__c
|
|
```
|
|
|
|
**Which Step types populate `ssot__GenerationId__c`?** Only `LLM_STEP`
|
|
(verified live). `ACTION_STEP`, `TOPIC_STEP`, `TRUST_GUARDRAILS_STEP`,
|
|
and `SESSION_END` always have `NOT_SET` here; the chain never resolves
|
|
for them.
|
|
|
|
**Expected declared-share on typical Agentforce sessions:** minority.
|
|
Most gateway calls on a real session land via the timestamp-window
|
|
fallback, not the declared chain. Sessions that rely on Flow-invoked
|
|
prompt-template actions produce zero declared bindings and many
|
|
timestamp_window bindings — normal, not a defect.
|
|
|
|
### 2.4 Timestamp-window fallback
|
|
|
|
Any `GatewayRequest` not reached via the declared chain is placed by
|
|
timestamp containment.
|
|
|
|
**Window semantics:**
|
|
- **Closed-closed:** `start_ts <= gw_req.timestamp__c <= end_ts`.
|
|
- **Null end_ts** (active session / interaction / step) is treated as
|
|
`+∞`.
|
|
- **Missing start_ts**: fall through to the next preference tier; never
|
|
match on start alone.
|
|
- Boundary ties bind to the earlier parent (Step) in preference to the
|
|
later one (Interaction).
|
|
|
|
**Tier dominates containment.** Selection order is:
|
|
|
|
1. Find all Step windows containing the GW timestamp.
|
|
2. Restrict to the highest-preference tier present: `ACTION_STEP` →
|
|
`TOPIC_STEP` → `TRUST_GUARDRAILS_STEP` → any other (catch-all:
|
|
LLM_STEP without declared binding, SESSION_END, unknown types —
|
|
never excluded).
|
|
3. Among those, pick the innermost (shortest-window) Step.
|
|
4. On exact window-size ties, pick the latest `start_ts`.
|
|
|
|
Example: if both a wide `ACTION_STEP` and a narrower nested `TOPIC_STEP`
|
|
contain the GW timestamp, the `ACTION_STEP` wins because its tier
|
|
dominates — even though TOPIC is innermost.
|
|
|
|
**Placement fallbacks:**
|
|
- If no Step window matches, attach to the enclosing `Interaction` as a
|
|
`timestamp_bound_gateway_calls[]` entry (`bound_to_step_id` is null).
|
|
- If no Interaction window matches, land in
|
|
`session.unbound_gateway_calls[]`.
|
|
|
|
**Never declared-bound twice.** A Step that already owns a GW via the
|
|
declared chain is excluded from timestamp-window candidates.
|
|
|
|
### 2.5 `binding_method` enum
|
|
|
|
Every GatewayRequest in the tree carries exactly one of these:
|
|
|
|
| Value | When emitted |
|
|
|---|---|
|
|
| `declared` | Placed under a Step via the declared chain in §2.3. |
|
|
| `timestamp_window` | Placed by §2.4 (under a Step or an Interaction). |
|
|
| `unbound` | Neither chain nor window matched; landed in `session.unbound_gateway_calls[]`. |
|
|
|
|
**Collision handling.** When two Steps' Generation chains resolve to
|
|
the same GatewayRequest (`Step.ssot__GenerationId__c` collisions at the
|
|
upstream DMO), first-step-wins: the earlier Step in walk order claims
|
|
the GW. Subsequent Steps get `"gateway_request": null` with an adjacent
|
|
`"gateway_request_collision": true` marker on the Step view, so readers
|
|
can see why the GW is absent. The session-level
|
|
`counts.gw_binding.declared_collisions` integer tracks the aggregate.
|
|
On observed data this is 0; non-zero values indicate a real defect in
|
|
the upstream DMO writers.
|
|
|
|
### 2.6 Moment placement
|
|
|
|
`session.moments[]` lives at the root of the session, not nested under
|
|
`Interaction`. Each moment carries `interaction_ids[]`, a sorted list
|
|
derived from `MomentInteraction` (the junction table). Rationale: the
|
|
schema supports many-to-many (one moment can span multiple
|
|
interactions; one interaction can belong to multiple moments), even
|
|
though observed live data is 1:N. Nesting under Interaction would
|
|
duplicate rows.
|
|
|
|
Moments are absent on orgs without Agent Optimization enabled; the
|
|
assembler never requires their presence.
|
|
|
|
### 2.7 1:1 audit chain invariant
|
|
|
|
For every `GatewayRequest` in the session, exactly one
|
|
`GatewayResponse` exists (in live data, modulo in-flight calls at
|
|
fetch time). `counts.audit_chain_1to1_ok` is `true` iff
|
|
`len(gateway_requests) == len(gateway_responses)`. Drift is flagged in
|
|
the counts block but does not crash the assembler — a caller
|
|
re-fetching during an active session can still produce a partial tree.
|
|
|
|
Note: this is Req↔Resp 1:1, not Req↔LLM_STEP. The typical session has
|
|
more GatewayRequests than LLM_STEPs because prompt-template /
|
|
plannerservice calls emit GatewayRequest rows without owning Step rows.
|
|
|
|
### 2.8 `session_shape` enum
|
|
|
|
Lifted verbatim from `dc._session_manifest.json.session_shape` into
|
|
`dc._session_tree.json counts.session_shape`. Six values, classified
|
|
by `fetch_dc.py` after the 5-wave waterfall; rules evaluated
|
|
top-to-bottom, first match wins:
|
|
|
|
| Value | Rule | Added |
|
|
|---|---|---|
|
|
| `session_not_found` | `sessions.json` returned 0 rows (bad sid or STDM not yet materialized) | original |
|
|
| `interactions_not_materialized_yet` | sessions row present, but interactions/steps/messages all 0 (fresh session — STDM hierarchy hasn't materialized yet, but gateway_requests may exist) — assembler takes the gateway-direct path; see §2.9 | |
|
|
| `gateway_requests_dropped_by_stdm` | DC `gateway_requests == 0` while runtime telemetry outside Data Cloud confirms ≥1 LLM call for the session — i.e. the STDM exporter has dropped writes the platform did emit. Distinct from `planner_ran_no_gateway_logs` (which means logging disabled at the source). This skill cannot disambiguate this state by itself; the operator must check runtime telemetry to reclassify. | |
|
|
| `abandoned_before_llm` | steps > 0, LLM_STEP count == 0, gw_reqs == 0 (user typed, planner didn't reach an LLM call) | original |
|
|
| `planner_ran_no_gateway_logs` | LLM_STEP > 0 AND steps_with_generation_id > 0 AND gw_reqs == 0 (Trust Layer gateway logging disabled; extra guard prevents misclassification from broken generations-IN clauses) | original |
|
|
| `complete` | everything else (the "normal" bucket, including partial chain-orphan sessions) | original |
|
|
|
|
The enum names only the operator-actionable states. For finer shape,
|
|
read `counts.gw_binding`: if `declared == 0` but `gateway_requests > 0`,
|
|
the session relied entirely on prompt-template / Flow-invoked LLM
|
|
calls — normal but worth noticing.
|
|
|
|
`gateway_requests_dropped_by_stdm` is the only shape that this skill
|
|
cannot definitively classify on its own — disambiguating from
|
|
`planner_ran_no_gateway_logs` requires a runtime telemetry probe
|
|
outside Data Cloud. In this skill the session is reported as
|
|
`planner_ran_no_gateway_logs`; the operator with access to the
|
|
platform's runtime LLM-gateway telemetry can check for a matching
|
|
gateway-call event to distinguish a real STDM exporter defect from
|
|
"logging genuinely disabled at the source."
|
|
|
|
### 2.9 Output: `dc._session_tree.json` schema
|
|
|
|
Root shape:
|
|
|
|
```jsonc
|
|
{
|
|
"session": {
|
|
"id": "<sid>",
|
|
"_schema_version": 1, // see §2.9b
|
|
"identity": { ... }, // see §2.9a
|
|
"org": { "alias": "...", "instance_url": "..." },
|
|
"start_ts": "...",
|
|
"end_ts": null, // null while active
|
|
"end_type": "Completed | Abandoned | ...",
|
|
"channel": "...",
|
|
"participants": [
|
|
{ "participant_id": "...", "role": "USER | AGENT",
|
|
"agent_api_name": "<null for USER>",
|
|
"agent_version": "...", "agent_type": "..." }
|
|
],
|
|
"moments": [
|
|
{ "moment_id": "...",
|
|
"agent_api_name": "...", "agent_version": "...",
|
|
"request_summary_text": "...", "response_summary_text": "...",
|
|
"interaction_ids": ["..."], // [] if no bridging rows; never omitted
|
|
"start_ts": "...", "end_ts": "...",
|
|
"tag_associations": [ ... ] // moment-scope only
|
|
}
|
|
],
|
|
"interactions": [
|
|
{
|
|
"id": "...", "type": "TURN | SESSION_END", "topic": "...",
|
|
"trace_id": "...", // primary column or extracted from AttributeText
|
|
"start_ts": "...", "end_ts": "...",
|
|
"messages": [
|
|
{ "message_id": "...", "type": "Input | Output",
|
|
"role": "USER | AGENT", // derived via participant join
|
|
"participant_id": "...", "text": "...",
|
|
"content_type": "...", "modality": "...", "ts": "..." }
|
|
],
|
|
"telemetry_spans": [ ... ], // usually []
|
|
"steps": [
|
|
{
|
|
"id": "...",
|
|
"type": "LLM_STEP | ACTION_STEP | TOPIC_STEP | TRUST_GUARDRAILS_STEP | SESSION_END",
|
|
"name": "...",
|
|
"start_ts": "...", "end_ts": "...",
|
|
"error_text": null, // NOT_SET → null
|
|
"generation": { // present iff ssot__GenerationId__c != NOT_SET
|
|
"generation_id": "...",
|
|
"response_id": "...",
|
|
"response_text": "...", "masked_response_text": "...",
|
|
"feature": "...",
|
|
"quality": [ ... ], // ContentQuality rows parented on generationId__c
|
|
"categories": [ ... ], // ContentCategory rows parented on generationId__c
|
|
"feedback": [
|
|
{ "feedback_id": "...", "feedback": "UP | DOWN", "action": "...",
|
|
"details": [ ... ], "records": [ ... ] }
|
|
]
|
|
},
|
|
"gateway_request": { // present only when declared chain resolves
|
|
"binding_method": "declared",
|
|
"gateway_request_id": "...",
|
|
"feature": "...", "model": "...", "provider": "...",
|
|
"prompt_template_dev_name": "...", // "prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0,
|
|
"prompt_text": "...", // raw input prompt
|
|
"response": { ... },
|
|
"tags": [ ... ], "records": [ ... ],
|
|
"metadata": [ ... ], "llm": []
|
|
},
|
|
"gateway_request_collision": true // present only when a prior step
|
|
// claimed this step's declared GW;
|
|
// gateway_request is null in this case
|
|
}
|
|
],
|
|
"timestamp_bound_gateway_calls": [ // chain-orphans placed by ts-window
|
|
{ "binding_method": "timestamp_window",
|
|
"bound_to_step_id": "...", // null if bound to Interaction, not a Step
|
|
"gateway_request_id": "...", "feature": "...", "model": "...",
|
|
"response": { ... }, "tags": [...], "records": [...],
|
|
"metadata": [...], "llm": [] }
|
|
],
|
|
"tag_associations": [ ... ] // interaction-scope only
|
|
}
|
|
],
|
|
"session_tag_associations": [ ... ], // session-scope only
|
|
"unbound_gateway_calls": [ // chain-orphans with no window match
|
|
{ "binding_method": "unbound", "gateway_request_id": "...", /* ... */ }
|
|
],
|
|
"counts": {
|
|
"interactions_total": 0, "interactions_turn": 0, "interactions_session_end": 0,
|
|
"steps_total": 0,
|
|
"steps_by_type": { "LLM_STEP": 0, "ACTION_STEP": 0, "TOPIC_STEP": 0,
|
|
"TRUST_GUARDRAILS_STEP": 0, "SESSION_END": 0 },
|
|
"generations": 0,
|
|
"gateway_requests": 0, "gateway_responses": 0,
|
|
"gateway_metadata": 0, "gateway_llm": 0,
|
|
"gateway_records_grounded": 0, "gateway_records_feedback": 0,
|
|
"feedback": 0,
|
|
"audit_chain_1to1_ok": true,
|
|
"gw_binding": { "declared": 0, "timestamp_window": 0, "unbound": 0,
|
|
"declared_collisions": 0 },
|
|
"session_shape": "complete | abandoned_before_llm | planner_ran_no_gateway_logs | gateway_requests_dropped_by_stdm | interactions_not_materialized_yet | session_not_found",
|
|
"pk_collisions": [ ], // {dmo, key} records; first-write-wins applied
|
|
"parse_warnings": [ ] // dc.<name>.json files that failed to parse
|
|
}
|
|
},
|
|
"catalog": { // session-filtered, not full org vocabulary
|
|
"agents_observed": [ "..." ],
|
|
"tag_definitions": [ ... ],
|
|
"tag_definition_associations": [ ... ],
|
|
"tags": [ ... ]
|
|
},
|
|
"_doc": "Assembled from DATA_ROOT/<sid>/dc.*.json. See dc._session_manifest.json."
|
|
}
|
|
```
|
|
|
|
**Polymorphic dispatch:**
|
|
- `GenAIGtwyObjRecord.parent__c` — if in `gw_req_by_id` → attached as
|
|
`GatewayRequest.records[]` (grounded attachments); if in
|
|
`feedback_by_id` → attached as `Feedback.records[]`; else silently
|
|
dropped.
|
|
- `GenAIContentCategory.parent__c` — if in `generations_by_id` →
|
|
attached as `Generation.categories[]` (non-TOXICITY detectors); if
|
|
in `quality_by_id` → nested under the Quality row as
|
|
`_toxicity_subcategories[]`; else silently dropped.
|
|
- `GenAIAiAgentTagAssociation` — routed by which of the three scope FKs
|
|
(`SessionId`, `InteractionId`, `MomentId`) is populated. Rows with
|
|
none are silently dropped.
|
|
|
|
**Polymorphic dispatch failures are silent by design.** If these id
|
|
spaces collide in future (they don't today — `gatewayRequestId__c`,
|
|
`feedbackId__c`, `generationId__c`, and `quality.id__c` are all
|
|
distinct UUID namespaces), the first-branch-wins behavior records the
|
|
defect nowhere. Revisit if a real org ever shows a cross-namespace id.
|
|
|
|
#### 2.9a `session.identity` sub-object
|
|
|
|
Harvested from already-fetched rows. Fields that require values absent
|
|
from every observed DMO row come out as `null`. Every string harvested
|
|
from `tagValue__c` is piped through `html.unescape()` → quote-strip →
|
|
`_clean()` to coerce `""`/`NOT_SET`/`UNSET_VALUE` to `None`.
|
|
|
|
| Field | Source DMO | Source column / lookup |
|
|
|---|---|---|
|
|
| `org_id` | gateway_requests | `orgId__c`, first non-null after sort |
|
|
| `platform_user_id` | gateway_requests | `userId__c`, first non-null after sort |
|
|
| `planner_id` | gateway_requests | `plannerId__c`, first non-null after sort |
|
|
| `bot_version_id` | gateway_requests | `botVersionId__c`, first non-null after sort |
|
|
| `app_type` | gateway_requests | `appType__c`, first non-null after sort |
|
|
| `bot_id` | gateway_request_tags | `tagValue__c` where `tag__c == "bot_id"` |
|
|
| `bot_name` | gateway_request_tags | `tagValue__c` where `tag__c == "bot_name"` |
|
|
| `agent_api_name` | gateway_request_tags | `tagValue__c` where `tag__c == "agent_developer_name"` (no fallback; MyAgent has no developer-name tag — look at `bot_name` + `agent_label` instead) |
|
|
| `agent_label` | gateway_request_tags | `tagValue__c` where `tag__c == "agent_label"` |
|
|
| `agent_version` | gateway_request_tags | `tagValue__c` where `tag__c == "agent_version_api_name"`, fallback `"version_api_name"` |
|
|
| `agent_type` | gateway_request_tags | `tagValue__c` where `tag__c == "agent_type"` |
|
|
| `planner_name` | gateway_request_tags | `tagValue__c` where `tag__c == "planner_name"` |
|
|
| `planner_type` | gateway_request_tags | `tagValue__c` where `tag__c == "planner_type"` |
|
|
| `configured_model` | gateway_request_tags | `tagValue__c` where `tag__c == "configured_model_name"` |
|
|
| `messaging_session_id` | sessions[0] | `ssot__RelatedMessagingSessionId__c` |
|
|
| `messaging_end_user_id` | participants | `ssot__ParticipantId__c` on the first USER-role row (sorted by `ssot__Id__c`) |
|
|
| `voice_call_id` | sessions[0] | `ssot__RelatedVoiceCallId__c` |
|
|
| `individual_id` | sessions[0] | `ssot__IndividualId__c` |
|
|
| `bootstrap_variables` | sessions[0] | parsed from `ssot__VariableText__c` — channel-specific bootstrap key→value dict (e.g. `__resolved_locale__`, `__supports_result_display__`); empty `{}` when the column is null/empty/unparseable |
|
|
| `mode` | derived | one of `production_messaging` / `builder_previewer` / `voice` / `unknown` — derived from `(channel, RelatedMessagingSessionId__c, RelatedVoiceCallId__c, bootstrap_variables.keys)`. See `_derive_mode` + `_BUILDER_PREVIEWER_INDICATOR_KEYS` in `assemble_dc.py`. |
|
|
|
|
**Sorting (required for idempotence):**
|
|
- `gateway_requests` — sort by `(timestamp__c, gatewayRequestId__c)`
|
|
ascending, then take first non-null.
|
|
- `gateway_request_tags` — sort by `(parent__c, tag__c, tagValue__c)`
|
|
ascending, then filter by tag name and take first non-null.
|
|
- `participants` — sort by `ssot__Id__c` ascending, then take the first
|
|
USER-role row.
|
|
- `sessions[0]` — by contract, one row per session; no sort needed.
|
|
|
|
**Trimmed fields:** `user_location`, `user_context` are not promoted
|
|
(low debugging value, opaque without a decoder ring).
|
|
|
|
**Minimal tree exclusion.** The session-not-found short-circuit path
|
|
(§2.11) does **not** emit an `identity` sub-object — identity
|
|
harvesters need non-empty gateway rows, which are guaranteed empty in
|
|
this branch. Renderer's minimal branch handles absent identity
|
|
gracefully.
|
|
|
|
#### 2.9b `session._schema_version`
|
|
|
|
Current value: `1`. Emitted on both the full tree and the minimal
|
|
session-not-found tree. Additive changes (new sub-objects, new fields)
|
|
do NOT bump this. Breaking changes (rename or remove a top-level key,
|
|
change the shape of `interactions[]`, etc.) bump it.
|
|
|
|
Renderer refuses to render a tree with an incompatible version; see
|
|
§3.10.
|
|
|
|
### 2.10 Idempotence
|
|
|
|
Running the assembler N times on the same `DATA_ROOT/<sid>/` produces
|
|
a byte-identical `dc._session_tree.json`. Rules:
|
|
|
|
- Arrays sort by source timestamp ascending; on ties, by primary-key
|
|
string.
|
|
- Any set-derived array (`agents_observed`, `interaction_ids[]`, etc.)
|
|
is emitted via `sorted(...)` before serialization.
|
|
- `json.dumps(..., sort_keys=True)` for all dict output.
|
|
- Identity harvest uses the explicit sort keys in §2.9a for
|
|
first-non-null picks.
|
|
- Verified on the live test fixture: `diff` of two runs returns empty.
|
|
|
|
### 2.11 Session-not-found short-circuit
|
|
|
|
If `sessions.json` returned 0 rows, the assembler writes a minimal
|
|
tree with only `session.id`, `session.org`,
|
|
`session._schema_version: 1`, and
|
|
`counts.session_shape = "session_not_found"`. No `identity`, no
|
|
`interactions[]`, no `catalog{}`.
|
|
|
|
---
|
|
|
|
## 3. Render stage
|
|
|
|
`scripts/render_dc.py` turns `dc._session_tree.json` into a human
|
|
markdown file.
|
|
|
|
### 3.1 Purpose + scope
|
|
|
|
**Input:** `dc._session_tree.json` + `dc._session_manifest.json` (the
|
|
latter only for the Empties diagnostics section).
|
|
|
|
**Output:** `dc._session_summary.md` — 11 top-level sections, with `Session bootstrap` and `Planner LLM calls` conditionally suppressed (see §3.3).
|
|
|
|
**Contract:** pure tree reader. No DMO loads. No fetches. Everything
|
|
the renderer displays either lives directly on `tree.session.*` or is
|
|
composed from tree fields in the renderer's presentation layer.
|
|
|
|
### 3.2 Inputs
|
|
|
|
- `dc._session_tree.json` — primary input. Renderer SystemExits with a
|
|
clear "tree not found" message if absent.
|
|
- `dc._session_manifest.json` — consulted for the Empties diagnostics
|
|
section. If absent, that section renders empty.
|
|
|
|
The 24 raw `dc.<name>.json` artifacts are NOT loaded by the renderer.
|
|
|
|
### 3.3 Output: section order
|
|
|
|
The full-tree branch emits these top-level sections in order. The
|
|
gateway-direct branch (§2.8 `interactions_not_materialized_yet`) and
|
|
the minimal session-not-found branch use abbreviated section sets;
|
|
see `_render_gateway_direct` / `_render_minimal` in `render_dc.py`.
|
|
|
|
1. `## Session identity` — identity fields from `tree.session.identity`
|
|
plus display-only cells (Agent, Total duration) composed in-renderer.
|
|
2. `## Session bootstrap` — channel mode + bootstrap variables
|
|
(`identity.mode`, `identity.bootstrap_variables`). Surfaces these to
|
|
make MIAW vs Builder Previewer distinguishable at-a-glance —
|
|
`ssot__AiAgentChannelType__c` alone collides between the two. Section
|
|
is suppressed (not rendered) when both `mode` and `bootstrap_variables`
|
|
are absent — backwards-compat with older trees.
|
|
3. `## ID reference` — full UUIDs for everything truncated in the
|
|
hierarchical trace.
|
|
4. `## Transcript` — USER ↔ AGENT narrative per TURN interaction.
|
|
SESSION_END interactions have no messages and are skipped.
|
|
5. `## Complete hierarchical trace` — Interaction → Step → Generation →
|
|
GatewayRequest, with `+start + duration = +end` math on Interaction
|
|
and Step lines. Single timestamps on Generation and GatewayRequest
|
|
(no computed latencies; see §3.6). UUIDs truncated to 8 chars + `…`;
|
|
full forms in §2 above.
|
|
6. `## Per-turn summary` — one row per interaction.
|
|
7. `## Planner LLM calls (full prompts + responses)` — **opt-in via
|
|
`--show-prompts`; suppressed by default**. Walks `interactions[].steps[]`, builds one
|
|
`#### LLM call N — <short-id>` block per step that has a
|
|
`gateway_request`. Renders the full input prompt (from
|
|
`gateway_request.prompt_text`) and full response (from
|
|
`generation.response_text`, HTML-unescaped) inside fenced code
|
|
blocks. Per-payload display capped at 64 KB; full payloads always
|
|
remain authoritative on disk in `dc.gateway_requests.json` /
|
|
`dc.generations.json`. Off by default because multi-turn sessions
|
|
with 30 KB+ prompts would dominate the summary.
|
|
8. `## Visual analysis` — gantt + LLM-call overlay.
|
|
9. `## Session counts` — engineer-facing table of manifest counts.
|
|
10. `## Empties diagnostics` — one row per DMO with `rows == 0` and a
|
|
populated `_unavailable_reason` lifted verbatim from the manifest.
|
|
11. `## Catalog (session-filtered)` — TagDefinitions /
|
|
TagDefinitionAssociations / Tags filtered to agents observed in the
|
|
session.
|
|
|
|
DC alone does not expose per-turn LLM latency in a useful form, so this
|
|
skill emits no `## Latency rollups` section. Generation and
|
|
GatewayRequest carry single-write timestamps (see §3.6); they are not
|
|
start/end pairs.
|
|
|
|
### 3.4 Ellipsis rule
|
|
|
|
In the `## Complete hierarchical trace`:
|
|
|
|
- All UUIDs (interaction_id, step_id, generation_id,
|
|
gateway_request_id) render as the first 8 characters followed by `…`.
|
|
- Full UUIDs live in the `## ID reference` table above, keyed by the
|
|
same 8-char prefix.
|
|
|
|
This keeps the trace scannable while preserving the ability to
|
|
cross-reference by ID.
|
|
|
|
### 3.5 Session-end derivation
|
|
|
|
`session.end_ts` can be null for two reasons:
|
|
|
|
1. The session is still active.
|
|
2. The session ended but STDM hasn't materialized the `end_ts` column
|
|
yet.
|
|
|
|
Renderer display rule: if `session.end_ts` is non-null, render it
|
|
directly with `✓ materialized` suffix. If null, look at the last
|
|
interaction by timestamp:
|
|
|
|
- If a SESSION_END interaction exists, use its `start_ts` as the
|
|
derived end with `from SESSION_END interaction` suffix.
|
|
- Otherwise, use the last TURN interaction's `end_ts` (or `start_ts`
|
|
if its end is null) with `session still open (last TURN)` suffix.
|
|
|
|
The derivation is display-only; the tree's `session.end_ts` is not
|
|
modified.
|
|
|
|
### 3.6 Why we don't compute latencies on generation/gateway_request
|
|
|
|
`Generation.timestamp__c` and `GatewayRequest.timestamp__c` are
|
|
single timestamps representing when the DC row was written. They are
|
|
NOT `start_ts`/`end_ts` pairs. Treating the delta between two of them
|
|
as a "latency" is misleading — the gap reflects how DC serialized the
|
|
audit, not how long the LLM call took.
|
|
|
|
Interaction and Step DMOs DO expose start_ts + end_ts pairs; the
|
|
renderer computes `+offset + duration = +end` math for those.
|
|
|
|
### 3.7 Empties diagnostics
|
|
|
|
Walk `manifest["queries"]` for entries with `rows == 0` and a
|
|
populated `_unavailable_reason`. Render each as a row:
|
|
`| <dmo_name> | <rows> | <verbatim reason> |`. The renderer does not
|
|
infer reasons.
|
|
|
|
Artifacts that failed to parse go into `tree.counts.parse_warnings` and
|
|
surface in the `## Session counts` section, not here.
|
|
|
|
### 3.8 Idempotence
|
|
|
|
Running `render_dc.py --session <sid>` N times on the same
|
|
`dc._session_tree.json` produces a byte-identical
|
|
`dc._session_summary.md`. Rules:
|
|
|
|
- All section builders walk tree arrays in the order the tree provides
|
|
them; the tree itself is already sorted.
|
|
- No timestamps of "now" embedded in the output.
|
|
- No set-derived ordering; any set-to-list conversion goes through
|
|
`sorted(...)`.
|
|
- Verified on the live test fixture: `diff` of two runs returns empty.
|
|
|
|
### 3.9 Session-not-found short-path
|
|
|
|
If the tree has `_schema_version: 1` but no `interactions[]` key, the
|
|
renderer emits a minimal markdown file:
|
|
|
|
```markdown
|
|
# Session <sid>
|
|
|
|
## Session identity
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| Session id | <sid> |
|
|
| Session shape | session_not_found |
|
|
|
|
_No interactions resolved in Data Cloud. Check the session id, or
|
|
wait for STDM materialization._
|
|
```
|
|
|
|
No transcript, no trace, no counts, no catalog.
|
|
|
|
### 3.10 Tree schema version check
|
|
|
|
First check in `main_for_session()`:
|
|
|
|
- `tree.session._schema_version == 1` → render.
|
|
- `tree.session._schema_version` missing → stderr WARN, render anyway
|
|
(treat as best-effort for older trees).
|
|
- Any other value → `SystemExit("render_dc: unsupported tree
|
|
_schema_version=<v>; expected 1")`. Refuses rather than risking a
|
|
silent schema mismatch.
|
|
|
|
---
|
|
|
|
## 4. Reading order for a new skill author
|
|
|
|
1. Read this file (contract).
|
|
2. Run the pipeline once against a fresh session: `python3
|
|
scripts/fetch_dc.py --session <sid> --org <alias>`. Inspect
|
|
`DATA_ROOT/<sid>/` — you get the 24 `dc.<name>.json`, the manifest,
|
|
the tree, and the summary.
|
|
3. Open `dc._session_tree.json` and trace the declared chain for one
|
|
LLM_STEP end-to-end.
|
|
4. Read the matching `dc._session_summary.md`.
|
|
5. Read `scripts/assemble_dc.py` (module docstring points back here),
|
|
then `scripts/render_dc.py`.
|
|
6. Cross-reference [`dc_dmo_fields.md`](./dc_dmo_fields.md) when you
|
|
hit a field whose origin is unclear.
|