agent-first-data 0.8.0__tar.gz → 0.9.0__tar.gz

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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.8.0
4
- Summary: Agent-First Data (AFDATA) suffix-driven output formatting and protocol templates for AI agents
3
+ Version: 0.9.0
4
+ Summary: A naming convention that lets AI agents understand your data without being told what it means.
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
7
7
  Requires-Python: >=3.9
@@ -74,7 +74,7 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
74
74
 
75
75
  ## API Reference
76
76
 
77
- Total: **15 public APIs and 2 types** + **AFDATA logging** (3 protocol builders + 2 redacted value helpers + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat` + `RedactionPolicy`)
77
+ Public APIs are grouped by role: protocol builders, redaction helpers, output formatters, internal redaction tools, utility/CLI helpers, types, and AFDATA logging integration.
78
78
 
79
79
  ### Protocol Builders (returns dict)
80
80
 
@@ -91,13 +91,14 @@ build_json_error(message: str, hint: str = None, trace: Any = None) -> dict
91
91
  build_json(code: str, fields: Any, trace: Any = None) -> dict
92
92
  ```
93
93
 
94
- ### Redacted Values (returns Any)
94
+ ### Redaction Helpers (returns Any)
95
95
 
96
96
  Use these before raw HTTP/MCP/SSE serializers that do not call `output_json`.
97
97
 
98
98
  ```python
99
99
  redacted_value(value: Any) -> Any
100
100
  redacted_value_with(value: Any, redaction_policy: RedactionPolicy) -> Any
101
+ redacted_value_with_options(value: Any, redaction_options: RedactionOptions) -> Any
101
102
  ```
102
103
 
103
104
  **Use case:** structured protocol payloads (frameworks automatically serialize)
@@ -138,15 +139,18 @@ not_found = build_json(
138
139
  )
139
140
  ```
140
141
 
141
- ### CLI/Log Output (returns str)
142
+ ### Output Formatters (returns str)
142
143
 
143
- Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. YAML and Plain always redact `_secret` and apply human-readable formatting.
144
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. Use `OutputOptions` to pass legacy secret names such as `api_key` or request schema-preserving YAML/plain rendering with `OutputStyle.Raw`.
144
145
 
145
146
  ```python
146
147
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
147
148
  output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
149
+ output_json_with_options(value: Any, output_options: OutputOptions) -> str
148
150
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
151
+ output_yaml_with_options(value: Any, output_options: OutputOptions) -> str
149
152
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
153
+ output_plain_with_options(value: Any, output_options: OutputOptions) -> str
150
154
  ```
151
155
 
152
156
  ```python
@@ -154,8 +158,18 @@ class RedactionPolicy(enum.Enum):
154
158
  RedactionTraceOnly = "RedactionTraceOnly"
155
159
  RedactionNone = "RedactionNone"
156
160
  RedactionStrict = "RedactionStrict"
161
+
162
+ RedactionOptions(policy: RedactionPolicy | None = None, secret_names: Sequence[str] = ())
163
+
164
+ class OutputStyle(enum.Enum):
165
+ Readable = "Readable"
166
+ Raw = "Raw"
167
+
168
+ OutputOptions(redaction: RedactionOptions = RedactionOptions(), style: OutputStyle = OutputStyle.Readable)
157
169
  ```
158
170
 
171
+ Secret names match exact field names at any nesting level; there is no trim, case folding, hyphen/underscore normalization, glob, regex, or substring matching. `OutputOptions` combines `RedactionOptions` with `OutputStyle.Readable` (default suffix stripping and value formatting) or `OutputStyle.Raw` (no suffix stripping or value formatting).
172
+
159
173
  **Example:**
160
174
  ```python
161
175
  from agent_first_data import *
@@ -188,6 +202,7 @@ print(output_plain(data))
188
202
 
189
203
  ```python
190
204
  internal_redact_secrets(value: Any) -> None # Manually redact secrets in-place
205
+ internal_redact_secrets_with_options(value: Any, redaction_options: RedactionOptions) -> None
191
206
  ```
192
207
 
193
208
  Most users don't need this. Output functions automatically protect secrets.
@@ -219,6 +234,7 @@ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
219
234
  cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
220
235
  cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
221
236
  cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
237
+ cli_output_with_options(value: Any, format: OutputFormat, output_options: OutputOptions) -> str
222
238
  build_cli_error(message: str, hint: str = None) -> dict # {code:"error", error_code:"invalid_request", hint?, retryable:False, trace:{duration_ms:0}}
223
239
  ```
224
240
 
@@ -1,12 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: agent-first-data
3
- Version: 0.8.0
4
- Summary: Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates for AI agents
5
- License-Expression: MIT
6
- Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
7
- Requires-Python: >=3.9
8
- Description-Content-Type: text/markdown
9
-
10
1
  # agent-first-data
11
2
 
12
3
  **Agent-First Data (AFDATA)** — Suffix-driven output formatting and protocol templates for AI agents.
@@ -74,7 +65,7 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
74
65
 
75
66
  ## API Reference
76
67
 
77
- Total: **15 public APIs and 2 types** + **AFDATA logging** (3 protocol builders + 2 redacted value helpers + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat` + `RedactionPolicy`)
68
+ Public APIs are grouped by role: protocol builders, redaction helpers, output formatters, internal redaction tools, utility/CLI helpers, types, and AFDATA logging integration.
78
69
 
