@salesforce/afv-skills 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/activating-datacloud/SKILL.md +0 -1
- package/skills/analyzing-omnistudio-dependencies/SKILL.md +0 -1
- package/skills/applying-slds/SKILL.md +322 -0
- package/skills/applying-slds/checklists.md +83 -0
- package/skills/applying-slds/examples.md +283 -0
- package/skills/applying-slds/guidance/README.md +83 -0
- package/skills/applying-slds/guidance/blueprints-index.md +213 -0
- package/skills/applying-slds/guidance/icons-guidance.md +186 -0
- package/skills/applying-slds/guidance/overviews/borders.md +236 -0
- package/skills/applying-slds/guidance/overviews/color.md +266 -0
- package/skills/applying-slds/guidance/overviews/display-density.md +366 -0
- package/skills/applying-slds/guidance/overviews/icons.md +240 -0
- package/skills/applying-slds/guidance/overviews/illustrations.md +235 -0
- package/skills/applying-slds/guidance/overviews/shadows.md +176 -0
- package/skills/applying-slds/guidance/overviews/spacing.md +216 -0
- package/skills/applying-slds/guidance/overviews/typography.md +323 -0
- package/skills/applying-slds/guidance/overviews/utilities.md +542 -0
- package/skills/applying-slds/guidance/slds-development-guide.md +288 -0
- package/skills/applying-slds/guidance/styling-hooks/borders.md +202 -0
- package/skills/applying-slds/guidance/styling-hooks/color/expressive-palette-hooks.md +153 -0
- package/skills/applying-slds/guidance/styling-hooks/color/index.md +171 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/accent-hooks.md +204 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/feedback-hooks.md +768 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/surface-hooks.md +337 -0
- package/skills/applying-slds/guidance/styling-hooks/color/system-hooks.md +132 -0
- package/skills/applying-slds/guidance/styling-hooks/index.md +327 -0
- package/skills/applying-slds/guidance/styling-hooks/shadows.md +238 -0
- package/skills/applying-slds/guidance/styling-hooks/spacing.md +254 -0
- package/skills/applying-slds/guidance/styling-hooks/typography.md +448 -0
- package/skills/applying-slds/guidance/utilities/alignment.md +119 -0
- package/skills/applying-slds/guidance/utilities/borders.md +131 -0
- package/skills/applying-slds/guidance/utilities/box.md +125 -0
- package/skills/applying-slds/guidance/utilities/color.md +165 -0
- package/skills/applying-slds/guidance/utilities/dark-mode.md +111 -0
- package/skills/applying-slds/guidance/utilities/description-list.md +168 -0
- package/skills/applying-slds/guidance/utilities/floats.md +117 -0
- package/skills/applying-slds/guidance/utilities/grid.md +264 -0
- package/skills/applying-slds/guidance/utilities/horizontal-list.md +110 -0
- package/skills/applying-slds/guidance/utilities/hyphenation.md +84 -0
- package/skills/applying-slds/guidance/utilities/index.md +205 -0
- package/skills/applying-slds/guidance/utilities/interactions.md +89 -0
- package/skills/applying-slds/guidance/utilities/layout.md +109 -0
- package/skills/applying-slds/guidance/utilities/line-clamp.md +131 -0
- package/skills/applying-slds/guidance/utilities/margin.md +155 -0
- package/skills/applying-slds/guidance/utilities/media-object.md +161 -0
- package/skills/applying-slds/guidance/utilities/name-value-list.md +152 -0
- package/skills/applying-slds/guidance/utilities/padding.md +155 -0
- package/skills/applying-slds/guidance/utilities/position.md +177 -0
- package/skills/applying-slds/guidance/utilities/print.md +114 -0
- package/skills/applying-slds/guidance/utilities/scrollable.md +126 -0
- package/skills/applying-slds/guidance/utilities/sizing.md +190 -0
- package/skills/applying-slds/guidance/utilities/themes.md +121 -0
- package/skills/applying-slds/guidance/utilities/truncate.md +127 -0
- package/skills/applying-slds/guidance/utilities/typography.md +166 -0
- package/skills/applying-slds/guidance/utilities/vertical-list.md +166 -0
- package/skills/applying-slds/guidance/utilities/visibility.md +228 -0
- package/skills/applying-slds/metadata/README.md +84 -0
- package/skills/applying-slds/metadata/blueprints/components/accordion.yaml +304 -0
- package/skills/applying-slds/metadata/blueprints/components/activity-timeline.yaml +92 -0
- package/skills/applying-slds/metadata/blueprints/components/alert.yaml +103 -0
- package/skills/applying-slds/metadata/blueprints/components/app-launcher.yaml +94 -0
- package/skills/applying-slds/metadata/blueprints/components/avatar-group.yaml +81 -0
- package/skills/applying-slds/metadata/blueprints/components/avatar.yaml +97 -0
- package/skills/applying-slds/metadata/blueprints/components/badges.yaml +102 -0
- package/skills/applying-slds/metadata/blueprints/components/brand-band.yaml +198 -0
- package/skills/applying-slds/metadata/blueprints/components/breadcrumbs.yaml +95 -0
- package/skills/applying-slds/metadata/blueprints/components/builder-header.yaml +192 -0
- package/skills/applying-slds/metadata/blueprints/components/button-groups.yaml +82 -0
- package/skills/applying-slds/metadata/blueprints/components/button-icons.yaml +295 -0
- package/skills/applying-slds/metadata/blueprints/components/buttons.yaml +230 -0
- package/skills/applying-slds/metadata/blueprints/components/cards.yaml +124 -0
- package/skills/applying-slds/metadata/blueprints/components/carousel.yaml +140 -0
- package/skills/applying-slds/metadata/blueprints/components/chat.yaml +179 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-button-group.yaml +192 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-button.yaml +204 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-toggle.yaml +177 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox.yaml +108 -0
- package/skills/applying-slds/metadata/blueprints/components/color-picker.yaml +172 -0
- package/skills/applying-slds/metadata/blueprints/components/combobox.yaml +136 -0
- package/skills/applying-slds/metadata/blueprints/components/counter.yaml +147 -0
- package/skills/applying-slds/metadata/blueprints/components/data-tables.yaml +157 -0
- package/skills/applying-slds/metadata/blueprints/components/datepickers.yaml +130 -0
- package/skills/applying-slds/metadata/blueprints/components/datetime-picker.yaml +155 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-composer.yaml +201 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-form-footer.yaml +161 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-utility-bar.yaml +175 -0
- package/skills/applying-slds/metadata/blueprints/components/drop-zone.yaml +115 -0
- package/skills/applying-slds/metadata/blueprints/components/dueling-picklist.yaml +196 -0
- package/skills/applying-slds/metadata/blueprints/components/dynamic-icons.yaml +128 -0
- package/skills/applying-slds/metadata/blueprints/components/dynamic-menu.yaml +141 -0
- package/skills/applying-slds/metadata/blueprints/components/expandable-section.yaml +115 -0
- package/skills/applying-slds/metadata/blueprints/components/expression.yaml +143 -0
- package/skills/applying-slds/metadata/blueprints/components/feeds.yaml +125 -0
- package/skills/applying-slds/metadata/blueprints/components/file-selector.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/files.yaml +119 -0
- package/skills/applying-slds/metadata/blueprints/components/form-element.yaml +145 -0
- package/skills/applying-slds/metadata/blueprints/components/global-header.yaml +120 -0
- package/skills/applying-slds/metadata/blueprints/components/global-navigation.yaml +100 -0
- package/skills/applying-slds/metadata/blueprints/components/icons.yaml +138 -0
- package/skills/applying-slds/metadata/blueprints/components/illustration.yaml +205 -0
- package/skills/applying-slds/metadata/blueprints/components/input.yaml +151 -0
- package/skills/applying-slds/metadata/blueprints/components/list-builder.yaml +127 -0
- package/skills/applying-slds/metadata/blueprints/components/lookups.yaml +132 -0
- package/skills/applying-slds/metadata/blueprints/components/map.yaml +118 -0
- package/skills/applying-slds/metadata/blueprints/components/menus.yaml +134 -0
- package/skills/applying-slds/metadata/blueprints/components/modals.yaml +152 -0
- package/skills/applying-slds/metadata/blueprints/components/notifications.yaml +88 -0
- package/skills/applying-slds/metadata/blueprints/components/page-headers.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/panels.yaml +149 -0
- package/skills/applying-slds/metadata/blueprints/components/path.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/picklist.yaml +125 -0
- package/skills/applying-slds/metadata/blueprints/components/pills.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/popovers.yaml +120 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-bar.yaml +110 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-indicator.yaml +133 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-ring.yaml +102 -0
- package/skills/applying-slds/metadata/blueprints/components/prompt.yaml +126 -0
- package/skills/applying-slds/metadata/blueprints/components/publishers.yaml +178 -0
- package/skills/applying-slds/metadata/blueprints/components/radio-button-group.yaml +172 -0
- package/skills/applying-slds/metadata/blueprints/components/radio-group.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/rich-text-editor.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/scoped-notifications.yaml +188 -0
- package/skills/applying-slds/metadata/blueprints/components/scoped-tabs.yaml +97 -0
- package/skills/applying-slds/metadata/blueprints/components/select.yaml +127 -0
- package/skills/applying-slds/metadata/blueprints/components/setup-assistant.yaml +152 -0
- package/skills/applying-slds/metadata/blueprints/components/slider.yaml +111 -0
- package/skills/applying-slds/metadata/blueprints/components/spinners.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/split-view.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/summary-detail.yaml +103 -0
- package/skills/applying-slds/metadata/blueprints/components/tabs.yaml +138 -0
- package/skills/applying-slds/metadata/blueprints/components/textarea.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/tiles.yaml +108 -0
- package/skills/applying-slds/metadata/blueprints/components/timepicker.yaml +111 -0
- package/skills/applying-slds/metadata/blueprints/components/toast.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/tooltips.yaml +107 -0
- package/skills/applying-slds/metadata/blueprints/components/tree-grid.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/trees.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/trial-bar.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/vertical-navigation.yaml +130 -0
- package/skills/applying-slds/metadata/blueprints/components/vertical-tabs.yaml +140 -0
- package/skills/applying-slds/metadata/blueprints/components/visual-picker.yaml +150 -0
- package/skills/applying-slds/metadata/blueprints/components/welcome-mat.yaml +136 -0
- package/skills/applying-slds/metadata/hooks-index.json +6272 -0
- package/skills/applying-slds/metadata/icon-metadata.json +38466 -0
- package/skills/applying-slds/metadata/utilities-index.json +21912 -0
- package/skills/applying-slds/references/component-selection.md +112 -0
- package/skills/applying-slds/references/icons-decision-guide.md +124 -0
- package/skills/applying-slds/references/styling-decision-guide.md +228 -0
- package/skills/applying-slds/references/utilities-quick-ref.md +125 -0
- package/skills/applying-slds/scripts/search-blueprints.cjs +117 -0
- package/skills/applying-slds/scripts/search-hooks.cjs +139 -0
- package/skills/applying-slds/scripts/search-icons.cjs +174 -0
- package/skills/applying-slds/scripts/search-utilities.cjs +161 -0
- package/skills/building-mobile-apps/SKILL.md +0 -1
- package/skills/building-omnistudio-callable-apex/SKILL.md +0 -1
- package/skills/building-omnistudio-datamapper/SKILL.md +0 -1
- package/skills/building-omnistudio-flexcard/SKILL.md +0 -1
- package/skills/building-omnistudio-integration-procedure/SKILL.md +0 -1
- package/skills/building-omnistudio-omniscript/SKILL.md +0 -1
- package/skills/building-sf-integrations/SKILL.md +0 -1
- package/skills/configuring-connected-apps/SKILL.md +0 -1
- package/skills/connecting-datacloud/SKILL.md +0 -1
- package/skills/creating-b2b-commerce-store/SKILL.md +0 -1
- package/skills/debugging-apex-logs/SKILL.md +0 -1
- package/skills/deploying-metadata/SKILL.md +0 -1
- package/skills/deploying-omnistudio-datapacks/SKILL.md +0 -1
- package/skills/developing-agentforce/SKILL.md +0 -1
- package/skills/fetching-salesforce-docs/SKILL.md +0 -1
- package/skills/generating-custom-lightning-type/SKILL.md +17 -39
- package/skills/generating-custom-lightning-type/assets/primitive-types-and-constraints.md +41 -0
- package/skills/generating-custom-lightning-type/references/widget-rendition.md +124 -0
- package/skills/generating-lwc-components/SKILL.md +0 -1
- package/skills/generating-mermaid-diagrams/SKILL.md +0 -1
- package/skills/generating-visual-diagrams/SKILL.md +0 -1
- package/skills/handling-sf-data/SKILL.md +0 -1
- package/skills/harmonizing-datacloud/SKILL.md +0 -1
- package/skills/integrating-b2b-commerce-open-code-components/SKILL.md +0 -1
- package/skills/investigating-agentforce-architecture/README.md +156 -0
- package/skills/investigating-agentforce-architecture/SKILL.md +230 -0
- package/skills/investigating-agentforce-architecture/assets/cli/describe_sobject.yaml +16 -0
- package/skills/investigating-agentforce-architecture/assets/cli/describe_tooling_sobject.yaml +17 -0
- package/skills/investigating-agentforce-architecture/assets/cli/list_metadata_genaiprompttemplate.yaml +17 -0
- package/skills/investigating-agentforce-architecture/assets/cli/org_display.yaml +15 -0
- package/skills/investigating-agentforce-architecture/assets/cli/retrieve_genai_plugin.yaml +18 -0
- package/skills/investigating-agentforce-architecture/assets/cli/show_access_token.yaml +27 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/action_tree.mmd +20 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/data_flow.mmd +19 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/dependency_graph.mmd +19 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/invocation_sequence.mmd +20 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/planner_state.mmd +18 -0
- package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_names.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/bot_definition_details.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/bot_version_lookup.soql +4 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_by_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_ids_by_names.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_view_by_durable_ids.soql +4 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_metadata_by_id.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/functions_by_plugins.soql +5 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_attrs_by_parent_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_bundle_functions.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_definition_by_agent_chain.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugin_functions_by_plugin_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugin_instructions_by_plugin_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugins_by_planner.soql +4 -0
- package/skills/investigating-agentforce-architecture/references/architecture_sections.md +243 -0
- package/skills/investigating-agentforce-architecture/references/contract.json +244 -0
- package/skills/investigating-agentforce-architecture/references/soql_fields.md +512 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/__init__.py +1 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/fs_guard.py +329 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/paths.py +110 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/runtime.py +59 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/sql.py +10 -0
- package/skills/investigating-agentforce-architecture/scripts/cache_check.py +234 -0
- package/skills/investigating-agentforce-architecture/scripts/config.py +131 -0
- package/skills/investigating-agentforce-architecture/scripts/fetch_soql.py +689 -0
- package/skills/investigating-agentforce-architecture/scripts/finalize.py +295 -0
- package/skills/investigating-agentforce-architecture/scripts/main.py +2835 -0
- package/skills/investigating-agentforce-architecture/scripts/metadata_listing.py +265 -0
- package/skills/investigating-agentforce-architecture/scripts/parallel_retrieve.py +69 -0
- package/skills/investigating-agentforce-architecture/scripts/parse_bundle.py +215 -0
- package/skills/investigating-agentforce-architecture/scripts/parse_wave.py +845 -0
- package/skills/investigating-agentforce-architecture/scripts/probe_channels.py +302 -0
- package/skills/investigating-agentforce-architecture/scripts/render_architecture.py +1043 -0
- package/skills/investigating-agentforce-architecture/scripts/resolve_bot.py +255 -0
- package/skills/investigating-agentforce-architecture/scripts/resolve_invocation_target.py +130 -0
- package/skills/investigating-agentforce-architecture/scripts/rest_client.py +763 -0
- package/skills/investigating-agentforce-architecture/scripts/retrieve_planner.py +13 -0
- package/skills/investigating-agentforce-architecture/scripts/sf_cli.py +242 -0
- package/skills/investigating-agentforce-architecture/scripts/soql_loader.py +253 -0
- package/skills/investigating-agentforce-architecture/scripts/summarize_tree.py +143 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/__init__.py +0 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/_bootstrap.py +23 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/__init__.py +0 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/genai_payloads.py +400 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check.py +307 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check_main.py +283 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_config.py +115 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_end_to_end_fixture.py +651 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_finalize.py +278 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_flow_children_inflation.py +582 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_fs_guard.py +113 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_iterative_wave_b.py +478 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_main_pipeline.py +3359 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parallel_retrieve.py +131 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_bundle.py +400 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave.py +644 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_classifiers.py +224 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_helpers.py +380 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_main.py +397 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_per_branch_visited.py +244 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_channels.py +359 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_cli_recipes.py +185 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_render_architecture.py +810 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_bot.py +203 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_creds.py +157 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_invocation_target.py +145 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_rest_client.py +1253 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_runtime_override.py +100 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_sf_cli.py +261 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_signature_stamping.py +466 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_soql_loader.py +501 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_summarize_tree.py +241 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_write_emit_ctx.py +480 -0
- package/skills/investigating-agentforce-architecture/tools/emit_env.py +157 -0
- package/skills/investigating-agentforce-architecture/tools/emit_result.py +262 -0
- package/skills/investigating-agentforce-architecture/tools/sanitize.py +33 -0
- package/skills/investigating-agentforce-architecture/tools/write_emit_ctx.py +332 -0
- package/skills/investigating-agentforce-d360/README.md +123 -0
- package/skills/investigating-agentforce-d360/SKILL.md +163 -0
- package/skills/investigating-agentforce-d360/assets/dc/app_generation.sql +51 -0
- package/skills/investigating-agentforce-d360/assets/dc/content_category.sql +44 -0
- package/skills/investigating-agentforce-d360/assets/dc/content_quality.sql +41 -0
- package/skills/investigating-agentforce-d360/assets/dc/discover_sessions.sql +36 -0
- package/skills/investigating-agentforce-d360/assets/dc/feedback.sql +47 -0
- package/skills/investigating-agentforce-d360/assets/dc/feedback_details.sql +38 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_records.sql +45 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_llm.sql +50 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_metadata.sql +44 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_tags.sql +42 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_requests.sql +89 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_responses.sql +43 -0
- package/skills/investigating-agentforce-d360/assets/dc/generations.sql +52 -0
- package/skills/investigating-agentforce-d360/assets/dc/interactions.sql +53 -0
- package/skills/investigating-agentforce-d360/assets/dc/messages.sql +53 -0
- package/skills/investigating-agentforce-d360/assets/dc/messaging_session.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/moment_interactions.sql +34 -0
- package/skills/investigating-agentforce-d360/assets/dc/moments.sql +39 -0
- package/skills/investigating-agentforce-d360/assets/dc/participants.sql +48 -0
- package/skills/investigating-agentforce-d360/assets/dc/sessions.sql +78 -0
- package/skills/investigating-agentforce-d360/assets/dc/steps.sql +64 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_associations.sql +46 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_definition_associations.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_definitions.sql +50 -0
- package/skills/investigating-agentforce-d360/assets/dc/tags.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/telemetry_spans.sql +55 -0
- package/skills/investigating-agentforce-d360/references/artifacts.md +50 -0
- package/skills/investigating-agentforce-d360/references/dc_dmo_fields.md +823 -0
- package/skills/investigating-agentforce-d360/references/dc_pipeline_contract.md +608 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/__init__.py +2 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/cli_override.py +98 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/fs_guard.py +334 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/paths.py +155 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/runtime.py +59 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/sql.py +14 -0
- package/skills/investigating-agentforce-d360/scripts/assemble_dc.py +1624 -0
- package/skills/investigating-agentforce-d360/scripts/config.py +45 -0
- package/skills/investigating-agentforce-d360/scripts/dc.py +188 -0
- package/skills/investigating-agentforce-d360/scripts/discover_sessions.py +556 -0
- package/skills/investigating-agentforce-d360/scripts/fetch_dc.py +1045 -0
- package/skills/investigating-agentforce-d360/scripts/render_dc.py +1750 -0
- package/skills/investigating-agentforce-d360/scripts/resolve_session.py +264 -0
- package/skills/investigating-agentforce-d360/scripts/storage.py +92 -0
- package/skills/investigating-agentforce-d360/scripts/tests/__init__.py +0 -0
- package/skills/investigating-agentforce-d360/scripts/tests/_bootstrap.py +15 -0
- package/skills/investigating-agentforce-d360/scripts/tests/fixtures/__init__.py +0 -0
- package/skills/investigating-agentforce-d360/scripts/tests/fixtures/synthetic_session.py +424 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_bootstrap_and_mode.py +115 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct.py +220 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct_integration.py +158 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_helpers.py +287 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_integration.py +247 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_dc_and_resolve_session.py +433 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions.py +458 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions_grep_ci.py +193 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_helpers.py +266 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_identity.py +528 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_main.py +251 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall.py +229 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall_full.py +283 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_identity_coherence.py +327 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_branches.py +256 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_gateway_direct.py +130 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_helpers.py +291 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_integration.py +220 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_planner_llm_calls.py +284 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_show_prompts_gating.py +215 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_from_disk.py +100 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_session_main.py +149 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_runtime_override.py +104 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape.py +95 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape_dropped_by_stdm.py +85 -0
- package/skills/managing-managed-event-subscription/SKILL.md +152 -0
- package/skills/managing-managed-event-subscription/assets/managed-event-subscription-template.xml +20 -0
- package/skills/managing-managed-event-subscription/references/delete-guide.md +57 -0
- package/skills/managing-managed-event-subscription/references/topic-name-formats.md +26 -0
- package/skills/managing-managed-event-subscription/references/update-constraints.md +30 -0
- package/skills/modeling-omnistudio-epc-catalog/SKILL.md +0 -1
- package/skills/observing-agentforce/SKILL.md +0 -1
- package/skills/orchestrating-datacloud/SKILL.md +0 -1
- package/skills/preparing-datacloud/SKILL.md +0 -1
- package/skills/querying-soql/SKILL.md +0 -1
- package/skills/retrieving-datacloud/SKILL.md +0 -1
- package/skills/running-apex-tests/SKILL.md +0 -1
- package/skills/running-code-analyzer/SKILL.md +0 -1
- package/skills/segmenting-datacloud/SKILL.md +0 -1
- package/skills/testing-agentforce/SKILL.md +0 -1
- package/skills/uplifting-components-to-slds2/SKILL.md +3 -2
- package/skills/uplifting-components-to-slds2/references/color-hooks-decision-guide.md +30 -9
- package/skills/uplifting-components-to-slds2/references/examples.md +24 -6
- package/skills/validating-slds/SKILL.md +262 -0
- package/skills/validating-slds/references/quality-checks.md +308 -0
- package/skills/validating-slds/references/report-format.md +302 -0
- package/skills/validating-slds/scripts/analyze-quality.cjs +521 -0
|
@@ -0,0 +1,1750 @@
|
|
|
1
|
+
"""Render dc._session_summary.md from dc._session_tree.json + dc._session_manifest.json.
|
|
2
|
+
|
|
3
|
+
Given `DATA_ROOT/<sid>/dc._session_tree.json` (produced by `scripts/assemble_dc.py`)
|
|
4
|
+
this module emits a human-readable `dc._session_summary.md`. Pure tree reader —
|
|
5
|
+
no raw DMO fetches, no joins. The `session.identity` sub-object on the tree
|
|
6
|
+
(added by the assembler) supplies all identity fields; everything else
|
|
7
|
+
(counts, trees, catalog) comes from the tree itself.
|
|
8
|
+
|
|
9
|
+
Output has 8 `##` sections:
|
|
10
|
+
|
|
11
|
+
1. Session identity — org/agent/bot/planner/session-start/end/duration
|
|
12
|
+
2. ID reference — full UUIDs for every ellipsized id in the tree
|
|
13
|
+
3. Transcript — narrative USER/AGENT text per TURN
|
|
14
|
+
4. Complete hierarchical trace — tree with `+start + dur = +end` math
|
|
15
|
+
5. Per-turn summary — one row per interaction
|
|
16
|
+
6. Session counts — engineer-facing totals
|
|
17
|
+
7. Empties diagnostics — DMOs that returned 0 rows + reason
|
|
18
|
+
8. Catalog (session-filtered)
|
|
19
|
+
|
|
20
|
+
Contract:
|
|
21
|
+
- Pure in-memory compute over already-produced artifacts.
|
|
22
|
+
- Reads DATA_ROOT/<sid>/dc._session_tree.json and dc._session_manifest.json.
|
|
23
|
+
- UUIDs in the tree are truncated to first 8 chars + `…`; full forms
|
|
24
|
+
live in the ID reference block.
|
|
25
|
+
- Session-not-found trees render only Session identity + a note.
|
|
26
|
+
- Tree schema version check: refuses incompatible versions, warns on
|
|
27
|
+
missing version (backward compat).
|
|
28
|
+
|
|
29
|
+
Invocation:
|
|
30
|
+
python3 scripts/render_dc.py --session <sid>
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import html
|
|
36
|
+
import json
|
|
37
|
+
import sys
|
|
38
|
+
from datetime import datetime
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Dict, List, Optional
|
|
41
|
+
|
|
42
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
43
|
+
|
|
44
|
+
from config import DATA_ROOT, paths
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---- schema ---------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
_SUPPORTED_SCHEMA_VERSION = 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---- timestamp / string helpers -------------------------------------------
|
|
53
|
+
|
|
54
|
+
def _parse_iso(s: Optional[str]) -> Optional[datetime]:
|
|
55
|
+
if not s:
|
|
56
|
+
return None
|
|
57
|
+
try:
|
|
58
|
+
return datetime.fromisoformat(s.replace("Z", "+00:00"))
|
|
59
|
+
except (ValueError, AttributeError):
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _fmt_offset(ts_iso: Optional[str], start_dt: Optional[datetime]) -> str:
|
|
64
|
+
"""Return '+12.345s' or '—' if timestamp is missing."""
|
|
65
|
+
ts = _parse_iso(ts_iso)
|
|
66
|
+
if ts is None or start_dt is None:
|
|
67
|
+
return "—"
|
|
68
|
+
return f"+{(ts - start_dt).total_seconds():.3f}s"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _fmt_duration_ms(start_iso: Optional[str], end_iso: Optional[str]) -> str:
|
|
72
|
+
s = _parse_iso(start_iso)
|
|
73
|
+
e = _parse_iso(end_iso)
|
|
74
|
+
if s is None or e is None:
|
|
75
|
+
return "—"
|
|
76
|
+
return f"{int((e - s).total_seconds() * 1000)}ms"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _decode(s: Optional[str]) -> str:
|
|
80
|
+
if not s:
|
|
81
|
+
return ""
|
|
82
|
+
return html.unescape(s).replace("\n", " ").strip()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _truncate(s: Optional[str], n: int = 80) -> str:
|
|
86
|
+
if not s:
|
|
87
|
+
return "—"
|
|
88
|
+
s = _decode(s)
|
|
89
|
+
return s if len(s) <= n else s[: n - 1] + "…"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _short(uid: Optional[str], keep: int = 8) -> str:
|
|
93
|
+
"""Truncate a UUID to `keep` chars + ellipsis. Full form lives in ID reference."""
|
|
94
|
+
if not uid:
|
|
95
|
+
return "—"
|
|
96
|
+
return uid[:keep] + "…" if len(uid) > keep else uid
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _fmt_total_duration(start_iso: Optional[str], end_iso: Optional[str]) -> Optional[str]:
|
|
100
|
+
s = _parse_iso(start_iso)
|
|
101
|
+
e = _parse_iso(end_iso)
|
|
102
|
+
if s is None or e is None:
|
|
103
|
+
return None
|
|
104
|
+
total_secs = (e - s).total_seconds()
|
|
105
|
+
if total_secs >= 3600:
|
|
106
|
+
h = int(total_secs // 3600)
|
|
107
|
+
m = int((total_secs % 3600) // 60)
|
|
108
|
+
return f"{h}h {m}m {total_secs % 60:.3f}s"
|
|
109
|
+
if total_secs >= 60:
|
|
110
|
+
m = int(total_secs // 60)
|
|
111
|
+
return f"{m}m {total_secs % 60:.3f}s"
|
|
112
|
+
return f"{total_secs:.3f}s"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---- session-end derivation ----------------------------------------------
|
|
116
|
+
|
|
117
|
+
def _derive_session_end(sess: dict) -> tuple[Optional[str], Optional[str]]:
|
|
118
|
+
"""Return (effective_end_iso, source_label).
|
|
119
|
+
|
|
120
|
+
Contract §3.5 derivation:
|
|
121
|
+
1. If `session.end_ts` is non-null → return it as-is (caller adds
|
|
122
|
+
"✓ materialized" suffix).
|
|
123
|
+
2. Else prefer a SESSION_END interaction's `start_ts`
|
|
124
|
+
(label: "from SESSION_END interaction").
|
|
125
|
+
3. Else fall back to the last TURN interaction's `end_ts`
|
|
126
|
+
(or `start_ts` if end is null) (label: "session still open (last TURN)").
|
|
127
|
+
4. Else `(None, None)`.
|
|
128
|
+
"""
|
|
129
|
+
end_iso = sess.get("end_ts")
|
|
130
|
+
if end_iso:
|
|
131
|
+
return end_iso, None # no derivation needed
|
|
132
|
+
interactions = sess.get("interactions") or []
|
|
133
|
+
# Prefer SESSION_END start_ts.
|
|
134
|
+
for iv in interactions:
|
|
135
|
+
if iv.get("type") == "SESSION_END" and iv.get("start_ts"):
|
|
136
|
+
return iv["start_ts"], "from SESSION_END interaction"
|
|
137
|
+
# Fall back to the last TURN (by interaction order; tree is already sorted).
|
|
138
|
+
last_turn = None
|
|
139
|
+
for iv in interactions:
|
|
140
|
+
if iv.get("type") == "TURN":
|
|
141
|
+
last_turn = iv
|
|
142
|
+
if last_turn is not None:
|
|
143
|
+
fallback = last_turn.get("end_ts") or last_turn.get("start_ts")
|
|
144
|
+
if fallback:
|
|
145
|
+
return fallback, "session still open (last TURN)"
|
|
146
|
+
return None, None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---- section builders -----------------------------------------------------
|
|
150
|
+
|
|
151
|
+
# Channel-mode value → human cell. Annotated so the reader can tell at
|
|
152
|
+
# a glance why we believe the mode is
|
|
153
|
+
# what it is — `ssot__AiAgentChannelType__c` is identical for MIAW
|
|
154
|
+
# production and Builder Previewer, which makes it useless without the
|
|
155
|
+
# annotation.
|
|
156
|
+
_MODE_HINTS: dict[str, str] = {
|
|
157
|
+
"production_messaging": "production_messaging ← inferred from RelatedMessagingSessionId",
|
|
158
|
+
"builder_previewer": "builder_previewer ← inferred from VariableText__c bootstrap keys",
|
|
159
|
+
"voice": "voice ← inferred from RelatedVoiceCallId",
|
|
160
|
+
"unknown": "unknown ← no signals (headless API, etc.)",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _fmt_mode_cell(mode_value: str) -> str:
|
|
165
|
+
"""Format the `mode` field with a short why-this-value annotation."""
|
|
166
|
+
return _MODE_HINTS.get(mode_value, mode_value)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# Channel values that flag a production messaging session. `VariableText__c`
|
|
170
|
+
# is expected to be NOT_SET on these — the bootstrap variables ride the
|
|
171
|
+
# messaging session record, not the AI session. Any other channel
|
|
172
|
+
# (E & O headless API, Builder Previewer, voice, unknown/integration)
|
|
173
|
+
# can also legitimately produce NOT_SET, but for a different reason
|
|
174
|
+
# (no bootstrap variables were attached at session start) — and we
|
|
175
|
+
# must not mislabel those as "production messaging path".
|
|
176
|
+
_MESSAGING_CHANNELS = frozenset({
|
|
177
|
+
"SCRT2 - EmbeddedMessaging",
|
|
178
|
+
"Messaging",
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _section_session_bootstrap(identity: dict, channel: Optional[str] = None) -> List[str]:
|
|
183
|
+
"""Render the `bootstrap_variables` block parsed from VariableText__c.
|
|
184
|
+
|
|
185
|
+
Three states (all rendered as a small subtable so they don't clutter
|
|
186
|
+
the main identity table):
|
|
187
|
+
- None / NOT_SET → "no bootstrap variables for this session"
|
|
188
|
+
(with a messaging-path addendum only when the
|
|
189
|
+
session's `channel` is actually a messaging
|
|
190
|
+
channel — see `_MESSAGING_CHANNELS`).
|
|
191
|
+
- parse error → "_parse_error" with the raw prefix
|
|
192
|
+
- populated → key/value pairs sorted, plus a "Builder Previewer
|
|
193
|
+
indicators" tally derived from the same key set
|
|
194
|
+
used in `_derive_mode`.
|
|
195
|
+
|
|
196
|
+
Empty section returned when bootstrap_variables is missing entirely
|
|
197
|
+
(older artifacts predate the bootstrap-variables harvest).
|
|
198
|
+
"""
|
|
199
|
+
if "bootstrap_variables" not in identity:
|
|
200
|
+
return [] # older artifact: no bootstrap_variables harvested
|
|
201
|
+
|
|
202
|
+
bootstrap = identity.get("bootstrap_variables")
|
|
203
|
+
lines: List[str] = ["## Session bootstrap", ""]
|
|
204
|
+
|
|
205
|
+
if bootstrap is None:
|
|
206
|
+
if channel in _MESSAGING_CHANNELS:
|
|
207
|
+
lines.append(
|
|
208
|
+
"`ssot__VariableText__c` is `NOT_SET` — no bootstrap variables "
|
|
209
|
+
"(production messaging path; messaging sessions don't carry VariableText)."
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
lines.append(
|
|
213
|
+
"`ssot__VariableText__c` is `NOT_SET` — no bootstrap variables for this session."
|
|
214
|
+
)
|
|
215
|
+
lines.append("")
|
|
216
|
+
return lines
|
|
217
|
+
|
|
218
|
+
if isinstance(bootstrap, dict) and bootstrap.get("_parse_error"):
|
|
219
|
+
raw = bootstrap.get("_raw") or ""
|
|
220
|
+
lines.append("`ssot__VariableText__c` failed to parse as JSON:")
|
|
221
|
+
lines.append("")
|
|
222
|
+
lines.append("```")
|
|
223
|
+
lines.append(raw)
|
|
224
|
+
lines.append("```")
|
|
225
|
+
lines.append("")
|
|
226
|
+
return lines
|
|
227
|
+
|
|
228
|
+
# Populated case.
|
|
229
|
+
indicator_keys = {
|
|
230
|
+
"__resolved_locale__",
|
|
231
|
+
"__locale_instruction__",
|
|
232
|
+
"__supports_result_display__",
|
|
233
|
+
"__show_tool_results_invoked__",
|
|
234
|
+
}
|
|
235
|
+
present_indicators = sorted(set(bootstrap) & indicator_keys)
|
|
236
|
+
indicator_cell = (
|
|
237
|
+
"yes · " + ", ".join(present_indicators)
|
|
238
|
+
if present_indicators else "no"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
lines.append("| Key | Value |")
|
|
242
|
+
lines.append("|---|---|")
|
|
243
|
+
for key in sorted(bootstrap):
|
|
244
|
+
value = bootstrap[key]
|
|
245
|
+
# Render lists / dicts compactly; keep strings/numbers/bools as-is.
|
|
246
|
+
if isinstance(value, (list, dict)):
|
|
247
|
+
value_repr = json.dumps(value)
|
|
248
|
+
else:
|
|
249
|
+
value_repr = str(value)
|
|
250
|
+
# Pipe character would break the markdown table — escape.
|
|
251
|
+
value_repr = value_repr.replace("|", "\\|")
|
|
252
|
+
lines.append(f"| {key} | {value_repr} |")
|
|
253
|
+
lines.append(f"| **Builder Previewer indicators** | {indicator_cell} |")
|
|
254
|
+
lines.append("")
|
|
255
|
+
return lines
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _compose_agent_cell(identity: dict) -> Optional[str]:
|
|
259
|
+
"""Build the human Agent cell from identity fields; drop None components."""
|
|
260
|
+
parts: List[str] = []
|
|
261
|
+
api_name = identity.get("agent_api_name")
|
|
262
|
+
version = identity.get("agent_version")
|
|
263
|
+
if api_name and version:
|
|
264
|
+
parts.append(f"{api_name} {version}")
|
|
265
|
+
elif api_name:
|
|
266
|
+
parts.append(api_name)
|
|
267
|
+
elif version:
|
|
268
|
+
parts.append(version)
|
|
269
|
+
label = identity.get("agent_label")
|
|
270
|
+
if label:
|
|
271
|
+
parts.append(f"— {label}" if parts else label)
|
|
272
|
+
atype = identity.get("agent_type")
|
|
273
|
+
if atype:
|
|
274
|
+
parts.append(f"({atype})")
|
|
275
|
+
return " ".join(parts) if parts else None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _fmt_session_end_cell(effective_end_iso: Optional[str],
|
|
279
|
+
end_source: Optional[str],
|
|
280
|
+
raw_end_iso: Optional[str]) -> Optional[str]:
|
|
281
|
+
"""Compose the identity-table value for `Session end`.
|
|
282
|
+
|
|
283
|
+
- Materialized end (`raw_end_iso` truthy) → "<iso> ✓ materialized"
|
|
284
|
+
- Derived end (end_source truthy) → "<iso> (<source label>)"
|
|
285
|
+
- Neither → None (row is dropped)
|
|
286
|
+
"""
|
|
287
|
+
if effective_end_iso is None:
|
|
288
|
+
return None
|
|
289
|
+
if raw_end_iso:
|
|
290
|
+
return f"{effective_end_iso} ✓ materialized"
|
|
291
|
+
if end_source:
|
|
292
|
+
return f"{effective_end_iso} ({end_source})"
|
|
293
|
+
return effective_end_iso
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _section_session_identity(sess: dict, effective_end_iso: Optional[str],
|
|
297
|
+
end_source: Optional[str]) -> List[str]:
|
|
298
|
+
identity = sess.get("identity") or {}
|
|
299
|
+
org = sess.get("org") or {}
|
|
300
|
+
|
|
301
|
+
# `mode` is "production_messaging" / "builder_previewer" / "voice" /
|
|
302
|
+
# "unknown" — derived in assemble_dc from RelatedMessagingSessionId +
|
|
303
|
+
# RelatedVoiceCallId + bootstrap_variables because
|
|
304
|
+
# ssot__AiAgentChannelType__c is identical for MIAW production and
|
|
305
|
+
# Builder Previewer.
|
|
306
|
+
mode_value = identity.get("mode")
|
|
307
|
+
mode_cell = _fmt_mode_cell(mode_value) if mode_value else None
|
|
308
|
+
|
|
309
|
+
rows_out = [
|
|
310
|
+
("Session id", sess.get("id")),
|
|
311
|
+
("Org id", identity.get("org_id")),
|
|
312
|
+
("Org alias", org.get("alias")),
|
|
313
|
+
("Instance URL", org.get("instance_url")),
|
|
314
|
+
("Channel", sess.get("channel")),
|
|
315
|
+
("Mode", mode_cell),
|
|
316
|
+
("App type", identity.get("app_type")),
|
|
317
|
+
("Agent", _compose_agent_cell(identity)),
|
|
318
|
+
("Bot id", identity.get("bot_id")),
|
|
319
|
+
("Bot name", identity.get("bot_name")),
|
|
320
|
+
("Bot version id", identity.get("bot_version_id")),
|
|
321
|
+
("Planner id", identity.get("planner_id")),
|
|
322
|
+
("Planner name", identity.get("planner_name")),
|
|
323
|
+
("Planner type", identity.get("planner_type")),
|
|
324
|
+
("Configured model", identity.get("configured_model")),
|
|
325
|
+
("Platform user id", identity.get("platform_user_id")),
|
|
326
|
+
("Messaging session id", identity.get("messaging_session_id")),
|
|
327
|
+
("Messaging end-user id", identity.get("messaging_end_user_id")),
|
|
328
|
+
("Voice call id", identity.get("voice_call_id")),
|
|
329
|
+
("Individual id", identity.get("individual_id")),
|
|
330
|
+
("Session start", sess.get("start_ts")),
|
|
331
|
+
(
|
|
332
|
+
"Session end",
|
|
333
|
+
_fmt_session_end_cell(effective_end_iso, end_source, sess.get("end_ts")),
|
|
334
|
+
),
|
|
335
|
+
("End type", sess.get("end_type")),
|
|
336
|
+
("Total duration", _fmt_total_duration(sess.get("start_ts"), effective_end_iso)),
|
|
337
|
+
]
|
|
338
|
+
lines: List[str] = ["## Session identity", "", "| Field | Value |", "|---|---|"]
|
|
339
|
+
for label, value in rows_out:
|
|
340
|
+
if value is None or value == "" or value == "—":
|
|
341
|
+
continue
|
|
342
|
+
lines.append(f"| {label} | {value} |")
|
|
343
|
+
lines.append("")
|
|
344
|
+
return lines
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _section_id_reference(sess: dict) -> List[str]:
|
|
348
|
+
"""Full-UUID lookup for every id the tree truncates."""
|
|
349
|
+
lines: List[str] = [
|
|
350
|
+
"## ID reference",
|
|
351
|
+
"",
|
|
352
|
+
"The tree truncates UUIDs to the first 8 chars + `…`. Look up the full "
|
|
353
|
+
"form here. Ordered by type → first occurrence.",
|
|
354
|
+
"",
|
|
355
|
+
"```",
|
|
356
|
+
f"session = {sess.get('id')}",
|
|
357
|
+
"",
|
|
358
|
+
]
|
|
359
|
+
# Interactions
|
|
360
|
+
lines.append("interactions:")
|
|
361
|
+
for iv in sess.get("interactions", []):
|
|
362
|
+
lines.append(
|
|
363
|
+
f" {iv.get('id')} type={iv.get('type') or '?'} "
|
|
364
|
+
f"trace={iv.get('trace_id') or '—'}"
|
|
365
|
+
)
|
|
366
|
+
lines.append("")
|
|
367
|
+
# Participants
|
|
368
|
+
lines.append("participants:")
|
|
369
|
+
for p in sess.get("participants", []):
|
|
370
|
+
lines.append(
|
|
371
|
+
f" {p.get('participant_id') or '—'} role={p.get('role') or '—'} "
|
|
372
|
+
f"agent={p.get('agent_api_name') or '—'} version={p.get('agent_version') or '—'} "
|
|
373
|
+
f"type={p.get('agent_type') or '—'}"
|
|
374
|
+
)
|
|
375
|
+
lines.append("")
|
|
376
|
+
|
|
377
|
+
# Steps, generations, gateway_requests, messages harvested from the tree.
|
|
378
|
+
all_steps: List[tuple] = []
|
|
379
|
+
all_gens: List[tuple] = []
|
|
380
|
+
all_gws: List[tuple] = []
|
|
381
|
+
all_msgs: List[tuple] = []
|
|
382
|
+
for iv in sess.get("interactions", []):
|
|
383
|
+
for m in iv.get("messages", []):
|
|
384
|
+
if m.get("message_id"):
|
|
385
|
+
all_msgs.append((m["message_id"], m.get("role") or m.get("type") or "?"))
|
|
386
|
+
for st in iv.get("steps", []):
|
|
387
|
+
if st.get("id"):
|
|
388
|
+
all_steps.append((st["id"], st.get("type") or "?", st.get("name") or "—"))
|
|
389
|
+
gen = st.get("generation")
|
|
390
|
+
if gen and gen.get("generation_id"):
|
|
391
|
+
all_gens.append((
|
|
392
|
+
gen["generation_id"],
|
|
393
|
+
gen.get("response_id") or "—",
|
|
394
|
+
gen.get("feature") or "—",
|
|
395
|
+
))
|
|
396
|
+
gw = st.get("gateway_request")
|
|
397
|
+
if gw and gw.get("gateway_request_id"):
|
|
398
|
+
all_gws.append((
|
|
399
|
+
gw["gateway_request_id"], "declared",
|
|
400
|
+
gw.get("feature") or "—", gw.get("model") or "—",
|
|
401
|
+
))
|
|
402
|
+
for tb in iv.get("timestamp_bound_gateway_calls", []):
|
|
403
|
+
if tb.get("gateway_request_id"):
|
|
404
|
+
all_gws.append((
|
|
405
|
+
tb["gateway_request_id"], "timestamp_window",
|
|
406
|
+
tb.get("feature") or "—", tb.get("model") or "—",
|
|
407
|
+
))
|
|
408
|
+
for g in sess.get("unbound_gateway_calls", []):
|
|
409
|
+
if g.get("gateway_request_id"):
|
|
410
|
+
all_gws.append((
|
|
411
|
+
g["gateway_request_id"], "unbound",
|
|
412
|
+
g.get("feature") or "—", g.get("model") or "—",
|
|
413
|
+
))
|
|
414
|
+
|
|
415
|
+
if all_steps:
|
|
416
|
+
lines.append("steps:")
|
|
417
|
+
for sid_, stype, sname in all_steps:
|
|
418
|
+
lines.append(f" {sid_} type={stype} name={sname}")
|
|
419
|
+
lines.append("")
|
|
420
|
+
if all_gens:
|
|
421
|
+
lines.append("generations:")
|
|
422
|
+
for gid, rid, feat in all_gens:
|
|
423
|
+
lines.append(f" {gid} response_id={rid} feature={feat}")
|
|
424
|
+
lines.append("")
|
|
425
|
+
if all_gws:
|
|
426
|
+
lines.append("gateway_requests:")
|
|
427
|
+
for gwid, bm, feat, model in all_gws:
|
|
428
|
+
lines.append(f" {gwid} binding={bm} feature={feat} model={model}")
|
|
429
|
+
lines.append("")
|
|
430
|
+
if all_msgs:
|
|
431
|
+
lines.append("messages:")
|
|
432
|
+
for mid, role in all_msgs:
|
|
433
|
+
lines.append(f" {mid} role={role}")
|
|
434
|
+
lines.append("```")
|
|
435
|
+
lines.append("")
|
|
436
|
+
return lines
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _section_transcript(sess: dict, start_dt: Optional[datetime]) -> List[str]:
|
|
440
|
+
turns = [iv for iv in sess.get("interactions", []) if iv.get("type") == "TURN"]
|
|
441
|
+
if not turns:
|
|
442
|
+
return []
|
|
443
|
+
lines: List[str] = ["## Transcript", ""]
|
|
444
|
+
for iv in turns:
|
|
445
|
+
start_off = _fmt_offset(iv.get("start_ts"), start_dt)
|
|
446
|
+
dur = _fmt_duration_ms(iv.get("start_ts"), iv.get("end_ts"))
|
|
447
|
+
lines.append(f"**Interaction {_short(iv.get('id'))}** · {start_off} · {dur}")
|
|
448
|
+
for m in iv.get("messages", []):
|
|
449
|
+
role = m.get("role") or m.get("type") or "?"
|
|
450
|
+
lines.append(f"> **{role}:** {_decode(m.get('text') or '')}")
|
|
451
|
+
lines.append("")
|
|
452
|
+
return lines
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _section_hierarchical_trace(sess: dict, start_dt: Optional[datetime],
|
|
456
|
+
effective_end_iso: Optional[str],
|
|
457
|
+
end_source: Optional[str]) -> List[str]:
|
|
458
|
+
lines: List[str] = [
|
|
459
|
+
"## Complete hierarchical trace",
|
|
460
|
+
"",
|
|
461
|
+
"Notation: `+Xs` = offset in seconds from session start. "
|
|
462
|
+
"UUIDs are truncated to 8 chars + `…` for readability; "
|
|
463
|
+
"full forms are in the **ID reference** block above.",
|
|
464
|
+
"",
|
|
465
|
+
"```",
|
|
466
|
+
]
|
|
467
|
+
|
|
468
|
+
# Session header
|
|
469
|
+
lines.append(f"SESSION {_short(sess.get('id'))}")
|
|
470
|
+
start_iso = sess.get("start_ts")
|
|
471
|
+
if start_iso:
|
|
472
|
+
lines.append(f"│ Start +0.000s ({start_iso})")
|
|
473
|
+
|
|
474
|
+
end_iso_raw = sess.get("end_ts")
|
|
475
|
+
end_type = sess.get("end_type") or None
|
|
476
|
+
outcome_s = end_type or "—"
|
|
477
|
+
if not end_type and not end_iso_raw:
|
|
478
|
+
outcome_s = "— (session end not yet materialized in STDM)"
|
|
479
|
+
display_end = end_iso_raw or effective_end_iso
|
|
480
|
+
if display_end:
|
|
481
|
+
label_suffix = f" [{end_source}]" if end_source else ""
|
|
482
|
+
lines.append(
|
|
483
|
+
f"│ End {_fmt_offset(display_end, start_dt)} ({display_end}){label_suffix} "
|
|
484
|
+
f"outcome={outcome_s}"
|
|
485
|
+
)
|
|
486
|
+
else:
|
|
487
|
+
lines.append(f"│ End — (session still open) outcome={outcome_s}")
|
|
488
|
+
lines.append("│")
|
|
489
|
+
|
|
490
|
+
interactions = sess.get("interactions", [])
|
|
491
|
+
for iv_idx, iv in enumerate(interactions):
|
|
492
|
+
is_last_iv = iv_idx == len(interactions) - 1
|
|
493
|
+
iv_branch = "└──" if is_last_iv else "├──"
|
|
494
|
+
iv_cont = " " if is_last_iv else "│ "
|
|
495
|
+
|
|
496
|
+
iv_start_off = _fmt_offset(iv.get("start_ts"), start_dt)
|
|
497
|
+
iv_end_off = _fmt_offset(iv.get("end_ts"), start_dt)
|
|
498
|
+
iv_dur = _fmt_duration_ms(iv.get("start_ts"), iv.get("end_ts"))
|
|
499
|
+
iv_type = iv.get("type") or "?"
|
|
500
|
+
lines.append(
|
|
501
|
+
f"{iv_branch} {iv_type} {_short(iv.get('id'))} "
|
|
502
|
+
f"{iv_start_off} + {iv_dur} = {iv_end_off}"
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
topic = iv.get("topic")
|
|
506
|
+
if topic:
|
|
507
|
+
lines.append(f"{iv_cont}├── TOPIC: {topic}")
|
|
508
|
+
|
|
509
|
+
for m in iv.get("messages", []):
|
|
510
|
+
role = m.get("role") or m.get("type") or "?"
|
|
511
|
+
m_off = _fmt_offset(m.get("ts"), start_dt) if m.get("ts") else "—"
|
|
512
|
+
lines.append(f"{iv_cont}├── {role} message {_short(m.get('message_id'))} ts={m_off}")
|
|
513
|
+
lines.append(f"{iv_cont}│ └── text: \"{_truncate(m.get('text'), 100)}\"")
|
|
514
|
+
|
|
515
|
+
steps = iv.get("steps", [])
|
|
516
|
+
tsbound = iv.get("timestamp_bound_gateway_calls", [])
|
|
517
|
+
tsb_by_step: Dict[str, List[dict]] = {}
|
|
518
|
+
tsb_interaction_level: List[dict] = []
|
|
519
|
+
for tb in tsbound:
|
|
520
|
+
bsid = tb.get("bound_to_step_id")
|
|
521
|
+
if bsid:
|
|
522
|
+
tsb_by_step.setdefault(bsid, []).append(tb)
|
|
523
|
+
else:
|
|
524
|
+
tsb_interaction_level.append(tb)
|
|
525
|
+
|
|
526
|
+
remaining_groups: List[str] = []
|
|
527
|
+
if steps:
|
|
528
|
+
remaining_groups.append("steps")
|
|
529
|
+
if tsb_interaction_level:
|
|
530
|
+
remaining_groups.append("ts_il")
|
|
531
|
+
|
|
532
|
+
for grp_idx, grp in enumerate(remaining_groups):
|
|
533
|
+
grp_is_last = grp_idx == len(remaining_groups) - 1
|
|
534
|
+
grp_branch = "└──" if grp_is_last else "├──"
|
|
535
|
+
grp_cont = " " if grp_is_last else "│ "
|
|
536
|
+
if grp == "steps":
|
|
537
|
+
lines.append(f"{iv_cont}{grp_branch} STEPS:")
|
|
538
|
+
for st_idx, st in enumerate(steps):
|
|
539
|
+
lines.extend(_render_step(
|
|
540
|
+
st, st_idx, len(steps), iv_cont, grp_cont,
|
|
541
|
+
start_dt, tsb_by_step,
|
|
542
|
+
))
|
|
543
|
+
else: # grp == "ts_il"
|
|
544
|
+
lines.append(f"{iv_cont}{grp_branch} INTERACTION-LEVEL TIMESTAMP-BOUND GATEWAY CALLS:")
|
|
545
|
+
for tb_idx, tb in enumerate(tsb_interaction_level):
|
|
546
|
+
tb_is_last = tb_idx == len(tsb_interaction_level) - 1
|
|
547
|
+
tb_branch = "└──" if tb_is_last else "├──"
|
|
548
|
+
lines.append(
|
|
549
|
+
f"{iv_cont}{grp_cont}{tb_branch} "
|
|
550
|
+
f"gateway_request [timestamp_window, interaction-level] "
|
|
551
|
+
f"{_short(tb.get('gateway_request_id'))} "
|
|
552
|
+
f"feature={tb.get('feature') or '—'} "
|
|
553
|
+
f"model={tb.get('model') or '—'}"
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
if not is_last_iv:
|
|
557
|
+
lines.append("│")
|
|
558
|
+
|
|
559
|
+
# Unbound gateway calls (session root level)
|
|
560
|
+
ub = sess.get("unbound_gateway_calls", [])
|
|
561
|
+
if ub:
|
|
562
|
+
lines.append("")
|
|
563
|
+
lines.append(
|
|
564
|
+
f"UNBOUND GATEWAY CALLS ({len(ub)}) — neither declared chain "
|
|
565
|
+
"nor timestamp window matched"
|
|
566
|
+
)
|
|
567
|
+
for i, g in enumerate(ub):
|
|
568
|
+
branch = "└──" if i == len(ub) - 1 else "├──"
|
|
569
|
+
lines.append(
|
|
570
|
+
f"{branch} {_short(g.get('gateway_request_id'))} "
|
|
571
|
+
f"feature={g.get('feature') or '—'} model={g.get('model') or '—'}"
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
lines.append("```")
|
|
575
|
+
lines.append("")
|
|
576
|
+
return lines
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _render_step(st: dict, st_idx: int, n_steps: int,
|
|
580
|
+
iv_cont: str, grp_cont: str,
|
|
581
|
+
start_dt: Optional[datetime],
|
|
582
|
+
tsb_by_step: Dict[str, List[dict]]) -> List[str]:
|
|
583
|
+
"""Render one step + its nested generation / gateway_request / ts-bound GWs."""
|
|
584
|
+
st_is_last = st_idx == n_steps - 1
|
|
585
|
+
st_branch = "└──" if st_is_last else "├──"
|
|
586
|
+
st_cont = " " if st_is_last else "│ "
|
|
587
|
+
st_start_off = _fmt_offset(st.get("start_ts"), start_dt)
|
|
588
|
+
st_end_off = _fmt_offset(st.get("end_ts"), start_dt)
|
|
589
|
+
st_dur = _fmt_duration_ms(st.get("start_ts"), st.get("end_ts"))
|
|
590
|
+
|
|
591
|
+
# Show the bound LLM model alongside the step name when known.
|
|
592
|
+
# `model_name` is mirrored from the bound gateway_request by
|
|
593
|
+
# assemble_dc; None when the declared chain didn't reach or when STDM
|
|
594
|
+
# dropped writes (the `gateway_requests_dropped_by_stdm` shape).
|
|
595
|
+
name_cell = st.get("name") or "—"
|
|
596
|
+
model_name = st.get("model_name")
|
|
597
|
+
if model_name:
|
|
598
|
+
name_cell = f"{name_cell} · {model_name}"
|
|
599
|
+
|
|
600
|
+
lines = [
|
|
601
|
+
f"{iv_cont}{grp_cont}{st_branch} {st.get('type') or '?'} {_short(st.get('id'))} "
|
|
602
|
+
f"name={name_cell} {st_start_off} + {st_dur} = {st_end_off}"
|
|
603
|
+
]
|
|
604
|
+
|
|
605
|
+
# Step children: error, generation (+ trust signals), gateway_request, collision, bound GWs.
|
|
606
|
+
step_kids: List[tuple] = []
|
|
607
|
+
if st.get("error_text"):
|
|
608
|
+
step_kids.append(("error", None))
|
|
609
|
+
gen = st.get("generation")
|
|
610
|
+
if gen:
|
|
611
|
+
step_kids.append(("gen", gen))
|
|
612
|
+
gw = st.get("gateway_request")
|
|
613
|
+
if gw:
|
|
614
|
+
step_kids.append(("gw", gw))
|
|
615
|
+
if st.get("gateway_request_collision"):
|
|
616
|
+
step_kids.append(("collision", None))
|
|
617
|
+
for tb in tsb_by_step.get(st.get("id", ""), []):
|
|
618
|
+
step_kids.append(("tsb", tb))
|
|
619
|
+
|
|
620
|
+
prefix = f"{iv_cont}{grp_cont}{st_cont}"
|
|
621
|
+
for k_idx, (ktype, kval) in enumerate(step_kids):
|
|
622
|
+
k_is_last = k_idx == len(step_kids) - 1
|
|
623
|
+
k_branch = "└──" if k_is_last else "├──"
|
|
624
|
+
k_cont = " " if k_is_last else "│ "
|
|
625
|
+
|
|
626
|
+
if ktype == "error":
|
|
627
|
+
lines.append(f"{prefix}{k_branch} ERROR: {st['error_text']}")
|
|
628
|
+
elif ktype == "collision":
|
|
629
|
+
lines.append(
|
|
630
|
+
f"{prefix}{k_branch} ⚠ gateway_request_collision: "
|
|
631
|
+
"earlier step claimed the declared GW"
|
|
632
|
+
)
|
|
633
|
+
elif ktype == "gen":
|
|
634
|
+
lines.extend(_render_generation(kval, prefix, k_branch, k_cont,
|
|
635
|
+
step_name=st.get("name")))
|
|
636
|
+
elif ktype == "gw":
|
|
637
|
+
lines.extend(_render_gw_declared(kval, prefix, k_branch, k_cont))
|
|
638
|
+
elif ktype == "tsb":
|
|
639
|
+
tb = kval
|
|
640
|
+
lines.append(
|
|
641
|
+
f"{prefix}{k_branch} gateway_request [timestamp_window] "
|
|
642
|
+
f"{_short(tb.get('gateway_request_id'))} "
|
|
643
|
+
f"feature={tb.get('feature') or '—'} "
|
|
644
|
+
f"model={tb.get('model') or '—'}"
|
|
645
|
+
)
|
|
646
|
+
return lines
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
_ROLE_LABELS = {
|
|
650
|
+
"ReactTopicPrompt": "topic-classification output",
|
|
651
|
+
"ReactInitialPrompt": "ReAct planner step",
|
|
652
|
+
"ReactValidationPrompt": "ReAct validator",
|
|
653
|
+
"ReactFormatSurfaceResponsePrompt": "response formatter",
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _role_label_for(step_name: Optional[str]) -> Optional[str]:
|
|
658
|
+
if not step_name:
|
|
659
|
+
return None
|
|
660
|
+
for key, label in _ROLE_LABELS.items():
|
|
661
|
+
if key in step_name:
|
|
662
|
+
return label
|
|
663
|
+
return None
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _parse_finish_reason(response_parameters: Optional[str]) -> Optional[str]:
|
|
667
|
+
"""Pull finish_reason out of the HTML-escaped JSON in responseParameters__c.
|
|
668
|
+
Shape: `{"finish_reason":"\"stop\"",...}`."""
|
|
669
|
+
if not response_parameters:
|
|
670
|
+
return None
|
|
671
|
+
try:
|
|
672
|
+
decoded = html.unescape(response_parameters)
|
|
673
|
+
parsed = json.loads(decoded)
|
|
674
|
+
except (ValueError, TypeError):
|
|
675
|
+
return None
|
|
676
|
+
raw = parsed.get("finish_reason") if isinstance(parsed, dict) else None
|
|
677
|
+
if not isinstance(raw, str):
|
|
678
|
+
return None
|
|
679
|
+
# value often comes wrapped in escaped quotes: `\"stop\"` → `stop`
|
|
680
|
+
return raw.replace("\\", "").strip('"').strip()
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _decoded_line(response_text: Optional[str]) -> str:
|
|
684
|
+
"""Render `decoded:` content. Detect tool-call JSON and summarize.
|
|
685
|
+
responseText__c is HTML-escaped (" etc.) in the wire format — unescape
|
|
686
|
+
before trying to parse as JSON."""
|
|
687
|
+
if not response_text:
|
|
688
|
+
return "—"
|
|
689
|
+
candidate = html.unescape(response_text).strip()
|
|
690
|
+
if candidate.startswith("{") and '"toolInvocations"' in candidate:
|
|
691
|
+
try:
|
|
692
|
+
obj = json.loads(candidate)
|
|
693
|
+
tools = obj.get("toolInvocations") or []
|
|
694
|
+
content = (obj.get("content") or "").strip()
|
|
695
|
+
if tools:
|
|
696
|
+
first = tools[0].get("function") or {}
|
|
697
|
+
name = first.get("name") or "?"
|
|
698
|
+
args_raw = first.get("arguments") or ""
|
|
699
|
+
arg_summary = ""
|
|
700
|
+
try:
|
|
701
|
+
args_obj = json.loads(args_raw) if isinstance(args_raw, str) else args_raw
|
|
702
|
+
if isinstance(args_obj, dict) and args_obj:
|
|
703
|
+
k, v = next(iter(args_obj.items()))
|
|
704
|
+
v_str = str(v)
|
|
705
|
+
if len(v_str) > 60:
|
|
706
|
+
v_str = v_str[:57] + "…"
|
|
707
|
+
arg_summary = f'{k}="{v_str}"'
|
|
708
|
+
except (ValueError, TypeError):
|
|
709
|
+
pass
|
|
710
|
+
prefix = "no user text" if not content else f'"{_truncate(content, 60)}"'
|
|
711
|
+
n = len(tools)
|
|
712
|
+
call_word = "tool call" if n == 1 else "tool calls"
|
|
713
|
+
return f"{prefix}; {n} {call_word} → {name}({arg_summary})"
|
|
714
|
+
except (ValueError, TypeError):
|
|
715
|
+
pass
|
|
716
|
+
return f'"{_truncate(response_text, 140)}"'
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _float_or_none(v) -> Optional[float]:
|
|
720
|
+
try:
|
|
721
|
+
return float(v)
|
|
722
|
+
except (ValueError, TypeError):
|
|
723
|
+
return None
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _trust_line(g: dict) -> str:
|
|
727
|
+
"""Summarize trust signals. TOXICITY sub-categories (parented on the
|
|
728
|
+
quality row) carry a dedicated `safety_score` category in the 0-1 range
|
|
729
|
+
(1.0 = clean). Non-`safety_score` sub-categories are the 8 hazard axes
|
|
730
|
+
(profanity, hate, violence, …); report the max of those when a detection
|
|
731
|
+
fires so the reader sees which axis tripped.
|
|
732
|
+
Non-TOXICITY detectors (InstructionAdherence, etc.) are parented directly
|
|
733
|
+
on the generation and render as textual verdicts."""
|
|
734
|
+
quality = g.get("quality") or []
|
|
735
|
+
cats = g.get("categories") or []
|
|
736
|
+
if not quality and not cats:
|
|
737
|
+
return "— (no quality/category rows)"
|
|
738
|
+
parts: List[str] = []
|
|
739
|
+
for q in quality:
|
|
740
|
+
subs = q.get("_toxicity_subcategories") or []
|
|
741
|
+
detected = str(q.get("isToxicityDetected__c", "")).lower() == "true"
|
|
742
|
+
safety: Optional[float] = None
|
|
743
|
+
hazard_subs: List[tuple] = []
|
|
744
|
+
for s in subs:
|
|
745
|
+
if (s.get("detectorType__c") or "").upper() != "TOXICITY":
|
|
746
|
+
continue
|
|
747
|
+
name = s.get("category__c") or ""
|
|
748
|
+
val = _float_or_none(s.get("value__c"))
|
|
749
|
+
if val is None:
|
|
750
|
+
continue
|
|
751
|
+
if name == "safety_score":
|
|
752
|
+
safety = val
|
|
753
|
+
else:
|
|
754
|
+
hazard_subs.append((name, val))
|
|
755
|
+
status = "detected" if detected else "clean"
|
|
756
|
+
safety_str = f"{safety:.2f}" if safety is not None else "—"
|
|
757
|
+
if not detected and hazard_subs and all(v == 0 for _, v in hazard_subs):
|
|
758
|
+
detail = f" — all {len(hazard_subs)} sub-categories 0.00"
|
|
759
|
+
elif hazard_subs:
|
|
760
|
+
top_name, top_val = max(hazard_subs, key=lambda kv: kv[1])
|
|
761
|
+
detail = f" — max {top_name}={top_val:.2f}"
|
|
762
|
+
else:
|
|
763
|
+
detail = ""
|
|
764
|
+
parts.append(f"TOXICITY safety_score={safety_str} ({status}{detail})")
|
|
765
|
+
# Non-TOXICITY detectors (generation-direct).
|
|
766
|
+
for c in cats:
|
|
767
|
+
dtype = (c.get("detectorType__c") or "?").strip()
|
|
768
|
+
if dtype.upper() == "TOXICITY":
|
|
769
|
+
continue
|
|
770
|
+
val = c.get("value__c")
|
|
771
|
+
parts.append(f"{dtype}: {_truncate(val, 80)}")
|
|
772
|
+
return "; ".join(parts) if parts else "—"
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _render_generation(g: dict, prefix: str, k_branch: str, k_cont: str,
|
|
776
|
+
*, step_name: Optional[str] = None) -> List[str]:
|
|
777
|
+
"""Generation node: 3 fixed children (decoded / finish_reason+masked / trust).
|
|
778
|
+
Header is naked (id only); `response_id` and `feature` live in the ID reference
|
|
779
|
+
block / on the sibling gateway_request line."""
|
|
780
|
+
lines = [
|
|
781
|
+
f"{prefix}{k_branch} generation {_short(g.get('generation_id'))}"
|
|
782
|
+
]
|
|
783
|
+
role = _role_label_for(step_name)
|
|
784
|
+
decoded = _decoded_line(g.get("response_text"))
|
|
785
|
+
if role:
|
|
786
|
+
decoded_line = f"decoded: {decoded} ({role})"
|
|
787
|
+
else:
|
|
788
|
+
decoded_line = f"decoded: {decoded}"
|
|
789
|
+
masked = g.get("masked_response_text")
|
|
790
|
+
masked_disp = _truncate(masked, 60) if masked else "—"
|
|
791
|
+
finish_reason = _parse_finish_reason(g.get("response_parameters")) or "—"
|
|
792
|
+
gprefix = f"{prefix}{k_cont}"
|
|
793
|
+
lines.append(f"{gprefix}├── {decoded_line}")
|
|
794
|
+
lines.append(f"{gprefix}├── finish_reason={finish_reason} masked={masked_disp}")
|
|
795
|
+
lines.append(f"{gprefix}└── trust: {_trust_line(g)}")
|
|
796
|
+
return lines
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def _render_gw_declared(g: dict, prefix: str, k_branch: str, k_cont: str) -> List[str]:
|
|
800
|
+
tokens = (
|
|
801
|
+
f"prompt={int(g.get('prompt_tokens') or 0)} "
|
|
802
|
+
f"completion={int(g.get('completion_tokens') or 0)} "
|
|
803
|
+
f"total={int(g.get('total_tokens') or 0)}"
|
|
804
|
+
)
|
|
805
|
+
tags = g.get("tags") or []
|
|
806
|
+
md = g.get("metadata") or []
|
|
807
|
+
recs = g.get("records") or []
|
|
808
|
+
llm = g.get("llm") or []
|
|
809
|
+
audit_line = (
|
|
810
|
+
f"audit: tags={len(tags)} metadata={len(md)} records={len(recs)} llm={len(llm)}"
|
|
811
|
+
if (tags or md or recs or llm)
|
|
812
|
+
else "audit: (none)"
|
|
813
|
+
)
|
|
814
|
+
return [
|
|
815
|
+
f"{prefix}{k_branch} gateway_request [declared] "
|
|
816
|
+
f"{_short(g.get('gateway_request_id'))} "
|
|
817
|
+
f"feature={g.get('feature') or '—'} "
|
|
818
|
+
f"model={g.get('model') or '—'} "
|
|
819
|
+
f"provider={g.get('provider') or '—'}",
|
|
820
|
+
f"{prefix}{k_cont}├── tokens: {tokens}",
|
|
821
|
+
f"{prefix}{k_cont}└── {audit_line}",
|
|
822
|
+
]
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _section_per_turn_summary(sess: dict, start_dt: Optional[datetime]) -> List[str]:
|
|
826
|
+
lines: List[str] = [
|
|
827
|
+
"## Per-turn summary",
|
|
828
|
+
"",
|
|
829
|
+
"| Interaction | Type | Start offset | Duration | Steps | "
|
|
830
|
+
"GW declared | GW ts_window | USER → AGENT |",
|
|
831
|
+
"|---|---|---|---|---|---|---|---|",
|
|
832
|
+
]
|
|
833
|
+
for iv in sess.get("interactions", []):
|
|
834
|
+
iv_type = iv.get("type") or "?"
|
|
835
|
+
start_off = _fmt_offset(iv.get("start_ts"), start_dt)
|
|
836
|
+
dur = _fmt_duration_ms(iv.get("start_ts"), iv.get("end_ts"))
|
|
837
|
+
step_count = len(iv.get("steps", []))
|
|
838
|
+
declared_gws = sum(
|
|
839
|
+
1 for st in iv.get("steps", [])
|
|
840
|
+
if st.get("gateway_request")
|
|
841
|
+
and st["gateway_request"].get("binding_method") == "declared"
|
|
842
|
+
)
|
|
843
|
+
tsw_gws = len(iv.get("timestamp_bound_gateway_calls", []))
|
|
844
|
+
user_msg = next((m for m in iv.get("messages", []) if m.get("role") == "USER"), None)
|
|
845
|
+
agent_msg = next((m for m in iv.get("messages", []) if m.get("role") == "AGENT"), None)
|
|
846
|
+
ut = _truncate(user_msg.get("text") if user_msg else None, 40)
|
|
847
|
+
at = _truncate(agent_msg.get("text") if agent_msg else None, 40)
|
|
848
|
+
lines.append(
|
|
849
|
+
f"| `{_short(iv.get('id'))}` | {iv_type} | {start_off} | {dur} | "
|
|
850
|
+
f"{step_count} | {declared_gws} | {tsw_gws} | {ut} → {at} |"
|
|
851
|
+
)
|
|
852
|
+
lines.append("")
|
|
853
|
+
return lines
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
# ---- visual analysis (mermaid) -------------------------------------------
|
|
857
|
+
|
|
858
|
+
# Chars that break mermaid label parsing when bare; quote-wrap the string
|
|
859
|
+
# when any of these show up. Underscore+digit tokens in topic strings are
|
|
860
|
+
# legal inside mermaid node labels — only syntax-significant chars need
|
|
861
|
+
# escaping here.
|
|
862
|
+
_MERMAID_LABEL_SPECIALS = set(',()<>:"#')
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
def _escape_mermaid_label(s: str) -> str:
|
|
866
|
+
if not s:
|
|
867
|
+
return '""'
|
|
868
|
+
if any(ch in _MERMAID_LABEL_SPECIALS for ch in s):
|
|
869
|
+
return '"' + s.replace('"', '\\"') + '"'
|
|
870
|
+
return s
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _sequence_msg(s: str) -> str:
|
|
874
|
+
"""Sanitize free-form text for the right-hand side of a sequenceDiagram
|
|
875
|
+
arrow. Mermaid parses `A->>B: <text>` up to newline — the text itself is
|
|
876
|
+
unquoted, so wrapping in `"..."` (as node-label escape does) renders the
|
|
877
|
+
quotes literally. We just strip newlines and escape the one char that
|
|
878
|
+
ends the message field (semicolon is the statement terminator)."""
|
|
879
|
+
if not s:
|
|
880
|
+
return "—"
|
|
881
|
+
return s.replace("\n", " ").replace(";", ",").strip()
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def _flowchart_edge_label(s: str) -> str:
|
|
885
|
+
"""Sanitize free-form text for a flowchart edge label `A -->|label| B`.
|
|
886
|
+
The delimiter is the pipe itself, so a literal `|` in the label shatters
|
|
887
|
+
the parser. Square brackets also clash with node-shape syntax. Strip/
|
|
888
|
+
substitute these; other specials (commas, colons, parens) are fine
|
|
889
|
+
inside quoted labels, which `_escape_mermaid_label` handles."""
|
|
890
|
+
if not s:
|
|
891
|
+
return ""
|
|
892
|
+
cleaned = s.replace("|", "/").replace("[", "(").replace("]", ")")
|
|
893
|
+
return _escape_mermaid_label(cleaned)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _step_display_name(st: dict) -> str:
|
|
897
|
+
"""Short, mermaid-safe row label for a step.
|
|
898
|
+
|
|
899
|
+
- Strip `AiCopilot__` prefix on LLM prompts.
|
|
900
|
+
- For ACTION_STEP names shaped `Topic_xxx.AGNT_Action_yyy`, keep the
|
|
901
|
+
action-name portion after the last `.` so the row reads as the
|
|
902
|
+
invoked action, not the topic.
|
|
903
|
+
"""
|
|
904
|
+
name = st.get("name") or st.get("type") or "?"
|
|
905
|
+
if name.startswith("AiCopilot__"):
|
|
906
|
+
name = name[len("AiCopilot__"):]
|
|
907
|
+
if st.get("type") == "ACTION_STEP" and "." in name:
|
|
908
|
+
name = name.rsplit(".", 1)[-1]
|
|
909
|
+
# Gantt task names cannot contain colons (task syntax is `label : id, start, dur`).
|
|
910
|
+
return name.replace(":", "·")
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _iter_turns(sess: dict) -> List[dict]:
|
|
914
|
+
return [iv for iv in sess.get("interactions", []) if iv.get("type") == "TURN"]
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def _mermaid_gantt(
|
|
918
|
+
sess: dict,
|
|
919
|
+
start_dt: Optional[datetime],
|
|
920
|
+
llm_calls: Optional[List[dict]] = None,
|
|
921
|
+
) -> List[str]:
|
|
922
|
+
"""Gantt chart — wall-clock timeline.
|
|
923
|
+
|
|
924
|
+
Rows per turn (ACTION steps tagged :crit). When `llm_calls` is
|
|
925
|
+
non-empty, a trailing "LLM calls" section renders per-call rows
|
|
926
|
+
tagged by region-class (:active = cross_region, :done = same-
|
|
927
|
+
region, plain = unknown). Mermaid's built-in 3-class palette is
|
|
928
|
+
enough signal for v1; full per-region color would need a classDef
|
|
929
|
+
block (deferred — see plan OOS-2.A).
|
|
930
|
+
|
|
931
|
+
Kill-criterion: <3 total rows after dropping zero-duration steps
|
|
932
|
+
AND no LLM-call rows → return []. If the step set is thin but the
|
|
933
|
+
call set is non-empty, we still render (the call rows are the
|
|
934
|
+
whole point of the overlay).
|
|
935
|
+
"""
|
|
936
|
+
if start_dt is None:
|
|
937
|
+
return []
|
|
938
|
+
rows: List[tuple] = [] # (section_label, row_label, start_ms, end_ms, is_action)
|
|
939
|
+
turns = _iter_turns(sess)
|
|
940
|
+
for iv in sess.get("interactions", []):
|
|
941
|
+
if iv.get("type") != "TURN":
|
|
942
|
+
continue
|
|
943
|
+
topic = iv.get("topic") or "(no topic)"
|
|
944
|
+
section = f"Turn {turns.index(iv) + 1} ({topic})"
|
|
945
|
+
for st in iv.get("steps", []):
|
|
946
|
+
if st.get("type") == "SESSION_END":
|
|
947
|
+
continue
|
|
948
|
+
s_dt = _parse_iso(st.get("start_ts"))
|
|
949
|
+
e_dt = _parse_iso(st.get("end_ts"))
|
|
950
|
+
if s_dt is None or e_dt is None:
|
|
951
|
+
continue
|
|
952
|
+
s_ms = int((s_dt - start_dt).total_seconds() * 1000)
|
|
953
|
+
e_ms = int((e_dt - start_dt).total_seconds() * 1000)
|
|
954
|
+
if e_ms <= s_ms:
|
|
955
|
+
continue # skip zero-duration (e.g. TOPIC_STEP markers)
|
|
956
|
+
rows.append((section, _step_display_name(st), s_ms, e_ms,
|
|
957
|
+
st.get("type") == "ACTION_STEP"))
|
|
958
|
+
|
|
959
|
+
# LLM-call rows — one per gateway call, derived from _llm_calls.json.
|
|
960
|
+
# Rendered in a trailing section so they don't interleave with step
|
|
961
|
+
# rows (mermaid sections don't sort across boundaries).
|
|
962
|
+
call_rows: List[tuple] = [] # (row_label, start_ms, end_ms, class_tag)
|
|
963
|
+
for call in (llm_calls or []):
|
|
964
|
+
t_iso = call.get("_time")
|
|
965
|
+
dur_ms = call.get("duration_ms")
|
|
966
|
+
if not t_iso or dur_ms is None:
|
|
967
|
+
continue
|
|
968
|
+
t_dt = _parse_iso(t_iso)
|
|
969
|
+
if t_dt is None:
|
|
970
|
+
continue
|
|
971
|
+
s_ms = int((t_dt - start_dt).total_seconds() * 1000)
|
|
972
|
+
e_ms = s_ms + int(dur_ms)
|
|
973
|
+
if e_ms <= s_ms:
|
|
974
|
+
continue
|
|
975
|
+
# class_tag: cross_region=True → :active (mermaid's yellow),
|
|
976
|
+
# cross_region=False → :done (grey), unknown/None → no tag
|
|
977
|
+
# (default blue). Operators can scan for the yellow bars as
|
|
978
|
+
# the cross-region outliers.
|
|
979
|
+
cr = call.get("cross_region")
|
|
980
|
+
if cr is True:
|
|
981
|
+
class_tag = ":active, "
|
|
982
|
+
elif cr is False:
|
|
983
|
+
class_tag = ":done, "
|
|
984
|
+
else:
|
|
985
|
+
class_tag = ":"
|
|
986
|
+
model = call.get("model_name") or "call"
|
|
987
|
+
region = call.get("routing_decision") or "—"
|
|
988
|
+
label = _escape_mermaid_label(f"{model} [{region}]")
|
|
989
|
+
call_rows.append((label, s_ms, e_ms, class_tag))
|
|
990
|
+
|
|
991
|
+
if len(rows) < 3 and not call_rows:
|
|
992
|
+
return []
|
|
993
|
+
|
|
994
|
+
# axisFormat: `%M:%S` renders as minutes:seconds via d3. Our offsets
|
|
995
|
+
# are ms-from-start passed through `dateFormat x` (unix epoch); d3
|
|
996
|
+
# treats them as Jan 1 1970 timestamps, so for any session under
|
|
997
|
+
# 1 hour this reads cleanly as m:ss elapsed. `%L` (ms-of-second)
|
|
998
|
+
# always rendered 000 because ticks land at second boundaries.
|
|
999
|
+
# The leading `section anchor` + 1ms milestone forces the x-axis to
|
|
1000
|
+
# start at 00:00 — mermaid derives axis min from the smallest task
|
|
1001
|
+
# timestamp, and without the anchor that's the first step's offset
|
|
1002
|
+
# (~5s into a typical session), making the axis misleadingly slide.
|
|
1003
|
+
lines = [
|
|
1004
|
+
"```mermaid",
|
|
1005
|
+
"gantt",
|
|
1006
|
+
" title Session timeline (m:ss from start)",
|
|
1007
|
+
" dateFormat x",
|
|
1008
|
+
" axisFormat %M:%S",
|
|
1009
|
+
" section Session start",
|
|
1010
|
+
" t0 :milestone, 0, 0",
|
|
1011
|
+
]
|
|
1012
|
+
current_section: Optional[str] = None
|
|
1013
|
+
for section, label, s_ms, e_ms, is_action in rows:
|
|
1014
|
+
if section != current_section:
|
|
1015
|
+
lines.append(f" section {section}")
|
|
1016
|
+
current_section = section
|
|
1017
|
+
# Mermaid gantt task: `<name> :[tag,] <start>, <end>` — single colon,
|
|
1018
|
+
# tag is the first comma-separated field after it. Two colons (the
|
|
1019
|
+
# `:crit:` shape) is a parse error.
|
|
1020
|
+
prefix = ":crit, " if is_action else ":"
|
|
1021
|
+
lines.append(f" {label} {prefix}{s_ms}, {e_ms}")
|
|
1022
|
+
if call_rows:
|
|
1023
|
+
lines.append(" section LLM calls")
|
|
1024
|
+
for label, s_ms, e_ms, class_tag in call_rows:
|
|
1025
|
+
lines.append(f" {label} {class_tag}{s_ms}, {e_ms}")
|
|
1026
|
+
lines.append("```")
|
|
1027
|
+
lines.append("")
|
|
1028
|
+
return lines
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def _action_short_name(st: dict) -> str:
|
|
1032
|
+
name = st.get("name") or ""
|
|
1033
|
+
if "." in name:
|
|
1034
|
+
name = name.rsplit(".", 1)[-1]
|
|
1035
|
+
return name or "action"
|
|
1036
|
+
|
|
1037
|
+
|
|
1038
|
+
def _mermaid_sequence_per_turn(iv: dict, start_dt: Optional[datetime],
|
|
1039
|
+
turn_no: int) -> List[str]:
|
|
1040
|
+
steps = iv.get("steps", [])
|
|
1041
|
+
has_error = any(st.get("error_text") for st in steps)
|
|
1042
|
+
action_steps = [st for st in steps if st.get("type") == "ACTION_STEP"]
|
|
1043
|
+
if not has_error and len(action_steps) < 2:
|
|
1044
|
+
return []
|
|
1045
|
+
|
|
1046
|
+
topic = iv.get("topic") or "(no topic)"
|
|
1047
|
+
heading = f"### Turn {turn_no} ({topic}) — control flow"
|
|
1048
|
+
lines: List[str] = [heading, "", "```mermaid", "sequenceDiagram"]
|
|
1049
|
+
|
|
1050
|
+
# Participants: U, P, L are always present; actions get A1, A2, …
|
|
1051
|
+
lines.append(" participant U as USER")
|
|
1052
|
+
lines.append(" participant P as Planner")
|
|
1053
|
+
lines.append(" participant L as LLM Gateway")
|
|
1054
|
+
action_alias: Dict[str, str] = {} # step id → alias
|
|
1055
|
+
for i, st in enumerate(action_steps, start=1):
|
|
1056
|
+
alias = f"A{i}"
|
|
1057
|
+
action_alias[st.get("id") or f"_a{i}"] = alias
|
|
1058
|
+
label = _escape_mermaid_label(_action_short_name(st))
|
|
1059
|
+
lines.append(f" participant {alias} as {label}")
|
|
1060
|
+
|
|
1061
|
+
# User → Planner (utterance).
|
|
1062
|
+
user_msg = next((m for m in iv.get("messages", []) if m.get("role") == "USER"), None)
|
|
1063
|
+
user_text = _truncate(user_msg.get("text") if user_msg else None, 40)
|
|
1064
|
+
if user_msg:
|
|
1065
|
+
lines.append(f" U->>P: {_sequence_msg(user_text)}")
|
|
1066
|
+
|
|
1067
|
+
# Walk steps in order. LLM → L, ACTION → A<n>, errors → Note over.
|
|
1068
|
+
for st in steps:
|
|
1069
|
+
stype = st.get("type")
|
|
1070
|
+
if stype == "LLM_STEP":
|
|
1071
|
+
label = _step_display_name(st)
|
|
1072
|
+
gw = st.get("gateway_request") or {}
|
|
1073
|
+
model = gw.get("model")
|
|
1074
|
+
suffix = f" ({model})" if model else ""
|
|
1075
|
+
lines.append(f" P->>L: {_sequence_msg(label + suffix)}")
|
|
1076
|
+
dur = _fmt_duration_ms(st.get("start_ts"), st.get("end_ts"))
|
|
1077
|
+
if st.get("error_text"):
|
|
1078
|
+
lines.append(f" Note over L: ERROR in {dur}")
|
|
1079
|
+
else:
|
|
1080
|
+
lines.append(f" L-->>P: ok ({dur})")
|
|
1081
|
+
elif stype == "ACTION_STEP":
|
|
1082
|
+
alias = action_alias.get(st.get("id") or "", "A?")
|
|
1083
|
+
dur = _fmt_duration_ms(st.get("start_ts"), st.get("end_ts"))
|
|
1084
|
+
lines.append(f" P->>{alias}: invoke")
|
|
1085
|
+
lines.append(f" {alias}-->>P: result ({dur})")
|
|
1086
|
+
|
|
1087
|
+
# Agent reply (if any).
|
|
1088
|
+
agent_msg = next((m for m in iv.get("messages", []) if m.get("role") == "AGENT"), None)
|
|
1089
|
+
if agent_msg:
|
|
1090
|
+
agent_text = _truncate(agent_msg.get("text"), 40)
|
|
1091
|
+
lines.append(f" P-->>U: {_sequence_msg(agent_text)}")
|
|
1092
|
+
|
|
1093
|
+
lines.append("```")
|
|
1094
|
+
lines.append("")
|
|
1095
|
+
return lines
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def _mermaid_topic_flowchart(sess: dict) -> List[str]:
|
|
1099
|
+
turns = _iter_turns(sess)
|
|
1100
|
+
if not turns:
|
|
1101
|
+
return []
|
|
1102
|
+
# Order-preserving unique topics.
|
|
1103
|
+
topic_ids: Dict[str, str] = {}
|
|
1104
|
+
for iv in turns:
|
|
1105
|
+
t = iv.get("topic") or "(no topic)"
|
|
1106
|
+
if t not in topic_ids:
|
|
1107
|
+
topic_ids[t] = f"T{len(topic_ids) + 1}"
|
|
1108
|
+
if len(topic_ids) == len(turns):
|
|
1109
|
+
return [] # linear — no repeats, skip
|
|
1110
|
+
|
|
1111
|
+
lines: List[str] = ["```mermaid", "flowchart LR"]
|
|
1112
|
+
# Node declarations.
|
|
1113
|
+
for topic, node_id in topic_ids.items():
|
|
1114
|
+
lines.append(f" {node_id}[{_escape_mermaid_label(topic)}]")
|
|
1115
|
+
|
|
1116
|
+
# Edges: one per turn transition, labelled by the user utterance that
|
|
1117
|
+
# drove into that turn. The first turn has no predecessor so we just
|
|
1118
|
+
# declare the node; edges start from turn 2.
|
|
1119
|
+
prev_topic = turns[0].get("topic") or "(no topic)"
|
|
1120
|
+
for iv in turns[1:]:
|
|
1121
|
+
cur_topic = iv.get("topic") or "(no topic)"
|
|
1122
|
+
user_msg = next((m for m in iv.get("messages", []) if m.get("role") == "USER"), None)
|
|
1123
|
+
utter = _truncate(user_msg.get("text") if user_msg else None, 30)
|
|
1124
|
+
src = topic_ids[prev_topic]
|
|
1125
|
+
dst = topic_ids[cur_topic]
|
|
1126
|
+
label = _flowchart_edge_label(utter) if utter and utter != "—" else ""
|
|
1127
|
+
arrow = f" {src} -->|{label}| {dst}" if label else f" {src} --> {dst}"
|
|
1128
|
+
lines.append(arrow)
|
|
1129
|
+
prev_topic = cur_topic
|
|
1130
|
+
|
|
1131
|
+
# Error class for topics whose turn ended with an error_text.
|
|
1132
|
+
errored_topic_ids: List[str] = []
|
|
1133
|
+
for iv in turns:
|
|
1134
|
+
if any(st.get("error_text") for st in iv.get("steps", [])):
|
|
1135
|
+
tid = topic_ids[iv.get("topic") or "(no topic)"]
|
|
1136
|
+
if tid not in errored_topic_ids:
|
|
1137
|
+
errored_topic_ids.append(tid)
|
|
1138
|
+
if errored_topic_ids:
|
|
1139
|
+
lines.append(f" classDef err fill:#fee,stroke:#c00")
|
|
1140
|
+
for tid in errored_topic_ids:
|
|
1141
|
+
lines.append(f" class {tid} err")
|
|
1142
|
+
|
|
1143
|
+
lines.append("```")
|
|
1144
|
+
lines.append("")
|
|
1145
|
+
return lines
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def _mermaid_token_pie(sess: dict) -> List[str]:
|
|
1149
|
+
buckets: Dict[str, int] = {}
|
|
1150
|
+
total = 0
|
|
1151
|
+
for iv in sess.get("interactions", []):
|
|
1152
|
+
for st in iv.get("steps", []):
|
|
1153
|
+
gw = st.get("gateway_request") or {}
|
|
1154
|
+
p = int(gw.get("prompt_tokens") or 0)
|
|
1155
|
+
c = int(gw.get("completion_tokens") or 0)
|
|
1156
|
+
tok = p + c
|
|
1157
|
+
if tok <= 0:
|
|
1158
|
+
continue
|
|
1159
|
+
role = _role_label_for(st.get("name")) or "other"
|
|
1160
|
+
buckets[role] = buckets.get(role, 0) + tok
|
|
1161
|
+
total += tok
|
|
1162
|
+
if total < 1000 or not buckets:
|
|
1163
|
+
return []
|
|
1164
|
+
|
|
1165
|
+
lines = [
|
|
1166
|
+
"```mermaid",
|
|
1167
|
+
"pie showData",
|
|
1168
|
+
f" title Prompt-role token attribution (total = {total:,})",
|
|
1169
|
+
]
|
|
1170
|
+
# Largest slice first reads better in most renderers.
|
|
1171
|
+
for role, n in sorted(buckets.items(), key=lambda kv: -kv[1]):
|
|
1172
|
+
lines.append(f' "{role}" : {n}')
|
|
1173
|
+
lines.append("```")
|
|
1174
|
+
lines.append("")
|
|
1175
|
+
return lines
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
def _section_visual_analysis(
|
|
1179
|
+
sess: dict,
|
|
1180
|
+
start_dt: Optional[datetime],
|
|
1181
|
+
llm_calls: Optional[List[dict]] = None,
|
|
1182
|
+
) -> List[str]:
|
|
1183
|
+
blocks: List[List[str]] = []
|
|
1184
|
+
gantt = _mermaid_gantt(sess, start_dt, llm_calls=llm_calls)
|
|
1185
|
+
if gantt:
|
|
1186
|
+
blocks.append(gantt)
|
|
1187
|
+
# Per-turn sequence diagrams (at most one per turn; most turns skip).
|
|
1188
|
+
turns = _iter_turns(sess)
|
|
1189
|
+
for idx, iv in enumerate(turns, start=1):
|
|
1190
|
+
seq = _mermaid_sequence_per_turn(iv, start_dt, idx)
|
|
1191
|
+
if seq:
|
|
1192
|
+
blocks.append(seq)
|
|
1193
|
+
flow = _mermaid_topic_flowchart(sess)
|
|
1194
|
+
if flow:
|
|
1195
|
+
blocks.append(flow)
|
|
1196
|
+
pie = _mermaid_token_pie(sess)
|
|
1197
|
+
if pie:
|
|
1198
|
+
blocks.append(pie)
|
|
1199
|
+
|
|
1200
|
+
if not blocks:
|
|
1201
|
+
return []
|
|
1202
|
+
lines: List[str] = ["## Visual analysis", ""]
|
|
1203
|
+
for b in blocks:
|
|
1204
|
+
lines.extend(b)
|
|
1205
|
+
return lines
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def _section_session_counts(sess: dict) -> List[str]:
|
|
1209
|
+
c = sess.get("counts", {})
|
|
1210
|
+
st_by_type = c.get("steps_by_type", {})
|
|
1211
|
+
gwb = c.get("gw_binding", {})
|
|
1212
|
+
lines: List[str] = [
|
|
1213
|
+
"## Session counts",
|
|
1214
|
+
"",
|
|
1215
|
+
"| metric | value |",
|
|
1216
|
+
"|---|---|",
|
|
1217
|
+
f"| interactions | {c.get('interactions_total', 0)} |",
|
|
1218
|
+
f"| steps | {c.get('steps_total', 0)} |",
|
|
1219
|
+
f"| llm_steps | {st_by_type.get('LLM_STEP', 0)} |",
|
|
1220
|
+
f"| action_steps | {st_by_type.get('ACTION_STEP', 0)} |",
|
|
1221
|
+
f"| gateway_requests | {c.get('gateway_requests', 0)} |",
|
|
1222
|
+
f"| gateway_responses | {c.get('gateway_responses', 0)} |",
|
|
1223
|
+
f"| 1:1 invariant | {'✓' if c.get('audit_chain_1to1_ok') else '✗'} |",
|
|
1224
|
+
f"| gw declared | {gwb.get('declared', 0)} |",
|
|
1225
|
+
f"| gw timestamp_window | {gwb.get('timestamp_window', 0)} |",
|
|
1226
|
+
f"| gw unbound | {gwb.get('unbound', 0)} |",
|
|
1227
|
+
]
|
|
1228
|
+
if gwb.get("declared_collisions"):
|
|
1229
|
+
lines.append(f"| ⚠ declared_collisions | {gwb['declared_collisions']} |")
|
|
1230
|
+
lines.append("")
|
|
1231
|
+
return lines
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def _section_empties_diagnostics(manifest: dict) -> List[str]:
|
|
1235
|
+
"""Operator-actionable: lift `_unavailable_reason` verbatim from the manifest."""
|
|
1236
|
+
queries = manifest.get("queries", []) if manifest else []
|
|
1237
|
+
empties = [
|
|
1238
|
+
q for q in queries
|
|
1239
|
+
if q.get("rows") == 0 and q.get("_unavailable_reason")
|
|
1240
|
+
]
|
|
1241
|
+
if not empties:
|
|
1242
|
+
return []
|
|
1243
|
+
lines: List[str] = [
|
|
1244
|
+
"## Empties diagnostics",
|
|
1245
|
+
"",
|
|
1246
|
+
"DMOs that returned zero rows, with the reason lifted verbatim from the manifest:",
|
|
1247
|
+
"",
|
|
1248
|
+
"| DMO | Reason |",
|
|
1249
|
+
"|---|---|",
|
|
1250
|
+
]
|
|
1251
|
+
for q in empties:
|
|
1252
|
+
name = q.get("name") or "?"
|
|
1253
|
+
# Pipe characters in reason text would break markdown tables.
|
|
1254
|
+
reason = str(q.get("_unavailable_reason") or "").replace("|", "\\|")
|
|
1255
|
+
lines.append(f"| {name} | {reason} |")
|
|
1256
|
+
lines.append("")
|
|
1257
|
+
return lines
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def _section_catalog(tree: dict) -> List[str]:
|
|
1261
|
+
catalog = tree.get("catalog", {}) or {}
|
|
1262
|
+
agents = ", ".join(catalog.get("agents_observed", []) or []) or "—"
|
|
1263
|
+
return [
|
|
1264
|
+
"## Catalog (session-filtered)",
|
|
1265
|
+
"",
|
|
1266
|
+
f"- TagDefinitions: {len(catalog.get('tag_definitions', []) or [])}",
|
|
1267
|
+
f"- TagDefinitionAssociations: "
|
|
1268
|
+
f"{len(catalog.get('tag_definition_associations', []) or [])} (agents: {agents})",
|
|
1269
|
+
f"- Tags: {len(catalog.get('tags', []) or [])}",
|
|
1270
|
+
"",
|
|
1271
|
+
]
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
# ---- top-level render + entry points -------------------------------------
|
|
1275
|
+
|
|
1276
|
+
def render(
|
|
1277
|
+
tree: dict,
|
|
1278
|
+
manifest: Optional[dict] = None,
|
|
1279
|
+
session_dir: Optional[Path] = None,
|
|
1280
|
+
*,
|
|
1281
|
+
show_prompts: bool = False,
|
|
1282
|
+
) -> str:
|
|
1283
|
+
"""Produce the summary markdown for a single session.
|
|
1284
|
+
|
|
1285
|
+
Branches on tree shape:
|
|
1286
|
+
- gateway-direct tree (``_source == "gateway_direct"``) → identity +
|
|
1287
|
+
lag banner + gateway-chain table + per-call detail; skips the
|
|
1288
|
+
Interaction-dependent sections.
|
|
1289
|
+
- minimal tree (session-not-found) → short markdown with just identity.
|
|
1290
|
+
- full tree → multi-section summary (see SKILL.md for the full list).
|
|
1291
|
+
|
|
1292
|
+
``session_dir`` is reserved for callers that want the renderer to look
|
|
1293
|
+
up artifacts beside the tree on disk. The standalone d360 skill produces
|
|
1294
|
+
no runtime-telemetry rollups — DC alone doesn't expose per-turn LLM
|
|
1295
|
+
latency in a useful form — so when ``None`` (the test-friendly default),
|
|
1296
|
+
the gantt simply draws without the LLM-call overlay.
|
|
1297
|
+
|
|
1298
|
+
``show_prompts`` (opt-in): when True, the full-tree branch appends
|
|
1299
|
+
a "Planner LLM calls" section with full prompt + response text per
|
|
1300
|
+
LLM call. Default False — the section can add hundreds of KB to the
|
|
1301
|
+
summary on multi-turn sessions.
|
|
1302
|
+
|
|
1303
|
+
Refuses incompatible tree schema versions (see `_assert_schema_version`).
|
|
1304
|
+
"""
|
|
1305
|
+
_assert_schema_version(tree)
|
|
1306
|
+
|
|
1307
|
+
# Gateway-direct branch — session resolved but STDM hierarchy hasn't
|
|
1308
|
+
# materialized yet. Handled before the has_interactions check because
|
|
1309
|
+
# the gateway-direct tree does set `session.interactions = []`.
|
|
1310
|
+
if tree.get("_source") == "gateway_direct":
|
|
1311
|
+
return _render_gateway_direct(tree, manifest, show_prompts=show_prompts)
|
|
1312
|
+
|
|
1313
|
+
sess = tree.get("session") or {}
|
|
1314
|
+
sid = sess.get("id") or "<unknown>"
|
|
1315
|
+
has_interactions = "interactions" in sess
|
|
1316
|
+
|
|
1317
|
+
# Minimal-tree early return.
|
|
1318
|
+
if not has_interactions:
|
|
1319
|
+
return _render_minimal(sid, sess)
|
|
1320
|
+
|
|
1321
|
+
start_iso = sess.get("start_ts")
|
|
1322
|
+
start_dt = _parse_iso(start_iso)
|
|
1323
|
+
effective_end_iso, end_source = _derive_session_end(sess)
|
|
1324
|
+
|
|
1325
|
+
# The d360 skill produces no runtime-telemetry rollups — DC alone
|
|
1326
|
+
# doesn't expose per-turn LLM latency in a useful form. Visual
|
|
1327
|
+
# analysis falls back to its pre-rollup output.
|
|
1328
|
+
llm_calls: List[dict] = []
|
|
1329
|
+
|
|
1330
|
+
lines: List[str] = [f"# Session {sid}", ""]
|
|
1331
|
+
lines.extend(_section_session_identity(sess, effective_end_iso, end_source))
|
|
1332
|
+
# VariableText__c bootstrap (channel-mode diagnostic).
|
|
1333
|
+
lines.extend(_section_session_bootstrap(
|
|
1334
|
+
sess.get("identity") or {}, channel=sess.get("channel"),
|
|
1335
|
+
))
|
|
1336
|
+
lines.extend(_section_id_reference(sess))
|
|
1337
|
+
lines.extend(_section_transcript(sess, start_dt))
|
|
1338
|
+
lines.extend(_section_hierarchical_trace(sess, start_dt, effective_end_iso, end_source))
|
|
1339
|
+
lines.extend(_section_per_turn_summary(sess, start_dt))
|
|
1340
|
+
# Opt-in full prompt + response per LLM call. Off by
|
|
1341
|
+
# default — multi-turn sessions can produce hundreds of KB here.
|
|
1342
|
+
lines.extend(_section_planner_llm_calls(sess, show_prompts=show_prompts))
|
|
1343
|
+
lines.extend(_section_visual_analysis(sess, start_dt, llm_calls=llm_calls))
|
|
1344
|
+
lines.extend(_section_session_counts(sess))
|
|
1345
|
+
lines.extend(_section_empties_diagnostics(manifest or {}))
|
|
1346
|
+
lines.extend(_section_catalog(tree))
|
|
1347
|
+
return "\n".join(lines) + "\n"
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
def _render_minimal(sid: str, sess: dict) -> str:
|
|
1351
|
+
"""Short markdown for session-not-found minimal trees."""
|
|
1352
|
+
shape = (sess.get("counts") or {}).get("session_shape", "session_not_found")
|
|
1353
|
+
lines = [
|
|
1354
|
+
f"# Session {sid}",
|
|
1355
|
+
"",
|
|
1356
|
+
"## Session identity",
|
|
1357
|
+
"",
|
|
1358
|
+
"| Field | Value |",
|
|
1359
|
+
"|---|---|",
|
|
1360
|
+
f"| Session id | {sid} |",
|
|
1361
|
+
f"| Session shape | {shape} |",
|
|
1362
|
+
"",
|
|
1363
|
+
"_No interactions resolved in Data Cloud. Check the session id, or wait for "
|
|
1364
|
+
"STDM materialization._",
|
|
1365
|
+
"",
|
|
1366
|
+
]
|
|
1367
|
+
return "\n".join(lines) + "\n"
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
# Display-only cap on `prompt_text` inside per-call detail. The raw JSON on
|
|
1371
|
+
# disk (dc.gateway_requests.json) is authoritative and never truncated — the
|
|
1372
|
+
# assembler stores the full prompt, and this slice only applies to markdown.
|
|
1373
|
+
_PROMPT_DISPLAY_CAP_BYTES = 65536
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
def _fmt_token_count(value) -> str:
|
|
1377
|
+
"""Tolerate ints, stringified ints, and None/''/NOT_SET."""
|
|
1378
|
+
if value in (None, "", "NOT_SET"):
|
|
1379
|
+
return "—"
|
|
1380
|
+
return str(value)
|
|
1381
|
+
|
|
1382
|
+
|
|
1383
|
+
def _render_gateway_direct(tree: dict, manifest: Optional[dict],
|
|
1384
|
+
*, show_prompts: bool = False) -> str:
|
|
1385
|
+
"""Render the STDM-hasn't-materialized-yet view.
|
|
1386
|
+
|
|
1387
|
+
Sections:
|
|
1388
|
+
1. Session identity (reused)
|
|
1389
|
+
2. ID reference (reused; gracefully handles the empty
|
|
1390
|
+
interactions/participants on this path)
|
|
1391
|
+
3. STDM lag banner (gateway-direct specific)
|
|
1392
|
+
4. Gateway chain table (gateway-direct specific)
|
|
1393
|
+
5. Per-call detail (gateway-direct specific)
|
|
1394
|
+
6. Empties diagnostics (reused; reads manifest, not tree)
|
|
1395
|
+
7. Catalog (reused; catalog may be empty on a
|
|
1396
|
+
fresh-session gateway-direct tree)
|
|
1397
|
+
|
|
1398
|
+
Skipped: Transcript, Hierarchical trace, Per-turn summary, Session
|
|
1399
|
+
counts — all require Interaction rows that don't exist yet.
|
|
1400
|
+
|
|
1401
|
+
``show_prompts`` (default False): forwarded to per-call detail so the
|
|
1402
|
+
full prompt block only renders under ``--show-prompts``. The prior
|
|
1403
|
+
behavior unconditionally leaked prompts on this branch, contradicting
|
|
1404
|
+
the documented contract for ``dc._session_summary.md``.
|
|
1405
|
+
"""
|
|
1406
|
+
sess = tree.get("session") or {}
|
|
1407
|
+
sid = sess.get("id") or "<unknown>"
|
|
1408
|
+
start_iso = sess.get("start_ts")
|
|
1409
|
+
start_dt = _parse_iso(start_iso)
|
|
1410
|
+
# No derivation needed — sessions.end_ts is the only source on this path.
|
|
1411
|
+
effective_end_iso = sess.get("end_ts")
|
|
1412
|
+
|
|
1413
|
+
lines: List[str] = [f"# Session {sid}", ""]
|
|
1414
|
+
lines.extend(_section_session_identity(sess, effective_end_iso, None))
|
|
1415
|
+
# VariableText__c bootstrap (channel-mode diagnostic).
|
|
1416
|
+
lines.extend(_section_session_bootstrap(
|
|
1417
|
+
sess.get("identity") or {}, channel=sess.get("channel"),
|
|
1418
|
+
))
|
|
1419
|
+
lines.extend(_section_id_reference(sess))
|
|
1420
|
+
lines.extend(_section_stdm_lag_banner())
|
|
1421
|
+
lines.extend(_section_gateway_chain_table(sess, start_dt))
|
|
1422
|
+
lines.extend(_section_gateway_per_call_detail(sess, show_prompts=show_prompts))
|
|
1423
|
+
lines.extend(_section_empties_diagnostics(manifest or {}))
|
|
1424
|
+
lines.extend(_section_catalog(tree))
|
|
1425
|
+
return "\n".join(lines) + "\n"
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
def _section_stdm_lag_banner() -> List[str]:
|
|
1429
|
+
return [
|
|
1430
|
+
"## STDM materialization lag",
|
|
1431
|
+
"",
|
|
1432
|
+
"> **Note** STDM Interaction/Step/Message DMOs have not yet "
|
|
1433
|
+
"materialized for this session. The view below is the gateway chain "
|
|
1434
|
+
"harvested directly from Gateway DMOs (materialize in minutes). "
|
|
1435
|
+
"Re-run in 24–72h for the full hierarchical trace.",
|
|
1436
|
+
"",
|
|
1437
|
+
]
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def _section_gateway_chain_table(sess: dict,
|
|
1441
|
+
start_dt: Optional[datetime]) -> List[str]:
|
|
1442
|
+
chain = sess.get("gateway_chain") or []
|
|
1443
|
+
lines: List[str] = [
|
|
1444
|
+
"## Gateway chain",
|
|
1445
|
+
"",
|
|
1446
|
+
"| # | Request ts | Model | Provider | Prompt template | "
|
|
1447
|
+
"Prompt tok | Completion tok | Total tok | Response ts |",
|
|
1448
|
+
"|---|---|---|---|---|---|---|---|---|",
|
|
1449
|
+
]
|
|
1450
|
+
for i, call in enumerate(chain, start=1):
|
|
1451
|
+
req_offset = _fmt_offset(call.get("timestamp"), start_dt)
|
|
1452
|
+
resp_offset = _fmt_offset(
|
|
1453
|
+
(call.get("response") or {}).get("timestamp"), start_dt)
|
|
1454
|
+
model = call.get("model") or "—"
|
|
1455
|
+
provider = call.get("provider") or "—"
|
|
1456
|
+
template = call.get("prompt_template_dev_name") or "—"
|
|
1457
|
+
prompt_tok = _fmt_token_count(call.get("prompt_tokens"))
|
|
1458
|
+
completion_tok = _fmt_token_count(call.get("completion_tokens"))
|
|
1459
|
+
total_tok = _fmt_token_count(call.get("total_tokens"))
|
|
1460
|
+
lines.append(
|
|
1461
|
+
f"| {i} | {req_offset} | {model} | {provider} | {template} | "
|
|
1462
|
+
f"{prompt_tok} | {completion_tok} | {total_tok} | {resp_offset} |"
|
|
1463
|
+
)
|
|
1464
|
+
lines.append("")
|
|
1465
|
+
return lines
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def _pick_fence(text: str) -> str:
|
|
1469
|
+
"""Pick a backtick fence long enough to wrap `text` safely.
|
|
1470
|
+
|
|
1471
|
+
CommonMark lets a fenced code block use any run of 3+ backticks; the
|
|
1472
|
+
closing fence must be at least as long as the opening. LLM prompts
|
|
1473
|
+
routinely contain triple-backticks inside tool-use examples, so a
|
|
1474
|
+
hardcoded ``` fence closes early and corrupts the rest of the doc.
|
|
1475
|
+
"""
|
|
1476
|
+
if not isinstance(text, str) or not text:
|
|
1477
|
+
return "```"
|
|
1478
|
+
longest = 0
|
|
1479
|
+
run = 0
|
|
1480
|
+
for ch in text:
|
|
1481
|
+
if ch == "`":
|
|
1482
|
+
run += 1
|
|
1483
|
+
if run > longest:
|
|
1484
|
+
longest = run
|
|
1485
|
+
else:
|
|
1486
|
+
run = 0
|
|
1487
|
+
return "`" * max(3, longest + 1)
|
|
1488
|
+
|
|
1489
|
+
|
|
1490
|
+
def _capped_payload(text: Optional[str], note_source: str) -> tuple[str, bool, str]:
|
|
1491
|
+
"""Cap a payload string at `_PROMPT_DISPLAY_CAP_BYTES` for display.
|
|
1492
|
+
|
|
1493
|
+
Returns ``(body, truncated, source_note)``. Byte-length check so
|
|
1494
|
+
multi-byte chars don't blow through the limit when the renderer emits
|
|
1495
|
+
UTF-8 text. Slicing happens on the encoded form, then decodes with
|
|
1496
|
+
``errors="ignore"`` so we never split a multi-byte char mid-sequence.
|
|
1497
|
+
`source_note` names the on-disk file with the authoritative full text.
|
|
1498
|
+
"""
|
|
1499
|
+
if not isinstance(text, str) or not text:
|
|
1500
|
+
return ("(empty)", False, note_source)
|
|
1501
|
+
encoded = text.encode("utf-8")
|
|
1502
|
+
if len(encoded) <= _PROMPT_DISPLAY_CAP_BYTES:
|
|
1503
|
+
return (text, False, note_source)
|
|
1504
|
+
body = encoded[:_PROMPT_DISPLAY_CAP_BYTES].decode("utf-8", errors="ignore")
|
|
1505
|
+
return (body, True, note_source)
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
def _render_call_detail_block(call: dict, idx: int, *,
|
|
1509
|
+
show_prompts: bool = False,
|
|
1510
|
+
show_response_text: bool = False) -> List[str]:
|
|
1511
|
+
"""Render one ``#### LLM call N — <short-id>`` block.
|
|
1512
|
+
|
|
1513
|
+
Used by both:
|
|
1514
|
+
- the gateway-direct branch (``_section_gateway_per_call_detail``)
|
|
1515
|
+
- the full-tree opt-in section (``_section_planner_llm_calls``)
|
|
1516
|
+
|
|
1517
|
+
``call`` shape (subset of fields used here):
|
|
1518
|
+
gateway_request_id, model, provider, prompt_template_dev_name,
|
|
1519
|
+
prompt_tokens, completion_tokens, total_tokens, prompt_text,
|
|
1520
|
+
response (-> finish_reason), response_text (only on full-tree path).
|
|
1521
|
+
|
|
1522
|
+
The prompt and response blocks are independently gated:
|
|
1523
|
+
- ``show_prompts`` controls the **Prompt** block.
|
|
1524
|
+
- ``show_response_text`` controls the **Response** block (full-tree
|
|
1525
|
+
only — gateway-direct chain entries don't carry response_text).
|
|
1526
|
+
Both default off so callers must opt in explicitly. The summary line
|
|
1527
|
+
(model/provider/template/tokens/finish_reason) always renders.
|
|
1528
|
+
"""
|
|
1529
|
+
gw_id = call.get("gateway_request_id") or "—"
|
|
1530
|
+
short_id = _short(gw_id)
|
|
1531
|
+
lines = [f"#### LLM call {idx} — {short_id}", ""]
|
|
1532
|
+
finish_reason = (call.get("response") or {}).get("finish_reason") or "—"
|
|
1533
|
+
summary = (
|
|
1534
|
+
f"- model={call.get('model') or '—'}"
|
|
1535
|
+
f" provider={call.get('provider') or '—'}"
|
|
1536
|
+
f" template={call.get('prompt_template_dev_name') or '—'}"
|
|
1537
|
+
f" prompt_tok={_fmt_token_count(call.get('prompt_tokens'))}"
|
|
1538
|
+
f" completion_tok={_fmt_token_count(call.get('completion_tokens'))}"
|
|
1539
|
+
f" total_tok={_fmt_token_count(call.get('total_tokens'))}"
|
|
1540
|
+
f" finish_reason={finish_reason}"
|
|
1541
|
+
)
|
|
1542
|
+
lines.append(summary)
|
|
1543
|
+
lines.append("")
|
|
1544
|
+
|
|
1545
|
+
# Prompt block — gated by show_prompts. Default-off everywhere; the
|
|
1546
|
+
# summary file should never leak full prompt text without an explicit
|
|
1547
|
+
# --show-prompts opt-in (matches the doc contract in SKILL.md).
|
|
1548
|
+
if show_prompts:
|
|
1549
|
+
body, truncated, src = _capped_payload(
|
|
1550
|
+
call.get("prompt_text"), "dc.gateway_requests.json")
|
|
1551
|
+
fence = _pick_fence(body)
|
|
1552
|
+
lines.append("**Prompt** (full input sent to the model):")
|
|
1553
|
+
lines.append(fence)
|
|
1554
|
+
lines.append(body)
|
|
1555
|
+
if truncated:
|
|
1556
|
+
lines.append(f"…[truncated; full prompt in {src}]")
|
|
1557
|
+
lines.append(fence)
|
|
1558
|
+
lines.append("")
|
|
1559
|
+
|
|
1560
|
+
# Response block — only the full-tree path carries response_text;
|
|
1561
|
+
# gateway-direct rows get finish_reason in the header line above and
|
|
1562
|
+
# nothing else (the response DMO doesn't carry text on that path).
|
|
1563
|
+
if show_response_text:
|
|
1564
|
+
body, truncated, src = _capped_payload(
|
|
1565
|
+
call.get("response_text"), "dc.generations.json")
|
|
1566
|
+
# html.unescape so the rendered block reads as plain JSON instead of
|
|
1567
|
+
# "-laden text — matches the existing _decoded_line treatment.
|
|
1568
|
+
if body and body != "(empty)":
|
|
1569
|
+
body = html.unescape(body)
|
|
1570
|
+
fence = _pick_fence(body)
|
|
1571
|
+
lines.append("**Response** (model output, including tool invocations):")
|
|
1572
|
+
lines.append(fence)
|
|
1573
|
+
lines.append(body)
|
|
1574
|
+
if truncated:
|
|
1575
|
+
lines.append(f"…[truncated; full response in {src}]")
|
|
1576
|
+
lines.append(fence)
|
|
1577
|
+
lines.append("")
|
|
1578
|
+
|
|
1579
|
+
return lines
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def _section_gateway_per_call_detail(sess: dict, *,
|
|
1583
|
+
show_prompts: bool = False) -> List[str]:
|
|
1584
|
+
"""Per-call detail for the gateway-direct branch.
|
|
1585
|
+
|
|
1586
|
+
``gateway_chain`` does NOT carry `response_text` (the responses DMO
|
|
1587
|
+
doesn't include it on that path), so the response block is always
|
|
1588
|
+
suppressed. The prompt block is gated by ``show_prompts`` so the
|
|
1589
|
+
default summary doesn't leak full prompt text — matches the
|
|
1590
|
+
documented contract for ``dc._session_summary.md``.
|
|
1591
|
+
"""
|
|
1592
|
+
chain = sess.get("gateway_chain") or []
|
|
1593
|
+
lines: List[str] = ["## Per-call detail", ""]
|
|
1594
|
+
for i, call in enumerate(chain, start=1):
|
|
1595
|
+
lines.extend(_render_call_detail_block(
|
|
1596
|
+
call, i, show_prompts=show_prompts, show_response_text=False))
|
|
1597
|
+
return lines
|
|
1598
|
+
|
|
1599
|
+
|
|
1600
|
+
def _collect_planner_llm_calls(sess: dict) -> List[dict]:
|
|
1601
|
+
"""Walk ``interactions[].steps[]`` and collect call-view dicts.
|
|
1602
|
+
|
|
1603
|
+
Each step that has a ``gateway_request`` (regardless of binding method)
|
|
1604
|
+
contributes one call view. ``response_text`` is sourced from the step's
|
|
1605
|
+
sibling ``generation.response_text`` when present; otherwise the call is
|
|
1606
|
+
still emitted with the prompt + token summary.
|
|
1607
|
+
|
|
1608
|
+
Returned dicts use the same field names as ``gateway_chain`` entries so
|
|
1609
|
+
``_render_call_detail_block`` can consume both shapes uniformly.
|
|
1610
|
+
"""
|
|
1611
|
+
calls: List[dict] = []
|
|
1612
|
+
for iv in sess.get("interactions") or []:
|
|
1613
|
+
for st in iv.get("steps") or []:
|
|
1614
|
+
gw = st.get("gateway_request")
|
|
1615
|
+
if not gw:
|
|
1616
|
+
continue
|
|
1617
|
+
gen = st.get("generation") or {}
|
|
1618
|
+
calls.append({
|
|
1619
|
+
"gateway_request_id": gw.get("gateway_request_id"),
|
|
1620
|
+
"model": gw.get("model"),
|
|
1621
|
+
"provider": gw.get("provider"),
|
|
1622
|
+
"prompt_template_dev_name": gw.get("prompt_template_dev_name"),
|
|
1623
|
+
"prompt_tokens": gw.get("prompt_tokens"),
|
|
1624
|
+
"completion_tokens": gw.get("completion_tokens"),
|
|
1625
|
+
"total_tokens": gw.get("total_tokens"),
|
|
1626
|
+
"prompt_text": gw.get("prompt_text"),
|
|
1627
|
+
"response": gw.get("response"),
|
|
1628
|
+
"response_text": gen.get("response_text"),
|
|
1629
|
+
})
|
|
1630
|
+
return calls
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
def _section_planner_llm_calls(sess: dict, *, show_prompts: bool) -> List[str]:
|
|
1634
|
+
"""Opt-in full-tree section showing the input prompt + response
|
|
1635
|
+
for every LLM call in the session's hierarchical trace.
|
|
1636
|
+
|
|
1637
|
+
Off by default — prompts can be 30 KB+ each on multi-turn sessions
|
|
1638
|
+
and would otherwise dominate the summary. Enable with
|
|
1639
|
+
``render_dc.py --show-prompts``.
|
|
1640
|
+
"""
|
|
1641
|
+
if not show_prompts:
|
|
1642
|
+
return []
|
|
1643
|
+
calls = _collect_planner_llm_calls(sess)
|
|
1644
|
+
if not calls:
|
|
1645
|
+
return []
|
|
1646
|
+
lines: List[str] = [
|
|
1647
|
+
"## Planner LLM calls (full prompts + responses)",
|
|
1648
|
+
"",
|
|
1649
|
+
f"_Found {len(calls)} LLM call(s) across the session's hierarchical "
|
|
1650
|
+
f"trace. Prompts are capped at "
|
|
1651
|
+
f"{_PROMPT_DISPLAY_CAP_BYTES // 1024} KB for display; full payloads "
|
|
1652
|
+
f"are on disk in `dc.gateway_requests.json` and "
|
|
1653
|
+
f"`dc.generations.json`._",
|
|
1654
|
+
"",
|
|
1655
|
+
]
|
|
1656
|
+
for i, call in enumerate(calls, start=1):
|
|
1657
|
+
# show_prompts is True here by section-guard above (early return when
|
|
1658
|
+
# show_prompts is False); pass through explicitly so the helper's new
|
|
1659
|
+
# prompt-gate stays aligned with the section's intent.
|
|
1660
|
+
lines.extend(_render_call_detail_block(
|
|
1661
|
+
call, i, show_prompts=True, show_response_text=True))
|
|
1662
|
+
return lines
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
def _assert_schema_version(tree: dict) -> None:
|
|
1666
|
+
"""Refuse unsupported versions; warn on missing version (older assembler)."""
|
|
1667
|
+
version = (tree.get("session") or {}).get("_schema_version")
|
|
1668
|
+
if version is None:
|
|
1669
|
+
print(
|
|
1670
|
+
"render_dc: WARN tree has no _schema_version "
|
|
1671
|
+
"(produced by an older assembler?); rendering anyway",
|
|
1672
|
+
file=sys.stderr,
|
|
1673
|
+
)
|
|
1674
|
+
return
|
|
1675
|
+
if version != _SUPPORTED_SCHEMA_VERSION:
|
|
1676
|
+
raise SystemExit(
|
|
1677
|
+
f"render_dc: unsupported tree _schema_version={version}; "
|
|
1678
|
+
f"expected {_SUPPORTED_SCHEMA_VERSION}"
|
|
1679
|
+
)
|
|
1680
|
+
|
|
1681
|
+
|
|
1682
|
+
def main_for_session(sid: str, *, show_prompts: bool = False) -> int:
|
|
1683
|
+
"""Read session tree + manifest from the nested session dir; emit summary.md.
|
|
1684
|
+
|
|
1685
|
+
Uses ``assemble_dc._find_session_dir`` to locate the session under
|
|
1686
|
+
``DATA_ROOT/<org>/<agent>__<ver>/<sid>/`` — follows the ``_sessions/*.link``
|
|
1687
|
+
breadcrumb when present, globs otherwise. No callers need to know the
|
|
1688
|
+
full identity triple upfront.
|
|
1689
|
+
|
|
1690
|
+
``show_prompts``: pass through to ``render`` to include the
|
|
1691
|
+
opt-in "Planner LLM calls" section.
|
|
1692
|
+
"""
|
|
1693
|
+
from assemble_dc import _find_session_dir
|
|
1694
|
+
session_dir = _find_session_dir(sid)
|
|
1695
|
+
tree_path = session_dir / "dc._session_tree.json"
|
|
1696
|
+
if not tree_path.is_file():
|
|
1697
|
+
raise SystemExit(
|
|
1698
|
+
f"render_dc: tree not found at {tree_path}; "
|
|
1699
|
+
f"run `python3 scripts/assemble_dc.py --session {sid}` first"
|
|
1700
|
+
)
|
|
1701
|
+
manifest_path = session_dir / "dc._session_manifest.json"
|
|
1702
|
+
manifest: Optional[dict] = None
|
|
1703
|
+
if manifest_path.is_file():
|
|
1704
|
+
try:
|
|
1705
|
+
manifest = json.loads(manifest_path.read_text())
|
|
1706
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
1707
|
+
print(
|
|
1708
|
+
f"render_dc: WARN could not read manifest: {str(e).splitlines()[0]}",
|
|
1709
|
+
file=sys.stderr,
|
|
1710
|
+
)
|
|
1711
|
+
|
|
1712
|
+
tree = json.loads(tree_path.read_text())
|
|
1713
|
+
_assert_schema_version(tree)
|
|
1714
|
+
|
|
1715
|
+
md_path = session_dir / "dc._session_summary.md"
|
|
1716
|
+
md_path.write_text(render(tree, manifest, session_dir=session_dir,
|
|
1717
|
+
show_prompts=show_prompts))
|
|
1718
|
+
print(f"render_dc: wrote {md_path}", file=sys.stderr)
|
|
1719
|
+
return 0
|
|
1720
|
+
|
|
1721
|
+
|
|
1722
|
+
def main() -> int:
|
|
1723
|
+
ap = argparse.ArgumentParser(
|
|
1724
|
+
prog="render_dc.py",
|
|
1725
|
+
description="Render dc._session_summary.md from dc._session_tree.json for one session.",
|
|
1726
|
+
)
|
|
1727
|
+
ap.add_argument("--session", required=True,
|
|
1728
|
+
help="AI-agent session UUID or MessagingSession id (0Mw...). "
|
|
1729
|
+
"Messaging ids are resolved from disk "
|
|
1730
|
+
"(DATA_ROOT/*/dc.sessions.json); run fetch_dc.py first "
|
|
1731
|
+
"if the session hasn't been fetched yet.")
|
|
1732
|
+
ap.add_argument("--show-prompts", action="store_true",
|
|
1733
|
+
help="Include the opt-in 'Planner LLM calls' section "
|
|
1734
|
+
"with the full input prompt + response per LLM call. "
|
|
1735
|
+
"Off by default — multi-turn sessions can produce "
|
|
1736
|
+
"hundreds of KB here. Per-prompt display is capped "
|
|
1737
|
+
"at 64 KB; full payloads remain on disk in "
|
|
1738
|
+
"dc.gateway_requests.json + dc.generations.json.")
|
|
1739
|
+
# Runtime-agnostic path overrides; default to ~/.vibe/...
|
|
1740
|
+
from _shared.cli_override import add_cli_flags, apply_overrides
|
|
1741
|
+
add_cli_flags(ap)
|
|
1742
|
+
args = ap.parse_args()
|
|
1743
|
+
apply_overrides(args, caller_globals=globals())
|
|
1744
|
+
from resolve_session import resolve_disk_or_live
|
|
1745
|
+
sid = resolve_disk_or_live(args.session)
|
|
1746
|
+
return main_for_session(sid, show_prompts=args.show_prompts)
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
if __name__ == "__main__":
|
|
1750
|
+
sys.exit(main())
|