@salesforce/afv-skills 1.66.0 → 1.68.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.
Files changed (165) hide show
  1. package/README.md +55 -5
  2. package/package.json +2 -4
  3. package/skills/agentforce-offline-experimentation-run/SKILL.md +26 -49
  4. package/skills/agentforce-offline-experimentation-run/scripts/bt_compare.py +81 -0
  5. package/skills/agentforce-offline-experimentation-run/scripts/bt_fetch.py +358 -0
  6. package/skills/agentforce-offline-experimentation-run/scripts/bt_patch_agent.py +110 -0
  7. package/skills/agentforce-offline-experimentation-run/scripts/btlib/__init__.py +34 -0
  8. package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentpatch.py +483 -0
  9. package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentsrc.py +565 -0
  10. package/skills/agentforce-offline-experimentation-run/scripts/btlib/cache.py +95 -0
  11. package/skills/agentforce-offline-experimentation-run/scripts/btlib/compare.py +617 -0
  12. package/skills/agentforce-offline-experimentation-run/scripts/btlib/fetch.py +1012 -0
  13. package/skills/agentforce-offline-experimentation-run/scripts/btlib/judge.py +148 -0
  14. package/skills/agentforce-offline-experimentation-run/scripts/btlib/messages.py +92 -0
  15. package/skills/agentforce-offline-experimentation-run/scripts/btlib/patch.py +327 -0
  16. package/skills/agentforce-offline-experimentation-run/scripts/btlib/redact.py +40 -0
  17. package/skills/agentforce-offline-experimentation-run/scripts/btlib/results.py +288 -0
  18. package/skills/agentforce-offline-experimentation-run/scripts/btlib/scoring.py +340 -0
  19. package/skills/agentforce-offline-experimentation-run/scripts/btlib/sfap.py +238 -0
  20. package/skills/agentforce-offline-experimentation-run/scripts/btlib/splits.py +136 -0
  21. package/skills/agentforce-offline-experimentation-run/scripts/btlib/stdm.py +98 -0
  22. package/skills/agentforce-offline-experimentation-run/scripts/btlib/tokens.py +173 -0
  23. package/skills/agentforce-offline-experimentation-run/scripts/btlib/units.py +199 -0
  24. package/skills/agentforce-offline-experimentation-run/scripts/btlib/versions.py +194 -0
  25. package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +366 -378
  26. package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +14 -4
  27. package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +472 -796
  28. package/skills/commerce-react-storefront-create/SKILL.md +350 -0
  29. package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +2 -2
  30. package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +28 -16
  31. package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +18 -1
  32. package/skills/dx-lifecycle-center/SKILL.md +5 -2
  33. package/skills/dx-lifecycle-center/assets/widgets/connect-connected/connect-connected.json +65 -0
  34. package/skills/dx-lifecycle-center/assets/widgets/connect-connected/schema.json +32 -0
  35. package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/connect-sign-in.json +57 -0
  36. package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/schema.json +21 -0
  37. package/skills/dx-lifecycle-center/examples/workflows.md +20 -6
  38. package/skills/dx-lifecycle-center/references/connect-org.md +21 -2
  39. package/skills/dx-lifecycle-center/references/deploy.md +2 -2
  40. package/skills/dx-lifecycle-center/references/list-orgs.md +1 -1
  41. package/skills/dx-lifecycle-center/scripts/card-policy.jq +71 -0
  42. package/skills/dx-lifecycle-center/scripts/fill-widget.jq +29 -0
  43. package/skills/dx-lifecycle-center/scripts/widget.sh +28 -0
  44. package/skills/dx-org-devhub-configure/SKILL.md +85 -84
  45. package/skills/dx-org-devhub-configure/examples/status-output.md +25 -2
  46. package/skills/dx-org-devhub-configure/scripts/devhub.sh +265 -140
  47. package/skills/dx-org-devhub-configure/scripts/test-devhub.sh +100 -15
  48. package/skills/dx-packaging-move-to-2gp-migrate/SKILL.md +89 -0
  49. package/skills/dx-packaging-move-to-2gp-migrate/references/api-operations.md +164 -0
  50. package/skills/dx-scratch-def-configure/SKILL.md +163 -0
  51. package/skills/dx-scratch-def-configure/examples/confirm-card.md +53 -0
  52. package/skills/dx-scratch-def-configure/examples/report-recipes.md +100 -0
  53. package/skills/dx-scratch-def-configure/references/features.json +3270 -0
  54. package/skills/dx-scratch-def-configure/references/project-scratch-def.schema.json +1841 -0
  55. package/skills/dx-scratch-def-configure/references/settings-map.json +682 -0
  56. package/skills/dx-scratch-def-configure/scripts/lib_scratch_def.py +1306 -0
  57. package/skills/dx-scratch-def-configure/scripts/requirements.txt +3 -0
  58. package/skills/dx-scratch-def-configure/scripts/scrape_config_values.py +848 -0
  59. package/skills/education-cloud-domain-configure/SKILL.md +22 -24
  60. package/skills/education-cloud-domain-configure/references/dependent-features.md +9 -0
  61. package/skills/education-cloud-domain-configure/references/domain-features.md +15 -0
  62. package/skills/education-cloud-domain-configure/references/gotchas-extended.md +8 -0
  63. package/skills/education-cloud-domain-configure/references/setup-widgets.md +123 -0
  64. package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -1
  65. package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -1
  66. package/skills/experience-design-validate/SKILL.md +1 -1
  67. package/skills/experience-search-coordinate/SKILL.md +19 -13
  68. package/skills/experience-search-coordinate/references/content-route.md +79 -1
  69. package/skills/experience-search-coordinate/references/media-route.md +30 -4
  70. package/skills/experience-ui-bundle-features-generate/SKILL.md +1 -1
  71. package/skills/experience-ui-bundle-mfa-configure/SKILL.md +1 -1
  72. package/skills/field-service-data-capture-migrate/SKILL.md +1 -1
  73. package/skills/platform-apex-generate/SKILL.md +14 -11
  74. package/skills/platform-apex-test-generate/SKILL.md +2 -1
  75. package/skills/platform-datamask-run/SKILL.md +52 -71
  76. package/skills/platform-datamask-run/references/api-surface.md +10 -4
  77. package/skills/platform-datamask-run/references/policy-authoring.md +56 -7
  78. package/skills/platform-datamask-run/references/run-and-abort.md +17 -10
  79. package/skills/platform-datamask-run/scripts/poll-job.sh +15 -25
  80. package/skills/platform-dataspace-access-configure/SKILL.md +19 -1
  81. package/skills/platform-metadata-deploy/README.md +1 -1
  82. package/skills/platform-metadata-deploy/SKILL.md +114 -15
  83. package/skills/platform-metadata-deploy/references/agent-deployment-guide.md +3 -4
  84. package/skills/platform-metadata-deploy/references/orchestration.md +2 -3
  85. package/skills/platform-metadata-discovery-get/SKILL.md +185 -0
  86. package/skills/platform-metadata-discovery-get/references/api-map.md +25 -0
  87. package/skills/platform-metadata-discovery-get/references/h360-routes.md +30 -0
  88. package/skills/platform-metadata-discovery-get/references/scoped-soql.md +52 -0
  89. package/skills/platform-metadata-grounding-get/SKILL.md +170 -0
  90. package/skills/platform-policy-rule-generate/SKILL.md +84 -232
  91. package/skills/platform-policy-rule-generate/references/condition-model.md +47 -0
  92. package/skills/platform-policy-rule-generate/references/condition-templates.md +89 -0
  93. package/skills/platform-policy-rule-generate/references/conjunction-compiler.md +25 -0
  94. package/skills/platform-policy-rule-generate/references/conjunction-expression.md +22 -0
  95. package/skills/platform-policy-rule-generate/references/consumer-all.md +9 -0
  96. package/skills/platform-policy-rule-generate/references/consumer-datacloud-ui.md +91 -0
  97. package/skills/platform-policy-rule-generate/references/deploy-errors.md +24 -36
  98. package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/package.xml +12 -0
  99. package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitionSets/ExistsJoinRLS.policyRuleDefinitionSet +6 -0
  100. package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitions/ExistsJoinRLS_Rule_1.policyRuleDefinition +91 -0
  101. package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/package.xml +12 -0
  102. package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitionSets/FieldReadDeny.policyRuleDefinitionSet +6 -0
  103. package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitions/FieldReadDeny_Rule_1.policyRuleDefinition +72 -0
  104. package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/package.xml +12 -0
  105. package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitionSets/LastCharactersFieldMasking.policyRuleDefinitionSet +6 -0
  106. package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitions/LastCharactersFieldMasking_Rule_1.policyRuleDefinition +77 -0
  107. package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/package.xml +12 -0
  108. package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitionSets/PrincipalFieldMatchRLS.policyRuleDefinitionSet +6 -0
  109. package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitions/PrincipalFieldMatchRLS_Rule_1.policyRuleDefinition +77 -0
  110. package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/package.xml +12 -0
  111. package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitionSets/TaggedObjectAccess.policyRuleDefinitionSet +4 -0
  112. package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitions/TaggedObjectAccess_Rule_1.policyRuleDefinition +65 -0
  113. package/skills/platform-policy-rule-generate/references/headless-transport.md +87 -0
  114. package/skills/platform-policy-rule-generate/references/policy-schema-full.md +17 -10
  115. package/skills/platform-policy-rule-generate/references/policy-shapes.md +74 -0
  116. package/skills/platform-policy-rule-generate/references/principal-expression.md +31 -0
  117. package/skills/platform-policy-rule-generate/references/reference-resolution.md +51 -0
  118. package/skills/platform-policy-rule-generate/references/resource-expression.md +48 -0
  119. package/skills/platform-policy-rule-generate/references/runtime-context.md +9 -0
  120. package/skills/platform-policy-rule-generate/references/templates-access.md +11 -9
  121. package/skills/platform-policy-rule-generate/references/templates-advanced.md +4 -2
  122. package/skills/platform-policy-rule-generate/references/templates-record.md +37 -22
  123. package/skills/platform-policy-rule-generate/references/templates-transform.md +10 -8
  124. package/skills/platform-policy-rule-generate/references/templates.md +2 -2
  125. package/skills/platform-policy-rule-generate/references/ui-compatibility.md +35 -17
  126. package/skills/platform-report-generate/SKILL.md +9 -4
  127. package/skills/platform-report-generate/references/chart-types.md +26 -2
  128. package/skills/platform-report-generate/references/column-names.md +43 -0
  129. package/skills/platform-report-generate/references/errors-and-troubleshooting.md +1 -1
  130. package/skills/platform-report-generate/references/verification-checklist.md +10 -2
  131. package/skills/platform-value-set-generate/SKILL.md +19 -19
  132. package/skills/platform-widget-generate/SKILL.md +25 -51
  133. package/skills/platform-widget-generate/examples/formulas.json +19 -2
  134. package/skills/platform-widget-generate/examples/localized-labels.json +57 -0
  135. package/skills/platform-widget-generate/references/widget-actions.md +29 -0
  136. package/skills/platform-widget-generate/references/widget-bundle-validation.md +1 -0
  137. package/skills/platform-widget-generate/references/widget-formulas.md +24 -7
  138. package/skills/platform-widget-generate/references/widget-gotchas.md +10 -0
  139. package/skills/platform-widget-generate/references/widget-labels.md +78 -0
  140. package/skills/platform-widget-generate/references/widget-styling.md +12 -0
  141. package/skills/sales-call-scoring-configure/SKILL.md +17 -10
  142. package/skills/sales-call-scoring-configure/scripts/enable-call-scoring.sh +55 -33
  143. package/skills/service-digital-engagement-channel-configure/SKILL.md +11 -7
  144. package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +33 -0
  145. package/skills/service-digital-engagement-channel-configure/references/channel_settings.md +123 -16
  146. package/skills/service-digital-engagement-deployment-configure/SKILL.md +37 -100
  147. package/skills/service-digital-engagement-deployment-configure/assets/esd_web_update_template.xml +83 -8
  148. package/skills/service-digital-engagement-deployment-configure/examples/esd_web_prechat_fields.xml +74 -0
  149. package/skills/service-digital-engagement-deployment-configure/references/connect_api_creation.md +13 -3
  150. package/skills/service-digital-engagement-deployment-configure/references/deployment_settings.md +13 -78
  151. package/skills/service-digital-engagement-deployment-configure/references/onboarding_contract.md +5 -6
  152. package/skills/service-digital-engagement-deployment-configure/references/prechat_forms.md +245 -0
  153. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +164 -120
  154. package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +61 -37
  155. package/skills/service-itsm-agentic-setup-incident-management/references/audit-tracking.md +42 -0
  156. package/skills/service-itsm-agentic-setup-incident-management/references/broadcast-communications.md +23 -0
  157. package/skills/service-itsm-agentic-setup-incident-management/references/email-to-incident.md +23 -0
  158. package/skills/service-itsm-agentic-setup-incident-management/references/incident-dashboards.md +25 -0
  159. package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +30 -146
  160. package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +32 -136
  161. package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +32 -155
  162. package/skills/service-itsm-agentic-setup-incident-management/references/manage-incidents-with-ai.md +45 -0
  163. package/skills/service-itsm-agentic-setup-incident-management/references/priority-matrix.md +32 -0
  164. package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +30 -140
  165. package/skills/service-itsm-agentic-setup-incident-management/references/sla-milestones.md +110 -0