79
70
  ### Protocol Builders (returns dict)
80
71
 
@@ -91,13 +82,14 @@ build_json_error(message: str, hint: str = None, trace: Any = None) -> dict
91
82
  build_json(code: str, fields: Any, trace: Any = None) -> dict
92
83
  ```
93
84
 
94
- ### Redacted Values (returns Any)
85
+ ### Redaction Helpers (returns Any)
95
86
 
96
87
  Use these before raw HTTP/MCP/SSE serializers that do not call `output_json`.
97
88
 
98
89
  ```python
99
90
  redacted_value(value: Any) -> Any
100
91
  redacted_value_with(value: Any, redaction_policy: RedactionPolicy) -> Any
92
+ redacted_value_with_options(value: Any, redaction_options: RedactionOptions) -> Any
101
93
  ```
102
94
 
103
95
  **Use case:** structured protocol payloads (frameworks automatically serialize)
@@ -138,15 +130,18 @@ not_found = build_json(
138
130
  )
139
131
  ```
140
132
 
141
- ### CLI/Log Output (returns str)
133
+ ### Output Formatters (returns str)
142
134
 
143
- Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. YAML and Plain always redact `_secret` and apply human-readable formatting.
135
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. Use `OutputOptions` to pass legacy secret names such as `api_key` or request schema-preserving YAML/plain rendering with `OutputStyle.Raw`.
144
136
 
145
137
  ```python
146
138
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
147
139
  output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
140
+ output_json_with_options(value: Any, output_options: OutputOptions) -> str
148
141
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
142
+ output_yaml_with_options(value: Any, output_options: OutputOptions) -> str
149
143
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
144
+ output_plain_with_options(value: Any, output_options: OutputOptions) -> str
150
145
  ```
151
146
 
152
147
  ```python
@@ -154,8 +149,18 @@ class RedactionPolicy(enum.Enum):
154
149
  RedactionTraceOnly = "RedactionTraceOnly"
155
150
  RedactionNone = "RedactionNone"
156
151
  RedactionStrict = "RedactionStrict"
152
+
153
+ RedactionOptions(policy: RedactionPolicy | None = None, secret_names: Sequence[str] = ())
154
+
155
+ class OutputStyle(enum.Enum):
156
+ Readable = "Readable"
157
+ Raw = "Raw"
158
+
159
+ OutputOptions(redaction: RedactionOptions = RedactionOptions(), style: OutputStyle = OutputStyle.Readable)
157
160
  ```
158
161
 
162
+ Secret names match exact field names at any nesting level; there is no trim, case folding, hyphen/underscore normalization, glob, regex, or substring matching. `OutputOptions` combines `RedactionOptions` with `OutputStyle.Readable` (default suffix stripping and value formatting) or `OutputStyle.Raw` (no suffix stripping or value formatting).
163
+
159
164
  **Example:**
160
165
  ```python
161
166
  from agent_first_data import *
@@ -188,6 +193,7 @@ print(output_plain(data))
188
193
 
189
194
  ```python
190
195
  internal_redact_secrets(value: Any) -> None # Manually redact secrets in-place
196
+ internal_redact_secrets_with_options(value: Any, redaction_options: RedactionOptions) -> None
191
197
  ```
192
198
 
193
199
  Most users don't need this. Output functions automatically protect secrets.
@@ -219,6 +225,7 @@ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
219
225
  cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
220
226
  cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
221
227
  cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
228
+ cli_output_with_options(value: Any, format: OutputFormat, output_options: OutputOptions) -> str
222
229
  build_cli_error(message: str, hint: str = None) -> dict # {code:"error", error_code:"invalid_request", hint?, retryable:False, trace:{duration_ms:0}}
223
230
  ```
224
231
 