@@ -0,0 +1,565 @@
1
+ """Read an agentscript `.agent` file: the texts a step's request is rendered from.
2
+
3
+ The patch hook (patch.py) and version attribution (versions.py) need, per node, the
4
+ instruction text blocks, the bindings that become the step's tools, and the action
5
+ definitions whose descriptions those tools carry. This is a line parser for exactly that,
6
+ not a compiler: procedure logic (set, run, conditions) is skipped, except that a text block
7
+ inside an `if` / `else` is marked conditional.
8
+
9
+ How the runtime renders these texts (verified on bt-orgfarm Shopper traces, 2026-10-09, and
10
+ bt-airline Voice_English_Airline traces, 2026-10-14):
11
+ - a `| text` block is dedented by the column its text starts at (`|text`: right after the
12
+ bar); a bare `|` block (text on the lines below) and a `key: |` block by the indent of
13
+ their first text line
14
+ - {!@actions.X} renders as X; other merge fields render as their values
15
+ - a line whose text starts with a merge field loses all its leading whitespace
16
+ - system.instructions ends the first system message, or, on a voice agent, sits in it as
17
+ whole lines between the platform's own texts; a node's instruction text is the last
18
+ system message, before "\\n\\n# CONTEXT"
19
+ - an action tool is named after its reasoning binding and carries the action definition's
20
+ description; the binding's LLM-filled inputs (`with p = ...`) are its parameters, with
21
+ the input descriptions; inputs bound to an expression are not offered
22
+ - a transition tool carries its binding's description plus TRANSITION_SUFFIX. Bindings are
23
+ per node, so one transition (or action) name can have a different description per node
24
+ Not verified (no matching traces): how several text blocks of one node are joined, and what
25
+ an `if` branch renders — patch ops stay within one block.
26
+
27
+ python3 scripts/btlib/agentsrc.py AGENT_OR_BUNDLE_DIR [--node NAME] [--rendered]
28
+ """
29
+
30
+ import json
31
+ import os
32
+ import re
33
+ import sys
34
+ from collections import Counter
35
+
36
+ TRANSITION_SUFFIX = ". Never use __user_input__ to collect inputs for this action."
37
+ CONTEXT_MARKER = "\n\n# CONTEXT" # ends a node's instruction text in the last system message
38
+ MERGE_FIELD = re.compile(r"\{!([^{}]*)\}")
39
+ NODE_KINDS = ("start_agent", "subagent", "topic")
40
+ _ACTION_REF = re.compile(r"^@actions\.(\w+)$")
41
+ _TRANSITION = re.compile(r"^@utils\.transition\s+to\s+@(?:subagent|topic)\.(\w+)$")
42
+ _STATEMENT = re.compile(r"^(if|elif|else|with|set|run|transition|available)\b")
43
+ _CONDITIONAL = re.compile(r"^(if|elif|else)\b")
44
+ _WITH = re.compile(r"^with\s+[\"']?(\w+)[\"']?\s*=\s*(.*)$")
45
+ _KEY = re.compile(r"^([^:\"]+?)\s*:(?:\s+(.*))?$")
46
+ _QUOTED_KEY = re.compile(r'^"((?:[^"\\]|\\.)*)"\s*:\s*(.*)$')
47
+
48
+
49
+ class Block:
50
+ """One text block: its source lines and its dedented text (merge fields as written)."""
51
+
52
+ def __init__(self, lines, base, style, first_line=None):
53
+ self.lines = lines # [(1-based line number, raw line)]
54
+ self.base = base # dedent column
55
+ self.style = style # "inline" (| text), "bare" (|), "block" (key: |)
56
+ self.conditional = False # inside an if / elif / else
57
+ self.start = lines[0][0] if lines else first_line
58
+ self.end = lines[-1][0] if lines else first_line
59
+
60
+ @property
61
+ def text(self):
62
+ out = []
63
+ for i, (_, raw) in enumerate(self.lines):
64
+ if i == 0 and self.style == "inline":
65
+ out.append(raw[self.base:])
66
+ elif not raw.strip():
67
+ out.append("")
68
+ elif _indent(raw) >= self.base:
69
+ out.append(raw[self.base:])
70
+ else:
71
+ out.append(raw.lstrip(" "))
72
+ return "\n".join(out)
73
+
74
+ def rendered(self):
75
+ """The text as the runtime renders it; non-action merge fields stay in place."""
76
+ return render_text(self.text)
77
+
78
+
79
+ class Input:
80
+ def __init__(self, name, line, type_):
81
+ self.name, self.line, self.type = name, line, type_
82
+ self.description = None
83
+ self.label = None
84
+ self.is_required = False
85
+ self.is_user_input = None
86
+ self.item = None # the parsed entry (agentpatch.py edits its description)
87
+
88
+
89
+ class ActionDef:
90
+ def __init__(self, name, line):
91
+ self.name, self.line = name, line
92
+ self.description = None
93
+ self.label = None
94
+ self.target = None
95
+ self.inputs = {}
96
+ self.item = None
97
+
98
+
99
+ class Binding:
100
+ """A reasoning action: becomes one tool of the node's steps."""
101
+
102
+ def __init__(self, name, line, target):
103
+ self.name, self.line, self.target = name, line, target
104
+ self.kind, self.action, self.to = "other", None, None
105
+ m = _ACTION_REF.match(target or "")
106
+ if m:
107
+ self.kind, self.action = "action", m.group(1)
108
+ m = _TRANSITION.match(target or "")
109
+ if m:
110
+ self.kind, self.to = "transition", m.group(1)
111
+ self.description = None
112
+ self.description_line = None
113
+ self.llm_inputs = [] # with p = ...
114
+ self.bound_inputs = {} # with p = <expression>
115
+ self.available_when = None
116
+ self.item = None
117
+
118
+
119
+ class Node:
120
+ def __init__(self, kind, name, line):
121
+ self.kind, self.name, self.line = kind, name, line
122
+ self.label = None
123
+ self.description = None
124
+ self.model = None
125
+ self.system = None # node-level system.instructions override
126
+ self.blocks = [] # reasoning instruction text blocks, in order
127
+ self.procedure = False # instructions given as a procedure (-> or statements)
128
+ self.bindings = {}
129
+ self.action_defs = {}
130
+
131
+
132
+ class AgentSource:
133
+ def __init__(self, path=None):
134
+ self.path = path
135
+ self.target = None # bundle-meta <target>, e.g. Shopper_Agent.v3
136
+ self.label = None # version label, e.g. v3
137
+ self.developer_name = None
138
+ self.model = None
139
+ self.system = None # global system.instructions block
140
+ self.nodes = {}
141
+
142
+ def action_def(self, node, name):
143
+ """The definition a binding refers to: the node's own first, then any node's."""
144
+ if name in node.action_defs:
145
+ return node.action_defs[name]
146
+ for other in self.nodes.values():
147
+ if name in other.action_defs:
148
+ return other.action_defs[name]
149
+ return None
150
+
151
+ def node_tools(self, node_name):
152
+ """{tool name: {kind, description, params {name: description}, required, conditional}}
153
+ for the tools a node's steps are offered (platform tools such as __end_session_action__
154
+ are not in the .agent)."""
155
+ node = self.nodes.get(node_name)
156
+ out = {}
157
+ if node is None:
158
+ return out
159
+ for b in node.bindings.values():
160
+ tool = {"kind": b.kind, "description": None, "params": {}, "required": [],
161
+ "conditional": b.available_when is not None, "line": b.line}
162
+ if b.kind == "transition":
163
+ if b.description is not None:
164
+ tool["description"] = b.description + TRANSITION_SUFFIX
165
+ elif b.kind == "action":
166
+ d = self.action_def(node, b.action)
167
+ if d is not None:
168
+ tool["description"] = d.description
169
+ for p in b.llm_inputs:
170
+ inp = d.inputs.get(p)
171
+ tool["params"][p] = inp.description if inp else None
172
+ if inp and inp.is_required:
173
+ tool["required"].append(p)
174
+ else:
175
+ tool["params"] = {p: None for p in b.llm_inputs}
176
+ else:
177
+ continue
178
+ out[b.name] = tool
179
+ return out
180
+
181
+
182
+ class AgentSourceError(ValueError):
183
+ pass
184
+
185
+
186
+ # ---------------------------------------------------------------------------------------
187
+ # rendering
188
+
189
+ def _action_name(field):
190
+ m = _ACTION_REF.match(field.strip())
191
+ return m.group(1) if m else None
192
+
193
+
194
+ def render_text(text):
195
+ """Text as rendered: {!@actions.X} -> X, and a line starting with a merge field loses its
196
+ leading whitespace. Other merge fields stay as written (their values are not known)."""
197
+ lines = [ln.lstrip() if ln.lstrip().startswith("{!") else ln for ln in text.split("\n")]
198
+ return MERGE_FIELD.sub(lambda m: _action_name(m.group(1)) or m.group(0), "\n".join(lines))
199
+
200
+
201
+ def value_fields(text):
202
+ """The non-action merge fields of a text, in order of first appearance."""
203
+ out = []
204
+ for m in MERGE_FIELD.finditer(text):
205
+ if _action_name(m.group(1)) is None and m.group(0) not in out:
206
+ out.append(m.group(0))
207
+ return out
208
+
209
+
210
+ def fragment_pattern(text, prefix="mf"):
211
+ """(regex source, {field: group name}) finding the rendered form of `text`.
212
+
213
+ Each non-action merge field becomes a capture group that matches whatever value the
214
+ field rendered to (a repeat of the field is a backreference). The group is lazy and may
215
+ not contain the literal just before it, so the match takes the shortest value even when
216
+ that literal occurs earlier in the text too. Group names are prefix + a number, so the
217
+ patterns of several texts can be combined.
218
+ """
219
+ rendered = render_text(text)
220
+ parts, groups, pos = [], {}, 0
221
+ for m in MERGE_FIELD.finditer(rendered):
222
+ literal = rendered[pos:m.start()]
223
+ parts.append(re.escape(literal))
224
+ field = m.group(0)
225
+ if field in groups:
226
+ parts.append(f"(?P={groups[field]})")
227
+ else:
228
+ groups[field] = f"{prefix}{len(groups)}"
229
+ char = f"(?:(?!{re.escape(literal)})[\\s\\S])" if literal else "[\\s\\S]"
230
+ parts.append(f"(?P<{groups[field]}>{char}*?)")
231
+ pos = m.end()
232
+ parts.append(re.escape(rendered[pos:]))
233
+ return "".join(parts), groups
234
+
235
+
236
+ # ---------------------------------------------------------------------------------------
237
+ # parsing
238
+
239
+ class _Item:
240
+ __slots__ = ("indent", "line", "key", "value", "block", "children")
241
+
242
+ def __init__(self, indent, line, key, value, block=None):
243
+ self.indent, self.line, self.key, self.value, self.block = indent, line, key, value, block
244
+ self.children = []
245
+
246
+
247
+ def _indent(raw):
248
+ return len(raw) - len(raw.lstrip(" "))
249
+
250
+
251
+ def _split_key(s):
252
+ """('key', 'value') for `key: value` / `"key": value`; (None, s) for a statement."""
253
+ if _STATEMENT.match(s):
254
+ return None, s
255
+ m = _QUOTED_KEY.match(s) if s.startswith('"') else _KEY.match(s)
256
+ if m:
257
+ return m.group(1), (m.group(2) or "").strip()
258
+ return None, s
259
+
260
+
261
+ def _take_block(lines, j, min_indent, head=None):
262
+ """Lines from index j that are blank or indented deeper than min_indent; trailing blanks
263
+ dropped. head: the inline first line (line number, raw), if any."""
264
+ body = [head] if head else []
265
+ while j < len(lines):
266
+ raw = lines[j]
267
+ if raw.strip() and _indent(raw) <= min_indent:
268
+ break
269
+ body.append((j + 1, raw))
270
+ j += 1
271
+ while len(body) > (1 if head else 0) and not body[-1][1].strip():
272
+ body.pop()
273
+ return body, j
274
+
275
+
276
+ def _first_text_indent(body):
277
+ for _, raw in body:
278
+ if raw.strip():
279
+ return _indent(raw)
280
+ return 0
281
+
282
+
283
+ def _scan(lines):
284
+ """Flat items in file order; text blocks are consumed whole (their lines can hold anything)."""
285
+ items, i = [], 0
286
+ while i < len(lines):
287
+ raw = lines[i]
288
+ s = raw.strip()
289
+ if not s or s.startswith("#"):
290
+ i += 1
291
+ continue
292
+ ind = _indent(raw)
293
+ if s.startswith("|"): # |, | text, |text
294
+ if s == "|":
295
+ body, nxt = _take_block(lines, i + 1, ind)
296
+ block = Block(body, _first_text_indent(body), "bare", first_line=i + 1)
297
+ else:
298
+ text_col = ind + 1 + _indent(raw[ind + 1:])
299
+ body, nxt = _take_block(lines, i + 1, ind, head=(i + 1, raw))
300
+ block = Block(body, text_col, "inline")
301
+ items.append(_Item(ind, i + 1, None, "|", block))
302
+ i = nxt
303
+ continue
304
+ key, value = _split_key(s)
305
+ if key is not None and value.startswith("|"):
306
+ if value == "|":
307
+ body, nxt = _take_block(lines, i + 1, ind)
308
+ block = Block(body, _first_text_indent(body), "block", first_line=i + 1)
309
+ else:
310
+ bar = raw.index("|", raw.index(":"))
311
+ text_col = bar + 1 + _indent(raw[bar + 1:])
312
+ body, nxt = _take_block(lines, i + 1, ind, head=(i + 1, raw))
313
+ block = Block(body, text_col, "inline")
314
+ items.append(_Item(ind, i + 1, key, "|", block))
315
+ i = nxt
316
+ continue
317
+ items.append(_Item(ind, i + 1, key, value))
318
+ i += 1
319
+ return items
320
+
321
+
322
+ def _tree(items):
323
+ root = _Item(-1, 0, None, None)
324
+ stack = [root]
325
+ for it in items:
326
+ while stack[-1].indent >= it.indent:
327
+ stack.pop()
328
+ stack[-1].children.append(it)
329
+ stack.append(it)
330
+ return root
331
+
332
+
333
+ def _str(value):
334
+ if value is None:
335
+ return None
336
+ v = value.strip()
337
+ if v.startswith('"'):
338
+ try:
339
+ return json.loads(v)
340
+ except json.JSONDecodeError:
341
+ return v.strip('"')
342
+ return v
343
+
344
+
345
+ def _bool(value):
346
+ return (value or "").strip().lower() == "true"
347
+
348
+
349
+ def _text_value(item):
350
+ return item.block.text if item.block is not None else _str(item.value)
351
+
352
+
353
+ def _child(item, key):
354
+ for ch in item.children:
355
+ if ch.key == key:
356
+ return ch
357
+ return None
358
+
359
+
360
+ def _instruction_blocks(item):
361
+ """(blocks, procedure) of a reasoning `instructions` entry."""
362
+ if item.block is not None:
363
+ return [item.block], False
364
+ blocks = []
365
+
366
+ def walk(it, conditional):
367
+ for ch in it.children:
368
+ if ch.block is not None and ch.key is None:
369
+ ch.block.conditional = conditional
370
+ blocks.append(ch.block)
371
+ continue
372
+ walk(ch, conditional or (ch.key is None and bool(_CONDITIONAL.match(ch.value or ""))))
373
+ walk(item, False)
374
+ return blocks, True
375
+
376
+
377
+ def _binding(item):
378
+ b = Binding(item.key, item.line, item.value)
379
+ b.item = item
380
+ for ch in item.children:
381
+ if ch.key == "description":
382
+ b.description = _text_value(ch)
383
+ b.description_line = ch.line
384
+ elif ch.key is None and ch.value:
385
+ m = _WITH.match(ch.value)
386
+ if m:
387
+ if m.group(2).strip() == "...":
388
+ b.llm_inputs.append(m.group(1))
389
+ else:
390
+ b.bound_inputs[m.group(1)] = m.group(2).strip()
391
+ elif ch.value.startswith("available when"):
392
+ b.available_when = ch.value[len("available when"):].strip()
393
+ return b
394
+
395
+
396
+ def _action_def(item):
397
+ d = ActionDef(item.key, item.line)
398
+ d.item = item
399
+ for ch in item.children:
400
+ if ch.key == "description":
401
+ d.description = _text_value(ch)
402
+ elif ch.key == "label":
403
+ d.label = _str(ch.value)
404
+ elif ch.key == "target":
405
+ d.target = _str(ch.value)
406
+ elif ch.key == "inputs":
407
+ for it in ch.children:
408
+ if it.key is None:
409
+ continue
410
+ inp = Input(it.key, it.line, it.value)
411
+ inp.item = it
412
+ for p in it.children:
413
+ if p.key == "description":
414
+ inp.description = _text_value(p)
415
+ elif p.key == "label":
416
+ inp.label = _str(p.value)
417
+ elif p.key == "is_required":
418
+ inp.is_required = _bool(p.value)
419
+ elif p.key == "is_user_input":
420
+ inp.is_user_input = _bool(p.value)
421
+ d.inputs[inp.name] = inp
422
+ return d
423
+
424
+
425
+ def _node(kind, name, item):
426
+ node = Node(kind, name, item.line)
427
+ for ch in item.children:
428
+ if ch.key == "label":
429
+ node.label = _str(ch.value)
430
+ elif ch.key == "description":
431
+ node.description = _text_value(ch)
432
+ elif ch.key == "model_config":
433
+ m = _child(ch, "model")
434
+ node.model = _str(m.value) if m else None
435
+ elif ch.key == "system":
436
+ ins = _child(ch, "instructions")
437
+ node.system = ins.block if ins is not None else None
438
+ elif ch.key == "actions":
439
+ node.action_defs = {d.name: d for d in (_action_def(c) for c in ch.children if c.key)}
440
+ elif ch.key == "reasoning":
441
+ for r in ch.children:
442
+ if r.key == "instructions":
443
+ node.blocks, node.procedure = _instruction_blocks(r)
444
+ elif r.key == "actions":
445
+ node.bindings = {b.name: b for b in (_binding(c) for c in r.children if c.key)}
446
+ return node
447
+
448
+
449
+ def bundle_target(agent_path):
450
+ """<target> of the bundle-meta.xml next to an .agent file, e.g. 'Shopper_Agent.v3'."""
451
+ d = os.path.dirname(os.path.abspath(agent_path))
452
+ for name in sorted(os.listdir(d)):
453
+ if name.endswith(".bundle-meta.xml"):
454
+ with open(os.path.join(d, name), encoding="utf-8") as f:
455
+ m = re.search(r"<target>\s*([^<]+?)\s*</target>", f.read())
456
+ if m:
457
+ return m.group(1)
458
+ return None
459
+
460
+
461
+ def parse_agent(text, path=None):
462
+ src = AgentSource(path)
463
+ root = _tree(_scan(text.split("\n")))
464
+ for top in root.children:
465
+ key = top.key or ""
466
+ head = key.split()
467
+ if key == "system":
468
+ ins = _child(top, "instructions")
469
+ src.system = ins.block if ins is not None else None
470
+ elif key == "model_config":
471
+ m = _child(top, "model")
472
+ src.model = _str(m.value) if m else None
473
+ elif key == "config":
474
+ dn = _child(top, "developer_name")
475
+ src.developer_name = _str(dn.value) if dn else None
476
+ elif len(head) == 2 and head[0] in NODE_KINDS:
477
+ if head[1] in src.nodes:
478
+ raise AgentSourceError(f"{path or 'agent'}: node {head[1]} defined twice")
479
+ src.nodes[head[1]] = _node(head[0], head[1], top)
480
+ if not src.nodes:
481
+ raise AgentSourceError(f"{path or 'agent'}: no start_agent / subagent nodes found")
482
+ return src
483
+
484
+
485
+ def agent_file(path):
486
+ """path itself, or the one .agent file inside a bundle dir."""
487
+ if not os.path.isdir(path):
488
+ return path
489
+ found = sorted(n for n in os.listdir(path) if n.endswith(".agent"))
490
+ if len(found) != 1:
491
+ raise AgentSourceError(f"{path}: expected one .agent file, found {len(found)}")
492
+ return os.path.join(path, found[0])
493
+
494
+
495
+ def load_agent(path, label=None):
496
+ """Parse an .agent file, or the one .agent inside a bundle dir. The label defaults to the
497
+ version suffix of the bundle-meta target ('v3'), else the file name."""
498
+ path = agent_file(path)
499
+ with open(path, encoding="utf-8") as f:
500
+ src = parse_agent(f.read(), path)
501
+ src.target = bundle_target(path)
502
+ if label:
503
+ src.label = label
504
+ elif src.target and "." in src.target:
505
+ src.label = src.target.rsplit(".", 1)[1]
506
+ else:
507
+ src.label = os.path.splitext(os.path.basename(path))[0]
508
+ return src
509
+
510
+
511
+ def _label_key(label):
512
+ return [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", label)]
513
+
514
+
515
+ def load_versions(path):
516
+ """The agent versions under path, sorted by label (v2 before v10): an .agent file or bundle
517
+ dir, a dir of .agent files, or a dir of bundle dirs (a retrieved aiAuthoringBundles/)."""
518
+ if os.path.isfile(path):
519
+ found = [load_agent(path)]
520
+ else:
521
+ names = sorted(os.listdir(path))
522
+ files = [n for n in names if n.endswith(".agent")]
523
+ if files:
524
+ found = [load_agent(os.path.join(path, n)) for n in files]
525
+ else:
526
+ found = [load_agent(os.path.join(path, n)) for n in names
527
+ if os.path.isdir(os.path.join(path, n))
528
+ and any(f.endswith(".agent") for f in os.listdir(os.path.join(path, n)))]
529
+ if not found:
530
+ raise AgentSourceError(f"{path}: no .agent files")
531
+ dupes = sorted(label for label, n in Counter(s.label for s in found).items() if n > 1)
532
+ if dupes:
533
+ raise AgentSourceError(f"{path}: version labels {dupes} occur more than once (one bundle dir per version)")
534
+ return sorted(found, key=lambda s: _label_key(s.label))
535
+
536
+
537
+ def main(argv=None):
538
+ import argparse
539
+ p = argparse.ArgumentParser(description="Print the instruction blocks and tools of an .agent file.")
540
+ p.add_argument("agent", help=".agent file or bundle dir")
541
+ p.add_argument("--node", help="only this node")
542
+ p.add_argument("--rendered", action="store_true", help="print blocks as rendered ({!@actions.X} -> X)")
543
+ args = p.parse_args(argv)
544
+ src = load_agent(args.agent)
545
+ print(f"# {src.path} label={src.label} target={src.target} model={src.model}")
546
+ if src.system is not None and not args.node:
547
+ print(f"\n== system.instructions (lines {src.system.start}-{src.system.end})")
548
+ print(src.system.rendered() if args.rendered else src.system.text)
549
+ for node in src.nodes.values():
550
+ if args.node and node.name != args.node:
551
+ continue
552
+ print(f"\n== {node.kind} {node.name} (line {node.line})" + (f" model={node.model}" if node.model else ""))
553
+ for k, b in enumerate(node.blocks):
554
+ flag = " conditional" if b.conditional else ""
555
+ print(f"-- block {k} lines {b.start}-{b.end} {b.style}{flag}")
556
+ print(b.rendered() if args.rendered else b.text)
557
+ for name, tool in src.node_tools(node.name).items():
558
+ print(f"-- tool {name} ({tool['kind']}, line {tool['line']}): {tool['description']!r}")
559
+ for prm, desc in tool["params"].items():
560
+ print(f" {prm}: {desc!r}")
561
+ return 0
562
+
563
+
564
+ if __name__ == "__main__":
565
+ sys.exit(main())
@@ -0,0 +1,95 @@
1
+ """Result cache (--cache-dir): a request already answered is not sent again.
2
+
3
+ An entry is keyed by the SFAP endpoint, the core tenant and the payload exactly as sent, so the
4
+ model alias, the generation settings and the seed (that is, the repeat) are all in the key: a
5
+ patched request, another model or another repeat is another entry. Judge calls go through the
6
+ same cache. Only answers are stored: an infra error, or a 200 without a generation, is sent
7
+ again by the next run.
8
+
9
+ <cache dir>/<key[:2]>/<key>.json {"schema": "bt.cache/1", "http_status", "latency_ms", "body"}
10
+
11
+ The response body is stored, not the scores, so a change to the scoring re-scores cached answers.
12
+ Entries are written through the redactor, atomically, with mode 0600. They hold the model's
13
+ answers to user conversations: keep the cache dir out of git, like run dirs.
14
+ """
15
+
16
+ import hashlib
17
+ import json
18
+ import os
19
+ import tempfile
20
+ import threading
21
+
22
+ from . import SCHEMA_CACHE
23
+ from .redact import redact_obj
24
+ from .sfap import parse_generation
25
+
26
+
27
+ class CachingClient:
28
+ """generate(payload) as SfapClient's, answered from cache_dir when the payload was answered before."""
29
+
30
+ def __init__(self, client, cache_dir):
31
+ self.client = client
32
+ self.cache_dir = cache_dir
33
+ self.hits = self.calls = self.write_errors = 0
34
+ self._lock = threading.Lock()
35
+ if not os.path.isdir(cache_dir):
36
+ os.makedirs(cache_dir)
37
+ os.chmod(cache_dir, 0o700)
38
+
39
+ def key(self, payload):
40
+ blob = json.dumps({"url": self.client.url, "tenant": self.client.core_tenant_id, "payload": payload},
41
+ sort_keys=True, separators=(",", ":"), ensure_ascii=False)
42
+ return hashlib.sha256(blob.encode("utf-8")).hexdigest()
43
+
44
+ def _path(self, key):
45
+ return os.path.join(self.cache_dir, key[:2], key + ".json")
46
+
47
+ @staticmethod
48
+ def _load(path):
49
+ try:
50
+ with open(path, encoding="utf-8") as f:
51
+ entry = json.load(f)
52
+ except (OSError, ValueError):
53
+ return None
54
+ return entry if isinstance(entry, dict) and entry.get("schema") == SCHEMA_CACHE else None
55
+
56
+ def _store(self, path, entry):
57
+ """Write the entry atomically; a failure is counted, not raised (the answer is still good)."""
58
+ tmp = None
59
+ try:
60
+ d = os.path.dirname(path)
61
+ if not os.path.isdir(d):
62
+ os.makedirs(d, exist_ok=True)
63
+ os.chmod(d, 0o700)
64
+ fd, tmp = tempfile.mkstemp(dir=d, prefix=".tmp-", suffix=".json") # mode 0600
65
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
66
+ json.dump(redact_obj(entry), f, ensure_ascii=False)
67
+ os.replace(tmp, path)
68
+ except OSError:
69
+ with self._lock:
70
+ self.write_errors += 1
71
+ if tmp and os.path.exists(tmp):
72
+ os.unlink(tmp)
73
+
74
+ def generate(self, payload):
75
+ path = self._path(self.key(payload))
76
+ entry = self._load(path)
77
+ parsed = parse_generation(entry.get("body")) if entry else None
78
+ if parsed is not None:
79
+ with self._lock:
80
+ self.hits += 1
81
+ return {"status": "ok", "http_status": entry.get("http_status"), "body": entry["body"],
82
+ "error": None, "error_kind": None, "attempts": 0, "latency_ms": entry.get("latency_ms"),
83
+ "response": parsed, "cached": True}
84
+ res = self.client.generate(payload)
85
+ with self._lock:
86
+ self.calls += 1
87
+ if res["status"] == "ok":
88
+ self._store(path, {"schema": SCHEMA_CACHE, "http_status": res.get("http_status"),
89
+ "latency_ms": res.get("latency_ms"), "body": res["body"]})
90
+ res["cached"] = False
91
+ return res
92
+
93
+ def stats(self):
94
+ with self._lock:
95
+ return {"hits": self.hits, "calls": self.calls, "write_errors": self.write_errors}