@@ -5,13 +5,21 @@ from agent_first_data.format import (
5
5
  build_json_error,
6
6
  build_json,
7
7
  RedactionPolicy,
8
+ RedactionOptions,
9
+ OutputStyle,
10
+ OutputOptions,
8
11
  output_json,
9
12
  output_json_with,
13
+ output_json_with_options,
10
14
  output_yaml,
15
+ output_yaml_with_options,
11
16
  output_plain,
17
+ output_plain_with_options,
12
18
  internal_redact_secrets,
19
+ internal_redact_secrets_with_options,
13
20
  redacted_value,
14
21
  redacted_value_with,
22
+ redacted_value_with_options,
15
23
  parse_size,
16
24
  )
17
25
 
@@ -30,6 +38,7 @@ from agent_first_data.cli import (
30
38
  cli_parse_output,
31
39
  cli_parse_log_filters,
32
40
  cli_output,
41
+ cli_output_with_options,
33
42
  build_cli_error,
34
43
  )
35
44
 
@@ -38,13 +47,21 @@ __all__ = [
38
47
  "build_json_error",
39
48
  "build_json",
40
49
  "RedactionPolicy",
50
+ "RedactionOptions",
51
+ "OutputStyle",
52
+ "OutputOptions",
41
53
  "output_json",
42
54
  "output_json_with",
55
+ "output_json_with_options",
43
56
  "output_yaml",
57
+ "output_yaml_with_options",
44
58
  "output_plain",
59
+ "output_plain_with_options",
45
60
  "internal_redact_secrets",
61
+ "internal_redact_secrets_with_options",
46
62
  "redacted_value",
47
63
  "redacted_value_with",
64
+ "redacted_value_with_options",
48
65
  "parse_size",
49
66
  "AfdataHandler",
50
67
  "AfdataJsonHandler",
@@ -57,5 +74,6 @@ __all__ = [
57
74
  "cli_parse_output",
58
75
  "cli_parse_log_filters",
59
76
  "cli_output",
77
+ "cli_output_with_options",
60
78
  "build_cli_error",
61
79
  ]
@@ -5,7 +5,15 @@ from __future__ import annotations
5
5
  import enum
6
6
  from typing import Any
7
7
 
8
- from agent_first_data.format import output_json, output_yaml, output_plain
8
+ from agent_first_data.format import (
9
+ OutputOptions,
10
+ output_json,
11
+ output_json_with_options,
12
+ output_yaml,
13
+ output_yaml_with_options,
14
+ output_plain,
15
+ output_plain_with_options,
16
+ )
9
17
 
10
18
 
11
19
  class OutputFormat(enum.Enum):
@@ -69,6 +77,22 @@ def cli_output(value: Any, format: OutputFormat) -> str:
69
77
  return output_json(value)
70
78
 
71
79
 
80
+ def cli_output_with_options(
81
+ value: Any,
82
+ format: OutputFormat,
83
+ output_options: OutputOptions,
84
+ ) -> str:
85
+ """Dispatch output formatting with explicit redaction and style.
86
+
87
+ JSON ignores OutputStyle and preserves original keys and values after redaction.
88
+ """
89
+ if format is OutputFormat.YAML:
90
+ return output_yaml_with_options(value, output_options)
91
+ if format is OutputFormat.PLAIN:
92
+ return output_plain_with_options(value, output_options)
93
+ return output_json_with_options(value, output_options)
94
+
95
+
72
96
  def build_cli_error(message: str, hint: str | None = None) -> dict:
73
97
  """Build a standard CLI parse error value.
74
98
 
@@ -1,16 +1,18 @@
1
1
  """AFDATA output formatting and protocol templates.
2
2
 
3
- 11 public APIs and 1 type: protocol builders, redacted value helpers,
4
- output formatters, redaction, parse_size, and RedactionPolicy.
3
+ 16 public APIs and 4 types: protocol builders, redacted value helpers,
4
+ output formatters, redaction, parse_size, RedactionPolicy, RedactionOptions,
5
+ OutputStyle, and OutputOptions.
5
6
  """
6
7
 
7
8
  from __future__ import annotations
8
9
 
9
10
  import json
10
11
  import math
12
+ from dataclasses import dataclass, field
11
13
  from datetime import datetime, timezone
12
14
  from enum import Enum
13
- from typing import Any
15
+ from typing import Any, Sequence
14
16
 
15
17
 
16
18
  # ═══════════════════════════════════════════
@@ -55,6 +57,30 @@ class RedactionPolicy(str, Enum):
55
57
  RedactionStrict = "RedactionStrict"
56
58
 
57
59
 
60
+ @dataclass(frozen=True)
61
+ class RedactionOptions:
62
+ """Redaction options for legacy secret field names."""
63
+
64
+ policy: RedactionPolicy | None = None
65
+ # Exact field-name matches at any nesting level.
66
+ secret_names: Sequence[str] = ()
67
+
68
+
69
+ class OutputStyle(str, Enum):
70
+ """Rendering style for YAML and plain output."""
71
+
72
+ Readable = "Readable"
73
+ Raw = "Raw"
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class OutputOptions:
78
+ """Output options combining redaction and rendering style."""
79
+
80
+ redaction: RedactionOptions = field(default_factory=RedactionOptions)
81
+ style: OutputStyle = OutputStyle.Readable
82
+
83
+
58
84
  def output_json(value: Any) -> str:
59
85
  """Format as single-line JSON. Secrets redacted, original keys, raw values."""
60
86
  return json.dumps(redacted_value(value), ensure_ascii=False, separators=(",", ":"))
@@ -65,19 +91,44 @@ def output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str:
65
91
  return json.dumps(redacted_value_with(value, redaction_policy), ensure_ascii=False, separators=(",", ":"))
66
92
 
67
93
 
94
+ def output_json_with_options(value: Any, output_options: OutputOptions) -> str:
95
+ """Format as single-line JSON with explicit output options."""
96
+ return json.dumps(
97
+ redacted_value_with_options(value, output_options.redaction),
98
+ ensure_ascii=False,
99
+ separators=(",", ":"),
100
+ )
101
+
102
+
68
103
  def output_yaml(value: Any) -> str:
69
104
  """Format as multi-line YAML. Keys stripped, values formatted, secrets redacted."""
70
- value = redacted_value(value)
105
+ return output_yaml_with_options(value, OutputOptions())
106
+
107
+
108
+ def output_yaml_with_options(value: Any, output_options: OutputOptions) -> str:
109
+ """Format as multi-line YAML with explicit output options."""
110
+ value = redacted_value_with_options(value, output_options.redaction)
71
111
  lines = ["---"]
72
- _render_yaml_processed(value, 0, lines)
112
+ if output_options.style is OutputStyle.Raw:
113
+ _render_yaml_raw(value, 0, lines)
114
+ else:
115
+ _render_yaml_processed(value, 0, lines)
73
116
  return "\n".join(lines)
74
117
 
75
118
 
76
119
  def output_plain(value: Any) -> str:
77
120
  """Format as single-line logfmt. Keys stripped, values formatted, secrets redacted."""
78
- value = redacted_value(value)
121
+ return output_plain_with_options(value, OutputOptions())
122
+
123
+
124
+ def output_plain_with_options(value: Any, output_options: OutputOptions) -> str:
125
+ """Format as single-line logfmt with explicit output options."""
126
+ value = redacted_value_with_options(value, output_options.redaction)
79
127
  pairs: list[tuple[str, str]] = []
80
- _collect_plain_pairs(value, "", pairs)
128
+ if output_options.style is OutputStyle.Raw:
129
+ _collect_plain_pairs_raw(value, "", pairs)
130
+ else:
131
+ _collect_plain_pairs(value, "", pairs)
81
132
  pairs.sort(key=lambda p: p[0].encode("utf-16-be"))
82
133
  parts = []
83
134
  for k, v in pairs:
@@ -95,6 +146,11 @@ def internal_redact_secrets(value: Any) -> None:
95
146
  _redact_secrets(value)
96
147
 
97
148
 
149
+ def internal_redact_secrets_with_options(value: Any, redaction_options: RedactionOptions) -> None:
150
+ """Redact secret fields in-place using explicit redaction options."""
151
+ _apply_redaction_options(value, redaction_options)
152
+
153
+
98
154
  def redacted_value(value: Any) -> Any:
99
155
  """Return a JSON-safe copy with default _secret redaction applied."""
100
156
  v = _sanitize_for_json(value)
@@ -109,18 +165,38 @@ def redacted_value_with(value: Any, redaction_policy: RedactionPolicy) -> Any:
109
165
  return v
110
166
 
111
167
 
168
+ def redacted_value_with_options(value: Any, redaction_options: RedactionOptions) -> Any:
169
+ """Return a JSON-safe copy with explicit redaction options applied."""
170
+ v = _sanitize_for_json(value)
171
+ _apply_redaction_options(v, redaction_options)
172
+ return v
173
+
174
+
112
175
  def _apply_redaction_policy(value: Any, redaction_policy: RedactionPolicy) -> None:
176
+ _apply_redaction_policy_with_names(value, redaction_policy, frozenset())
177
+
178
+
179
+ def _apply_redaction_options(value: Any, redaction_options: RedactionOptions) -> None:
180
+ secret_names = _secret_name_set(redaction_options.secret_names)
181
+ _apply_redaction_policy_with_names(value, redaction_options.policy, secret_names)
182
+
183
+
184
+ def _apply_redaction_policy_with_names(
185
+ value: Any,
186
+ redaction_policy: RedactionPolicy | None,
187
+ secret_names: frozenset[str],
188
+ ) -> None:
113
189
  if redaction_policy == RedactionPolicy.RedactionTraceOnly:
114
190
  if isinstance(value, dict) and "trace" in value:
115
- _redact_secrets(value["trace"])
191
+ _redact_secrets(value["trace"], secret_names)
116
192
  return
117
193
  if redaction_policy == RedactionPolicy.RedactionNone:
118
194
  return
119
195
  if redaction_policy == RedactionPolicy.RedactionStrict:
120
- _redact_secrets_strict(value)
196
+ _redact_secrets_strict(value, secret_names)
121
197
  return
122
- # Safety fallback for unknown values.
123
- _redact_secrets(value)
198
+ # Empty/unknown policy falls back to default full redaction.
199
+ _redact_secrets(value, secret_names)
124
200
 
125
201
 
126
202
  def parse_size(s: str) -> int | None:
@@ -208,31 +284,43 @@ def _sanitize_for_json(value: Any, stack: set[int] | None = None) -> Any:
208
284
  return f"<unsupported:{type(value).__name__}>"
209
285
 
210
286
 
211
- def _redact_secrets(value: Any) -> None:
287
+ def _secret_name_set(secret_names: Sequence[str]) -> frozenset[str]:
288
+ return frozenset(secret_names)
289
+
290
+
291
+ def _key_has_secret_suffix(key: str) -> bool:
292
+ return key.endswith("_secret") or key.endswith("_SECRET")
293
+
294
+
295
+ def _is_secret_key(key: str, secret_names: frozenset[str]) -> bool:
296
+ return _key_has_secret_suffix(key) or key in secret_names
297
+
298
+
299
+ def _redact_secrets(value: Any, secret_names: frozenset[str] = frozenset()) -> None:
212
300
  if isinstance(value, dict):
213
301
  for k in list(value.keys()):
214
- if k.endswith("_secret") or k.endswith("_SECRET"):
302
+ if _is_secret_key(k, secret_names):
215
303
  if isinstance(value[k], (dict, list)):
216
- _redact_secrets(value[k])
304
+ _redact_secrets(value[k], secret_names)
217
305
  else:
218
306
  value[k] = "***"
219
307
  else:
220
- _redact_secrets(value[k])
308
+ _redact_secrets(value[k], secret_names)
221
309
  elif isinstance(value, list):
222
310
  for item in value:
223
- _redact_secrets(item)
311
+ _redact_secrets(item, secret_names)
224
312
 
225
313
 
226
- def _redact_secrets_strict(value: Any) -> None:
314
+ def _redact_secrets_strict(value: Any, secret_names: frozenset[str] = frozenset()) -> None:
227
315
  if isinstance(value, dict):
228
316
  for k in list(value.keys()):
229
- if k.endswith("_secret") or k.endswith("_SECRET"):
317
+ if _is_secret_key(k, secret_names):
230
318
  value[k] = "***"
231
319
  else:
232
- _redact_secrets_strict(value[k])
320
+ _redact_secrets_strict(value[k], secret_names)
233
321
  elif isinstance(value, list):
234
322
  for item in value:
235
- _redact_secrets_strict(item)
323
+ _redact_secrets_strict(item, secret_names)
236
324
 
237
325
 
238
326
  # ═══════════════════════════════════════════
@@ -546,6 +634,53 @@ def _render_yaml_processed(value: Any, indent: int, lines: list[str]) -> None:
546
634
  lines.append(f"{prefix}{display_key}: {_yaml_scalar(v)}")
547
635
 
548
636
 
637
+ def _render_yaml_raw(value: Any, indent: int, lines: list[str]) -> None:
638
+ prefix = " " * indent
639
+ if isinstance(value, dict):
640
+ for key in _sorted_object_keys(value):
641
+ _render_yaml_field_raw(prefix, key, value[key], indent, lines)
642
+ elif isinstance(value, list):
643
+ _render_yaml_array_raw(value, indent, lines)
644
+ else:
645
+ lines.append(f"{prefix}{_yaml_scalar(value)}")
646
+
647
+
648
+ def _render_yaml_field_raw(prefix: str, key: str, value: Any, indent: int, lines: list[str]) -> None:
649
+ if isinstance(value, dict):
650
+ if value:
651
+ lines.append(f"{prefix}{key}:")
652
+ _render_yaml_raw(value, indent + 1, lines)
653
+ else:
654
+ lines.append(f"{prefix}{key}: {{}}")
655
+ elif isinstance(value, list):
656
+ if value:
657
+ lines.append(f"{prefix}{key}:")
658
+ _render_yaml_array_raw(value, indent + 1, lines)
659
+ else:
660
+ lines.append(f"{prefix}{key}: []")
661
+ else:
662
+ lines.append(f"{prefix}{key}: {_yaml_scalar(value)}")
663
+
664
+
665
+ def _render_yaml_array_raw(arr: list[Any], indent: int, lines: list[str]) -> None:
666
+ prefix = " " * indent
667
+ for item in arr:
668
+ if isinstance(item, dict):
669
+ if item:
670
+ lines.append(f"{prefix}-")
671
+ _render_yaml_raw(item, indent + 1, lines)
672
+ else:
673
+ lines.append(f"{prefix}- {{}}")
674
+ elif isinstance(item, list):
675
+ if item:
676
+ lines.append(f"{prefix}-")
677
+ _render_yaml_array_raw(item, indent + 1, lines)
678
+ else:
679
+ lines.append(f"{prefix}- []")
680
+ else:
681
+ lines.append(f"{prefix}- {_yaml_scalar(item)}")
682
+
683
+
549
684
  def _escape_yaml_str(s: str) -> str:
550
685
  return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
551
686
 
@@ -586,6 +721,23 @@ def _collect_plain_pairs(value: Any, prefix: str, pairs: list[tuple[str, str]])
586
721
  pairs.append((full_key, _plain_scalar(v)))
587
722
 
588
723
 
724
+ def _collect_plain_pairs_raw(value: Any, prefix: str, pairs: list[tuple[str, str]]) -> None:
725
+ if not isinstance(value, dict):
726
+ return
727
+ for key in _sorted_object_keys(value):
728
+ v = value[key]
729
+ full_key = f"{prefix}.{key}" if prefix else key
730
+ if isinstance(v, dict):
731
+ _collect_plain_pairs_raw(v, full_key, pairs)
732
+ elif isinstance(v, list):
733
+ joined = ",".join(_plain_scalar_raw(item) for item in v)
734
+ pairs.append((full_key, joined))
735
+ elif v is None:
736
+ pairs.append((full_key, ""))
737
+ else:
738
+ pairs.append((full_key, _plain_scalar(v)))
739
+
740
+
589
741
  def _plain_scalar(value: Any) -> str:
590
742
  if isinstance(value, str):
591
743
  return value
@@ -598,6 +750,12 @@ def _plain_scalar(value: Any) -> str:
598
750
  return str(value)
599
751
 
600
752
 
753
+ def _plain_scalar_raw(value: Any) -> str:
754
+ if isinstance(value, (dict, list)):
755
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
756
+ return _plain_scalar(value)
757
+
758
+
601
759
  def _quote_logfmt_value(value: str) -> str:
602
760
  if value == "":
603
761
  return ""
@@ -612,3 +770,7 @@ def _quote_logfmt_value(value: str) -> str:
612
770
  .replace("\t", "\\t")
613
771
  )
614
772
  return f'"{escaped}"'
773
+
774
+
775
+ def _sorted_object_keys(d: dict) -> list[str]:
776
+ return sorted(d.keys(), key=lambda k: k.encode("utf-16-be"))
@@ -1,3 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-first-data
3
+ Version: 0.9.0
4
+ Summary: A naming convention that lets AI agents understand your data without being told what it means.
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
1
10
  # agent-first-data
2
11
 
3
12
  **Agent-First Data (AFDATA)** — Suffix-driven output formatting and protocol templates for AI agents.
@@ -65,7 +74,7 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
65
74
 
66
75
  ## API Reference
67
76
 
68
- Total: **15 public APIs and 2 types** + **AFDATA logging** (3 protocol builders + 2 redacted value helpers + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat` + `RedactionPolicy`)
77
+ Public APIs are grouped by role: protocol builders, redaction helpers, output formatters, internal redaction tools, utility/CLI helpers, types, and AFDATA logging integration.
69
78
 
70
79
  ### Protocol Builders (returns dict)
71
80
 
@@ -82,13 +91,14 @@ build_json_error(message: str, hint: str = None, trace: Any = None) -> dict
82
91
  build_json(code: str, fields: Any, trace: Any = None) -> dict
83
92
  ```
84
93
 
85
- ### Redacted Values (returns Any)
94
+ ### Redaction Helpers (returns Any)
86
95
 
87
96
  Use these before raw HTTP/MCP/SSE serializers that do not call `output_json`.
88
97
 
89
98
  ```python
90
99
  redacted_value(value: Any) -> Any
91
100
  redacted_value_with(value: Any, redaction_policy: RedactionPolicy) -> Any
101
+ redacted_value_with_options(value: Any, redaction_options: RedactionOptions) -> Any
92
102
  ```
93
103
 
94
104
  **Use case:** structured protocol payloads (frameworks automatically serialize)
@@ -129,15 +139,18 @@ not_found = build_json(
129
139
  )
130
140
  ```
131
141
 
132
- ### CLI/Log Output (returns str)
142
+ ### Output Formatters (returns str)
133
143
 
134
- Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. YAML and Plain always redact `_secret` and apply human-readable formatting.
144
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. Use `OutputOptions` to pass legacy secret names such as `api_key` or request schema-preserving YAML/plain rendering with `OutputStyle.Raw`.
135
145
 
136
146
  ```python
137
147
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
138
148
  output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
149
+ output_json_with_options(value: Any, output_options: OutputOptions) -> str
139
150
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
151
+ output_yaml_with_options(value: Any, output_options: OutputOptions) -> str
140
152
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
153
+ output_plain_with_options(value: Any, output_options: OutputOptions) -> str
141
154
  ```
142
155
 
143
156
  ```python
@@ -145,8 +158,18 @@ class RedactionPolicy(enum.Enum):
145
158
  RedactionTraceOnly = "RedactionTraceOnly"
146
159
  RedactionNone = "RedactionNone"
147
160
  RedactionStrict = "RedactionStrict"
161
+
162
+ RedactionOptions(policy: RedactionPolicy | None = None, secret_names: Sequence[str] = ())
163
+
164
+ class OutputStyle(enum.Enum):
165
+ Readable = "Readable"
166
+ Raw = "Raw"
167
+
168
+ OutputOptions(redaction: RedactionOptions = RedactionOptions(), style: OutputStyle = OutputStyle.Readable)
148
169
  ```
149
170
 
171
+ Secret names match exact field names at any nesting level; there is no trim, case folding, hyphen/underscore normalization, glob, regex, or substring matching. `OutputOptions` combines `RedactionOptions` with `OutputStyle.Readable` (default suffix stripping and value formatting) or `OutputStyle.Raw` (no suffix stripping or value formatting).
172
+
150
173
  **Example:**
151
174
  ```python
152
175
  from agent_first_data import *
@@ -179,6 +202,7 @@ print(output_plain(data))
179
202
 
180
203
  ```python
181
204
  internal_redact_secrets(value: Any) -> None # Manually redact secrets in-place
205
+ internal_redact_secrets_with_options(value: Any, redaction_options: RedactionOptions) -> None
182
206
  ```
183
207
 
184
208
  Most users don't need this. Output functions automatically protect secrets.
@@ -210,6 +234,7 @@ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
210
234
  cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
211
235
  cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
212
236
  cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
237
+ cli_output_with_options(value: Any, format: OutputFormat, output_options: OutputOptions) -> str
213
238
  build_cli_error(message: str, hint: str = None) -> dict # {code:"error", error_code:"invalid_request", hint?, retryable:False, trace:{duration_ms:0}}
214
239
  ```
215
240
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.8.0"
4
- description = "Agent-First Data (AFDATA) suffix-driven output formatting and protocol templates for AI agents"
3
+ version = "0.9.0"
4
+ description = "A naming convention that lets AI agents understand your data without being told what it means."
5
5
  license = "MIT"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.9"
@@ -2,9 +2,12 @@
2
2
  import pytest
3
3
  from agent_first_data import (
4
4
  OutputFormat,
5
+ OutputStyle,
6
+ OutputOptions,
5
7
  cli_parse_output,
6
8
  cli_parse_log_filters,
7
9
  cli_output,
10
+ cli_output_with_options,
8
11
  build_cli_error,
9
12
  output_json,
10
13
  )
@@ -106,3 +109,14 @@ def test_cli_output_dispatches_plain():
106
109
  out = cli_output(v, OutputFormat.PLAIN)
107
110
  assert "\n" not in out
108
111
  assert "code=ok" in out
112
+
113
+
114
+ def test_cli_output_with_options_dispatches_raw_yaml():
115
+ v = {"size_bytes": 1024}
116
+ out = cli_output_with_options(
117
+ v,
118
+ OutputFormat.YAML,
119
+ OutputOptions(style=OutputStyle.Raw),
120
+ )
121
+ assert "size_bytes: 1024" in out
122
+ assert "size:" not in out
@@ -8,13 +8,21 @@ from agent_first_data import (
8
8
  build_json_error,
9
9
  build_json,
10
10
  RedactionPolicy,
11
+ RedactionOptions,
12
+ OutputStyle,
13
+ OutputOptions,
11
14
  internal_redact_secrets,
15
+ internal_redact_secrets_with_options,
12
16
  redacted_value,
13
17
  redacted_value_with,
18
+ redacted_value_with_options,
14
19
  output_json,
15
20
  output_json_with,
21
+ output_json_with_options,
16
22
  output_yaml,
23
+ output_yaml_with_options,
17
24
  output_plain,
25
+ output_plain_with_options,
18
26
  )
19
27
  from agent_first_data.format import (
20
28
  _format_bytes_human,
@@ -31,6 +39,12 @@ def _load(name):
31
39
  return json.load(f)
32
40
 
33
41
 
42
+ def _redaction_options(case):
43
+ opts = case.get("options", {})
44
+ policy = RedactionPolicy(opts["policy"]) if "policy" in opts else None
45
+ return RedactionOptions(policy=policy, secret_names=opts.get("secret_names", ()))
46
+
47
+
34
48
  # --- Redact fixtures ---
35
49
 
36
50
 
@@ -42,6 +56,31 @@ def test_redact_fixtures():
42
56
  assert inp == case["expected"], f"[redact/{name}] got {inp}"
43
57
 
44
58
 
59
+ def test_redaction_options_fixtures():
60
+ for case in _load("redaction_options.json"):
61
+ name = case["name"]
62
+ options = _redaction_options(case)
63
+ output_options = OutputOptions(redaction=options, style=OutputStyle.Readable)
64
+ expected = case["expected"]
65
+
66
+ got = redacted_value_with_options(case["input"], options)
67
+ assert got == expected, f"[redaction_options/{name}] value mismatch: {got}"
68
+
69
+ inp = json.loads(json.dumps(case["input"]))
70
+ internal_redact_secrets_with_options(inp, options)
71
+ assert inp == expected, f"[redaction_options/{name}] in-place mismatch: {inp}"
72
+
73
+ got_json = json.loads(output_json_with_options(case["input"], output_options))
74
+ assert got_json == expected, f"[redaction_options/{name}] json mismatch: {got_json}"
75
+
76
+ if "expected_yaml" in case:
77
+ got_yaml = output_yaml_with_options(case["input"], output_options)
78
+ assert got_yaml == case["expected_yaml"], f"[redaction_options/{name}] yaml mismatch: {got_yaml!r}"
79
+ if "expected_plain" in case:
80
+ got_plain = output_plain_with_options(case["input"], output_options)
81
+ assert got_plain == case["expected_plain"], f"[redaction_options/{name}] plain mismatch: {got_plain!r}"
82
+
83
+
45
84
  # --- Protocol fixtures ---
46
85
 
47
86
 
@@ -111,6 +150,57 @@ def test_output_format_fixtures():
111
150
  assert got_plain == case["expected_plain"], f"[output/{name}] plain mismatch: {got_plain!r}"
112
151
 
113
152
 
153
+ def test_output_yaml_raw_keeps_suffix_keys_and_structure():
154
+ options = OutputOptions(
155
+ redaction=RedactionOptions(policy=RedactionPolicy.RedactionTraceOnly),
156
+ style=OutputStyle.Raw,
157
+ )
158
+ out = output_yaml_with_options(
159
+ {
160
+ "code": "result",
161
+ "rows": [{"api_key_secret": "sk-live-1", "duration_ms": 42}],
162
+ "trace": {"request_secret": "top-secret"},
163
+ },
164
+ options,
165
+ )
166
+
167
+ assert "rows:\n -" in out
168
+ assert 'api_key_secret: "sk-live-1"' in out
169
+ assert "duration_ms: 42" in out
170
+ assert 'request_secret: "***"' in out
171
+ assert 'duration: "42ms"' not in out
172
+
173
+
174
+ def test_output_plain_raw_keeps_suffix_keys_and_redacts_trace():
175
+ options = OutputOptions(
176
+ redaction=RedactionOptions(policy=RedactionPolicy.RedactionTraceOnly),
177
+ style=OutputStyle.Raw,
178
+ )
179
+ out = output_plain_with_options(
180
+ {
181
+ "duration_ms": 42,
182
+ "trace": {"request_secret": "top-secret"},
183
+ },
184
+ options,
185
+ )
186
+
187
+ assert "duration_ms=42" in out
188
+ assert "trace.request_secret=***" in out
189
+ assert "duration=42ms" not in out
190
+
191
+
192
+ def test_output_with_options_defaults_to_readable_style():
193
+ out = output_yaml_with_options(
194
+ {"duration_ms": 42},
195
+ OutputOptions(
196
+ redaction=RedactionOptions(policy=RedactionPolicy.RedactionNone),
197
+ style=OutputStyle.Readable,
198
+ ),
199
+ )
200
+ assert 'duration: "42ms"' in out
201
+ assert "duration_ms:" not in out
202
+
203
+
114
204
  def test_output_json_exception_field_is_readable():
115
205
  out = output_json({"error": Exception("timeout")})
116
206
  parsed = json.loads(out)