agent-first-data 0.2.4__tar.gz → 0.4.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.2.4
4
- Summary: Agent-First Data (AFD) — suffix-driven output formatting and protocol templates for AI agents
3
+ Version: 0.4.0
4
+ Summary: Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates for AI agents
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/cmnspore/agent-first-data
7
7
  Requires-Python: >=3.9
@@ -9,7 +9,7 @@ Description-Content-Type: text/markdown
9
9
 
10
10
  # agent-first-data
11
11
 
12
- **Agent-First Data (AFD)** — Suffix-driven output formatting and protocol templates for AI agents.
12
+ **Agent-First Data (AFDATA)** — Suffix-driven output formatting and protocol templates for AI agents.
13
13
 
14
14
  The field name is the schema. Agents read `latency_ms` and know milliseconds, `api_key_secret` and know to redact, no external schema needed.
15
15
 
@@ -27,24 +27,39 @@ A backup tool invoked from the CLI — flags, env vars, and config all use the s
27
27
  API_KEY_SECRET=sk-1234 cloudback --timeout-s 30 --max-file-size-bytes 10737418240 /data/backup.tar.gz
28
28
  ```
29
29
 
30
- The tool reads env vars, flags, and config — all with AFD suffixes — and emits a startup message:
30
+ For CLI diagnostics, enable log categories explicitly:
31
+
32
+ ```bash
33
+ --log startup,request,progress,retry,redirect
34
+ --verbose # shorthand for all categories
35
+ ```
36
+
37
+ Without these flags, startup diagnostics should stay off by default.
38
+
39
+ The tool reads env vars, flags, and config — all with AFDATA suffixes — and can emit a startup diagnostic event:
31
40
 
32
41
  ```python
33
42
  from agent_first_data import *
34
43
  import os
35
44
 
36
- startup = build_json_startup(
37
- {"timeout_s": 30, "max_file_size_bytes": 10737418240},
38
- {"input_path": "/data/backup.tar.gz"},
39
- {"API_KEY_SECRET": os.environ.get("API_KEY_SECRET")},
45
+ startup = build_json(
46
+ "log",
47
+ {
48
+ "event": "startup",
49
+ "config": {"timeout_s": 30, "max_file_size_bytes": 10737418240},
50
+ "args": {"input_path": "/data/backup.tar.gz"},
51
+ "env": {"API_KEY_SECRET": os.environ.get("API_KEY_SECRET")},
52
+ },
53
+ trace=None,
40
54
  )
41
55
  ```
42
56
 
43
57
  Three output formats, same data:
44
58
 
45
59
  ```
46
- JSON: {"code":"startup","args":{"input_path":"/data/backup.tar.gz"},"config":{"max_file_size_bytes":10737418240,"timeout_s":30},"env":{"API_KEY_SECRET":"***"}}
47
- YAML: code: "startup"
60
+ JSON: {"code":"log","event":"startup","args":{"input_path":"/data/backup.tar.gz"},"config":{"max_file_size_bytes":10737418240,"timeout_s":30},"env":{"API_KEY_SECRET":"***"}}
61
+ YAML: code: "log"
62
+ event: "startup"
48
63
  args:
49
64
  input_path: "/data/backup.tar.gz"
50
65
  config:
@@ -52,23 +67,20 @@ YAML: code: "startup"
52
67
  timeout: "30s"
53
68
  env:
54
69
  API_KEY: "***"
55
- Plain: args.input_path=/data/backup.tar.gz code=startup config.max_file_size=10.0GB config.timeout=30s env.API_KEY=***
70
+ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_file_size=10.0GB config.timeout=30s env.API_KEY=***
56
71
  ```
57
72
 
58
73
  `--timeout-s` → `timeout_s` → `timeout: 30s`. `API_KEY_SECRET` → `API_KEY: "***"`. The suffix is the schema.
59
74
 
60
75
  ## API Reference
61
76
 
62
- Total: **9 public APIs** + **AFD logging** (4 protocol builders + 3 output functions + 1 internal + 1 utility)
77
+ Total: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
63
78
 
64
79
  ### Protocol Builders (returns dict)
65
80
 
66
- Build AFD protocol structures. Return dict objects for API responses.
81
+ Build AFDATA protocol structures. Return dict objects for API responses.
67
82
 
68
83
  ```python
69
- # Startup (configuration)
70
- build_json_startup(config: Any, args: Any, env: Any) -> dict
71
-
72
84
  # Success (result)
73
85
  build_json_ok(result: Any, trace: Any = None) -> dict
74
86
 
@@ -86,10 +98,15 @@ build_json(code: str, fields: Any, trace: Any = None) -> dict
86
98
  from agent_first_data import *
87
99
 
88
100
  # Startup
89
- startup = build_json_startup(
90
- {"api_key_secret": "sk-123", "timeout_s": 30},
91
- {"config_path": "config.yml"},
92
- {"RUST_LOG": "info"},
101
+ startup = build_json(
102
+ "log",
103
+ {
104
+ "event": "startup",
105
+ "config": {"api_key_secret": "sk-123", "timeout_s": 30},
106
+ "args": {"config_path": "config.yml"},
107
+ "env": {"RUST_LOG": "info"},
108
+ },
109
+ trace=None,
93
110
  )
94
111
 
95
112
  # Success (always include trace)
@@ -170,6 +187,41 @@ assert parse_size("1.5K") == 1536
170
187
  assert parse_size("512") == 512
171
188
  ```
172
189
 
190
+ ### CLI Helpers (for tools built on AFDATA)
191
+
192
+ Shared helpers that prevent flag-parsing drift between CLI tools. Use these instead of reimplementing `--output` and `--log` handling in each tool.
193
+
194
+ ```python
195
+ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
196
+
197
+ cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
198
+ cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
199
+ cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
200
+ build_cli_error(message: str) -> dict # {code:"error", error_code:"invalid_request", retryable:False, trace:{duration_ms:0}}
201
+ ```
202
+
203
+ **Canonical pattern** — parse all flags before doing work, emit JSONL errors to stdout:
204
+
205
+ ```python
206
+ import sys
207
+ from agent_first_data import (
208
+ OutputFormat, cli_parse_output, cli_parse_log_filters,
209
+ cli_output, build_cli_error, output_json,
210
+ )
211
+
212
+ try:
213
+ fmt = cli_parse_output(args.output)
214
+ except ValueError as e:
215
+ print(output_json(build_cli_error(str(e))))
216
+ sys.exit(2)
217
+
218
+ log = cli_parse_log_filters(args.log.split(",") if args.log else [])
219
+ # ... do work ...
220
+ print(cli_output(result, fmt))
221
+ ```
222
+
223
+ See `examples/agent_cli.py` for the complete working example (`pytest examples/agent_cli.py`).
224
+
173
225
  ## Usage Examples
174
226
 
175
227
  ### Example 1: REST API
@@ -196,14 +248,20 @@ async def get_user(user_id: int):
196
248
  from agent_first_data import *
197
249
 
198
250
  # 1. Startup
199
- startup = build_json_startup(
200
- {"api_key_secret": "sk-sensitive-key", "timeout_s": 30},
201
- {"input_path": "data.json"},
202
- {"RUST_LOG": "info"},
251
+ startup = build_json(
252
+ "log",
253
+ {
254
+ "event": "startup",
255
+ "config": {"api_key_secret": "sk-sensitive-key", "timeout_s": 30},
256
+ "args": {"input_path": "data.json"},
257
+ "env": {"RUST_LOG": "info"},
258
+ },
259
+ trace=None,
203
260
  )
204
261
  print(output_yaml(startup))
205
262
  # ---
206
- # code: "startup"
263
+ # code: "log"
264
+ # event: "startup"
207
265
  # args:
208
266
  # input_path: "data.json"
209
267
  # config:
@@ -253,6 +311,7 @@ result = build_json_ok(
253
311
  )
254
312
 
255
313
  # Print JSONL to stdout (secrets redacted, one JSON object per line)
314
+ # Channel policy: machine-readable protocol/log events must not use stderr.
256
315
  print(output_json(result))
257
316
  # {"code":"ok","result":{"status":"success"},"trace":{"api_key_secret":"***","duration_ms":250}}
258
317
  ```
@@ -294,23 +353,23 @@ print(output_plain(data))
294
353
  # api_key=*** cache_ttl=3600s count=42 created_at=2025-02-07T00:00:00.000Z file_size=5.0MB payment=50000000msats price=$99.99 request_timeout=5.0s success_rate=95.5% user_name=alice
295
354
  ```
296
355
 
297
- ## AFD Logging
356
+ ## AFDATA Logging
298
357
 
299
- AFD-compliant structured logging via Python's `logging` module. Every log line is formatted using the library's own `output_json`/`output_plain`/`output_yaml` functions. Span fields are carried via `contextvars` (async-safe), automatically flattened into each log line.
358
+ AFDATA-compliant structured logging via Python's `logging` module. Every log line is formatted using the library's own `output_json`/`output_plain`/`output_yaml` functions. Span fields are carried via `contextvars` (async-safe), automatically flattened into each log line.
300
359
 
301
360
  ### API
302
361
 
303
362
  ```python
304
363
  from agent_first_data import init_logging_json, init_logging_plain, init_logging_yaml
305
- from agent_first_data.afd_logging import AfdHandler, get_logger, span
364
+ from agent_first_data.afdata_logging import AfdataHandler, get_logger, span
306
365
 
307
- # Convenience initializers — set up the root logger with AFD output to stdout
366
+ # Convenience initializers — set up the root logger with AFDATA output to stdout
308
367
  init_logging_json(level="INFO") # Single-line JSONL (secrets redacted, original keys)
309
368
  init_logging_plain(level="INFO") # Single-line logfmt (keys stripped, values formatted)
310
369
  init_logging_yaml(level="INFO") # Multi-line YAML (keys stripped, values formatted)
311
370
 
312
371
  # Low-level — create a handler for custom logger stacks
313
- AfdHandler(format="json") # format: "json" | "plain" | "yaml"
372
+ AfdataHandler(format="json") # format: "json" | "plain" | "yaml"
314
373
 
315
374
  # Logger with default fields (returns logging.LoggerAdapter)
316
375
  get_logger(name, **fields)
@@ -391,8 +450,8 @@ The `code` field defaults to the log level. Override with an explicit field:
391
450
  from agent_first_data import get_logger
392
451
 
393
452
  logger = get_logger("myapp")
394
- logger.info("Server ready", extra={"code": "startup"})
395
- # {"timestamp_epoch_ms":...,"message":"Server ready","target":"myapp","code":"startup"}
453
+ logger.info("Server ready", extra={"code": "log", "event": "startup"})
454
+ # {"timestamp_epoch_ms":...,"message":"Server ready","target":"myapp","code":"log","event":"startup"}
396
455
  ```
397
456
 
398
457
  ### Output Fields
@@ -410,7 +469,7 @@ Every log line contains:
410
469
 
411
470
  ### Log Output Formats
412
471
 
413
- All three formats use the library's own output functions, so AFD suffix processing applies to log fields too:
472
+ All three formats use the library's own output functions, so AFDATA suffix processing applies to log fields too:
414
473
 
415
474
  | Format | Function | Keys | Values | Use case |
416
475
  |:-------|:---------|:-----|:-------|:---------|
@@ -444,8 +503,8 @@ All formats automatically redact `_secret` fields.
444
503
 
445
504
  This package is part of the [agent-first-data](https://github.com/cmnspore/agent-first-data) repository, which also contains:
446
505
 
447
- - **`spec/`** — Full AFD specification with suffix definitions, protocol format rules, and cross-language test fixtures
448
- - **`skills/`** — Claude Code skill for AI agents working with AFD conventions
506
+ - **`spec/`** — Full AFDATA specification with suffix definitions, protocol format rules, and cross-language test fixtures
507
+ - **`skills/`** — AI coding agent skill for working with AFDATA conventions
449
508
 
450
509
  To run tests, clone the full repository (tests use shared cross-language fixtures from `spec/fixtures/`):
451
510
 
@@ -1,15 +1,6 @@
1
- Metadata-Version: 2.4
2
- Name: agent-first-data
3
- Version: 0.2.4
4
- Summary: Agent-First Data (AFD) — suffix-driven output formatting and protocol templates for AI agents
5
- License-Expression: MIT
6
- Project-URL: Repository, https://github.com/cmnspore/agent-first-data
7
- Requires-Python: >=3.9
8
- Description-Content-Type: text/markdown
9
-
10
1
  # agent-first-data
11
2
 
12
- **Agent-First Data (AFD)** — Suffix-driven output formatting and protocol templates for AI agents.
3
+ **Agent-First Data (AFDATA)** — Suffix-driven output formatting and protocol templates for AI agents.
13
4
 
14
5
  The field name is the schema. Agents read `latency_ms` and know milliseconds, `api_key_secret` and know to redact, no external schema needed.
15
6
 
@@ -27,24 +18,39 @@ A backup tool invoked from the CLI — flags, env vars, and config all use the s
27
18
  API_KEY_SECRET=sk-1234 cloudback --timeout-s 30 --max-file-size-bytes 10737418240 /data/backup.tar.gz
28
19
  ```
29
20
 
30
- The tool reads env vars, flags, and config — all with AFD suffixes — and emits a startup message:
21
+ For CLI diagnostics, enable log categories explicitly:
22
+
23
+ ```bash
24
+ --log startup,request,progress,retry,redirect
25
+ --verbose # shorthand for all categories
26
+ ```
27
+
28
+ Without these flags, startup diagnostics should stay off by default.
29
+
30
+ The tool reads env vars, flags, and config — all with AFDATA suffixes — and can emit a startup diagnostic event:
31
31
 
32
32
  ```python
33
33
  from agent_first_data import *
34
34
  import os
35
35
 
36
- startup = build_json_startup(
37
- {"timeout_s": 30, "max_file_size_bytes": 10737418240},
38
- {"input_path": "/data/backup.tar.gz"},
39
- {"API_KEY_SECRET": os.environ.get("API_KEY_SECRET")},
36
+ startup = build_json(
37
+ "log",
38
+ {
39
+ "event": "startup",
40
+ "config": {"timeout_s": 30, "max_file_size_bytes": 10737418240},
41
+ "args": {"input_path": "/data/backup.tar.gz"},
42
+ "env": {"API_KEY_SECRET": os.environ.get("API_KEY_SECRET")},
43
+ },
44
+ trace=None,
40
45
  )
41
46
  ```
42
47
 
43
48
  Three output formats, same data:
44
49
 
45
50
  ```
46
- JSON: {"code":"startup","args":{"input_path":"/data/backup.tar.gz"},"config":{"max_file_size_bytes":10737418240,"timeout_s":30},"env":{"API_KEY_SECRET":"***"}}
47
- YAML: code: "startup"
51
+ JSON: {"code":"log","event":"startup","args":{"input_path":"/data/backup.tar.gz"},"config":{"max_file_size_bytes":10737418240,"timeout_s":30},"env":{"API_KEY_SECRET":"***"}}
52
+ YAML: code: "log"
53
+ event: "startup"
48
54
  args:
49
55
  input_path: "/data/backup.tar.gz"
50
56
  config:
@@ -52,23 +58,20 @@ YAML: code: "startup"
52
58
  timeout: "30s"
53
59
  env:
54
60
  API_KEY: "***"
55
- Plain: args.input_path=/data/backup.tar.gz code=startup config.max_file_size=10.0GB config.timeout=30s env.API_KEY=***
61
+ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_file_size=10.0GB config.timeout=30s env.API_KEY=***
56
62
  ```
57
63
 
58
64
  `--timeout-s` → `timeout_s` → `timeout: 30s`. `API_KEY_SECRET` → `API_KEY: "***"`. The suffix is the schema.
59
65
 
60
66
  ## API Reference
61
67
 
62
- Total: **9 public APIs** + **AFD logging** (4 protocol builders + 3 output functions + 1 internal + 1 utility)
68
+ Total: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
63
69
 
64
70
  ### Protocol Builders (returns dict)
65
71
 
66
- Build AFD protocol structures. Return dict objects for API responses.
72
+ Build AFDATA protocol structures. Return dict objects for API responses.
67
73
 
68
74
  ```python
69
- # Startup (configuration)
70
- build_json_startup(config: Any, args: Any, env: Any) -> dict
71
-
72
75
  # Success (result)
73
76
  build_json_ok(result: Any, trace: Any = None) -> dict
74
77
 
@@ -86,10 +89,15 @@ build_json(code: str, fields: Any, trace: Any = None) -> dict
86
89
  from agent_first_data import *
87
90
 
88
91
  # Startup
89
- startup = build_json_startup(
90
- {"api_key_secret": "sk-123", "timeout_s": 30},
91
- {"config_path": "config.yml"},
92
- {"RUST_LOG": "info"},
92
+ startup = build_json(
93
+ "log",
94
+ {
95
+ "event": "startup",
96
+ "config": {"api_key_secret": "sk-123", "timeout_s": 30},
97
+ "args": {"config_path": "config.yml"},
98
+ "env": {"RUST_LOG": "info"},
99
+ },
100
+ trace=None,
93
101
  )
94
102
 
95
103
  # Success (always include trace)
@@ -170,6 +178,41 @@ assert parse_size("1.5K") == 1536
170
178
  assert parse_size("512") == 512
171
179
  ```
172
180
 
181
+ ### CLI Helpers (for tools built on AFDATA)
182
+
183
+ Shared helpers that prevent flag-parsing drift between CLI tools. Use these instead of reimplementing `--output` and `--log` handling in each tool.
184
+
185
+ ```python
186
+ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
187
+
188
+ cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
189
+ cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
190
+ cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
191
+ build_cli_error(message: str) -> dict # {code:"error", error_code:"invalid_request", retryable:False, trace:{duration_ms:0}}
192
+ ```
193
+
194
+ **Canonical pattern** — parse all flags before doing work, emit JSONL errors to stdout:
195
+
196
+ ```python
197
+ import sys
198
+ from agent_first_data import (
199
+ OutputFormat, cli_parse_output, cli_parse_log_filters,
200
+ cli_output, build_cli_error, output_json,
201
+ )
202
+
203
+ try:
204
+ fmt = cli_parse_output(args.output)
205
+ except ValueError as e:
206
+ print(output_json(build_cli_error(str(e))))
207
+ sys.exit(2)
208
+
209
+ log = cli_parse_log_filters(args.log.split(",") if args.log else [])
210
+ # ... do work ...
211
+ print(cli_output(result, fmt))
212
+ ```
213
+
214
+ See `examples/agent_cli.py` for the complete working example (`pytest examples/agent_cli.py`).
215
+
173
216
  ## Usage Examples
174
217
 
175
218
  ### Example 1: REST API
@@ -196,14 +239,20 @@ async def get_user(user_id: int):
196
239
  from agent_first_data import *
197
240
 
198
241
  # 1. Startup
199
- startup = build_json_startup(
200
- {"api_key_secret": "sk-sensitive-key", "timeout_s": 30},
201
- {"input_path": "data.json"},
202
- {"RUST_LOG": "info"},
242
+ startup = build_json(
243
+ "log",
244
+ {
245
+ "event": "startup",
246
+ "config": {"api_key_secret": "sk-sensitive-key", "timeout_s": 30},
247
+ "args": {"input_path": "data.json"},
248
+ "env": {"RUST_LOG": "info"},
249
+ },
250
+ trace=None,
203
251
  )
204
252
  print(output_yaml(startup))
205
253
  # ---
206
- # code: "startup"
254
+ # code: "log"
255
+ # event: "startup"
207
256
  # args:
208
257
  # input_path: "data.json"
209
258
  # config:
@@ -253,6 +302,7 @@ result = build_json_ok(
253
302
  )
254
303
 
255
304
  # Print JSONL to stdout (secrets redacted, one JSON object per line)
305
+ # Channel policy: machine-readable protocol/log events must not use stderr.
256
306
  print(output_json(result))
257
307
  # {"code":"ok","result":{"status":"success"},"trace":{"api_key_secret":"***","duration_ms":250}}
258
308
  ```
@@ -294,23 +344,23 @@ print(output_plain(data))
294
344
  # api_key=*** cache_ttl=3600s count=42 created_at=2025-02-07T00:00:00.000Z file_size=5.0MB payment=50000000msats price=$99.99 request_timeout=5.0s success_rate=95.5% user_name=alice
295
345
  ```
296
346
 
297
- ## AFD Logging
347
+ ## AFDATA Logging
298
348
 
299
- AFD-compliant structured logging via Python's `logging` module. Every log line is formatted using the library's own `output_json`/`output_plain`/`output_yaml` functions. Span fields are carried via `contextvars` (async-safe), automatically flattened into each log line.
349
+ AFDATA-compliant structured logging via Python's `logging` module. Every log line is formatted using the library's own `output_json`/`output_plain`/`output_yaml` functions. Span fields are carried via `contextvars` (async-safe), automatically flattened into each log line.
300
350
 
301
351
  ### API
302
352
 
303
353
  ```python
304
354
  from agent_first_data import init_logging_json, init_logging_plain, init_logging_yaml
305
- from agent_first_data.afd_logging import AfdHandler, get_logger, span
355
+ from agent_first_data.afdata_logging import AfdataHandler, get_logger, span
306
356
 
307
- # Convenience initializers — set up the root logger with AFD output to stdout
357
+ # Convenience initializers — set up the root logger with AFDATA output to stdout
308
358
  init_logging_json(level="INFO") # Single-line JSONL (secrets redacted, original keys)
309
359
  init_logging_plain(level="INFO") # Single-line logfmt (keys stripped, values formatted)
310
360
  init_logging_yaml(level="INFO") # Multi-line YAML (keys stripped, values formatted)
311
361
 
312
362
  # Low-level — create a handler for custom logger stacks
313
- AfdHandler(format="json") # format: "json" | "plain" | "yaml"
363
+ AfdataHandler(format="json") # format: "json" | "plain" | "yaml"
314
364
 
315
365
  # Logger with default fields (returns logging.LoggerAdapter)
316
366
  get_logger(name, **fields)
@@ -391,8 +441,8 @@ The `code` field defaults to the log level. Override with an explicit field:
391
441
  from agent_first_data import get_logger
392
442
 
393
443
  logger = get_logger("myapp")
394
- logger.info("Server ready", extra={"code": "startup"})
395
- # {"timestamp_epoch_ms":...,"message":"Server ready","target":"myapp","code":"startup"}
444
+ logger.info("Server ready", extra={"code": "log", "event": "startup"})
445
+ # {"timestamp_epoch_ms":...,"message":"Server ready","target":"myapp","code":"log","event":"startup"}
396
446
  ```
397
447
 
398
448
  ### Output Fields
@@ -410,7 +460,7 @@ Every log line contains:
410
460
 
411
461
  ### Log Output Formats
412
462
 
413
- All three formats use the library's own output functions, so AFD suffix processing applies to log fields too:
463
+ All three formats use the library's own output functions, so AFDATA suffix processing applies to log fields too:
414
464
 
415
465
  | Format | Function | Keys | Values | Use case |
416
466
  |:-------|:---------|:-----|:-------|:---------|
@@ -444,8 +494,8 @@ All formats automatically redact `_secret` fields.
444
494
 
445
495
  This package is part of the [agent-first-data](https://github.com/cmnspore/agent-first-data) repository, which also contains:
446
496
 
447
- - **`spec/`** — Full AFD specification with suffix definitions, protocol format rules, and cross-language test fixtures
448
- - **`skills/`** — Claude Code skill for AI agents working with AFD conventions
497
+ - **`spec/`** — Full AFDATA specification with suffix definitions, protocol format rules, and cross-language test fixtures
498
+ - **`skills/`** — AI coding agent skill for working with AFDATA conventions
449
499
 
450
500
  To run tests, clone the full repository (tests use shared cross-language fixtures from `spec/fixtures/`):
451
501
 
@@ -1,7 +1,6 @@
1
- """Agent-First Data (AFD) — suffix-driven output formatting and protocol templates."""
1
+ """Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates."""
2
2
 
3
3
  from agent_first_data.format import (
4
- build_json_startup,
5
4
  build_json_ok,
6
5
  build_json_error,
7
6
  build_json,
@@ -12,9 +11,9 @@ from agent_first_data.format import (
12
11
  parse_size,
13
12
  )
14
13
 
15
- from agent_first_data.afd_logging import (
16
- AfdHandler,
17
- AfdJsonHandler,
14
+ from agent_first_data.afdata_logging import (
15
+ AfdataHandler,
16
+ AfdataJsonHandler,
18
17
  init_json as init_logging_json,
19
18
  init_plain as init_logging_plain,
20
19
  init_yaml as init_logging_yaml,
@@ -22,8 +21,15 @@ from agent_first_data.afd_logging import (
22
21
  span,
23
22
  )
24
23
 
24
+ from agent_first_data.cli import (
25
+ OutputFormat,
26
+ cli_parse_output,
27
+ cli_parse_log_filters,
28
+ cli_output,
29
+ build_cli_error,
30
+ )
31
+
25
32
  __all__ = [
26
- "build_json_startup",
27
33
  "build_json_ok",
28
34
  "build_json_error",
29
35
  "build_json",
@@ -32,11 +38,16 @@ __all__ = [
32
38
  "output_plain",
33
39
  "internal_redact_secrets",
34
40
  "parse_size",
35
- "AfdHandler",
36
- "AfdJsonHandler",
41
+ "AfdataHandler",
42
+ "AfdataJsonHandler",
37
43
  "init_logging_json",
38
44
  "init_logging_plain",
39
45
  "init_logging_yaml",
40
46
  "get_logger",
41
47
  "span",
48
+ "OutputFormat",
49
+ "cli_parse_output",
50
+ "cli_parse_log_filters",
51
+ "cli_output",
52
+ "build_cli_error",
42
53
  ]
@@ -1,4 +1,4 @@
1
- """AFD-compliant structured logging.
1
+ """AFDATA-compliant structured logging.
2
2
 
3
3
  Outputs log events using agent-first-data formatting functions:
4
4
  - JSON: single-line JSONL via output_json (secrets redacted, original keys)
@@ -8,7 +8,7 @@ Outputs log events using agent-first-data formatting functions:
8
8
  Span fields are carried via contextvars (async-safe).
9
9
 
10
10
  Usage:
11
- from agent_first_data.afd_logging import init_json, init_plain, init_yaml, span
11
+ from agent_first_data.afdata_logging import init_json, init_plain, init_yaml, span
12
12
  import logging
13
13
 
14
14
  init_json("INFO") # or init_plain("INFO") or init_yaml("DEBUG")
@@ -25,7 +25,7 @@ from typing import Any
25
25
 
26
26
  from agent_first_data.format import output_json, output_plain, output_yaml
27
27
 
28
- _span_fields: ContextVar[dict[str, Any]] = ContextVar("afd_span", default={})
28
+ _span_fields: ContextVar[dict[str, Any]] = ContextVar("afdata_span", default={})
29
29
 
30
30
  _LEVEL_TO_CODE = {
31
31
  "CRITICAL": "error",
@@ -38,8 +38,8 @@ _LEVEL_TO_CODE = {
38
38
  }
39
39
 
40
40
 
41
- class AfdHandler(logging.Handler):
42
- """Logging handler that outputs AFD-compliant log lines to stdout.
41
+ class AfdataHandler(logging.Handler):
42
+ """Logging handler that outputs AFDATA-compliant log lines to stdout.
43
43
 
44
44
  Formats output using the library's own output_json/output_plain/output_yaml.
45
45
  """
@@ -64,7 +64,7 @@ class AfdHandler(logging.Handler):
64
64
 
65
65
  # Event fields (passed via extra= in logging calls)
66
66
  has_code = False
67
- extra = getattr(record, "_afd_fields", None)
67
+ extra = getattr(record, "_afdata_fields", None)
68
68
  if extra:
69
69
  for k, v in extra.items():
70
70
  if k == "code":
@@ -88,11 +88,11 @@ class AfdHandler(logging.Handler):
88
88
 
89
89
 
90
90
  # Keep old name as alias for backwards compat
91
- AfdJsonHandler = AfdHandler
91
+ AfdataJsonHandler = AfdataHandler
92
92
 
93
93
 
94
- class _AfdLoggerAdapter(logging.LoggerAdapter):
95
- """Logger adapter that passes extra fields to AfdHandler."""
94
+ class _AfdataLoggerAdapter(logging.LoggerAdapter):
95
+ """Logger adapter that passes extra fields to AfdataHandler."""
96
96
 
97
97
  def process(self, msg: str, kwargs: Any) -> tuple[str, Any]:
98
98
  extra = kwargs.get("extra", {})
@@ -100,29 +100,29 @@ class _AfdLoggerAdapter(logging.LoggerAdapter):
100
100
  merged = {**self.extra, **extra}
101
101
  else:
102
102
  merged = extra
103
- kwargs["extra"] = {"_afd_fields": merged}
103
+ kwargs["extra"] = {"_afdata_fields": merged}
104
104
  return msg, kwargs
105
105
 
106
106
 
107
107
  def _init_with_format(format: str, level: str = "INFO") -> None:
108
- handler = AfdHandler(format=format)
108
+ handler = AfdataHandler(format=format)
109
109
  root = logging.getLogger()
110
110
  root.handlers = [handler]
111
111
  root.setLevel(getattr(logging, level.upper(), logging.INFO))
112
112
 
113
113
 
114
114
  def init_json(level: str = "INFO") -> None:
115
- """Initialize the root logger with AFD JSON output to stdout."""
115
+ """Initialize the root logger with AFDATA JSON output to stdout."""
116
116
  _init_with_format("json", level)
117
117
 
118
118
 
119
119
  def init_plain(level: str = "INFO") -> None:
120
- """Initialize the root logger with AFD plain/logfmt output to stdout."""
120
+ """Initialize the root logger with AFDATA plain/logfmt output to stdout."""
121
121
  _init_with_format("plain", level)
122
122
 
123
123
 
124
124
  def init_yaml(level: str = "INFO") -> None:
125
- """Initialize the root logger with AFD YAML output to stdout."""
125
+ """Initialize the root logger with AFDATA YAML output to stdout."""
126
126
  _init_with_format("yaml", level)
127
127
 
128
128
 
@@ -133,7 +133,7 @@ def get_logger(name: str, **fields: Any) -> logging.LoggerAdapter:
133
133
  Use for per-module or per-component fields.
134
134
  """
135
135
  base = logging.getLogger(name)
136
- return _AfdLoggerAdapter(base, fields)
136
+ return _AfdataLoggerAdapter(base, fields)
137
137
 
138
138
 
139
139
  class span:
@@ -0,0 +1,92 @@
1
+ """AFDATA CLI helpers — output format parsing, log filter normalization, error building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from typing import Any
7
+
8
+ from agent_first_data.format import output_json, output_yaml, output_plain
9
+
10
+
11
+ class OutputFormat(enum.Enum):
12
+ """Output format for CLI and pipe/MCP modes."""
13
+
14
+ JSON = "json"
15
+ YAML = "yaml"
16
+ PLAIN = "plain"
17
+
18
+
19
+ def cli_parse_output(s: str) -> OutputFormat:
20
+ """Parse the --output flag value into an OutputFormat.
21
+
22
+ Raises ValueError with a message suitable for build_cli_error on unknown values.
23
+
24
+ >>> cli_parse_output("json")
25
+ <OutputFormat.JSON: 'json'>
26
+ >>> cli_parse_output("xml")
27
+ Traceback (most recent call last):
28
+ ...
29
+ ValueError: invalid --output format 'xml': expected json, yaml, or plain
30
+ """
31
+ try:
32
+ return OutputFormat(s)
33
+ except ValueError:
34
+ raise ValueError(
35
+ f"invalid --output format {s!r}: expected json, yaml, or plain"
36
+ )
37
+
38
+
39
+ def cli_parse_log_filters(entries: list[str]) -> list[str]:
40
+ """Normalize --log flag entries: trim, lowercase, deduplicate, remove empty.
41
+
42
+ Accepts pre-split entries (e.g. after splitting on comma).
43
+
44
+ >>> cli_parse_log_filters(["Query", " error ", "query"])
45
+ ['query', 'error']
46
+ """
47
+ out: list[str] = []
48
+ for entry in entries:
49
+ s = entry.strip().lower()
50
+ if s and s not in out:
51
+ out.append(s)
52
+ return out
53
+
54
+
55
+ def cli_output(value: Any, format: OutputFormat) -> str:
56
+ """Dispatch output formatting by OutputFormat.
57
+
58
+ Equivalent to calling output_json, output_yaml, or output_plain directly.
59
+
60
+ >>> import json
61
+ >>> v = {"code": "ok"}
62
+ >>> cli_output(v, OutputFormat.JSON).startswith('{"code"')
63
+ True
64
+ """
65
+ if format is OutputFormat.YAML:
66
+ return output_yaml(value)
67
+ if format is OutputFormat.PLAIN:
68
+ return output_plain(value)
69
+ return output_json(value)
70
+
71
+
72
+ def build_cli_error(message: str) -> dict:
73
+ """Build a standard CLI parse error value.
74
+
75
+ Use when argument parsing fails or a flag value is invalid.
76
+ Print with output_json and exit with code 2.
77
+
78
+ >>> v = build_cli_error("--output: invalid value 'xml'")
79
+ >>> v["code"]
80
+ 'error'
81
+ >>> v["error_code"]
82
+ 'invalid_request'
83
+ >>> v["retryable"]
84
+ False
85
+ """
86
+ return {
87
+ "code": "error",
88
+ "error_code": "invalid_request",
89
+ "error": message,
90
+ "retryable": False,
91
+ "trace": {"duration_ms": 0},
92
+ }
@@ -1,6 +1,6 @@
1
- """AFD output formatting and protocol templates.
1
+ """AFDATA output formatting and protocol templates.
2
2
 
3
- 9 public APIs: 4 protocol builders + 3 output formatters + 1 redaction + 1 utility.
3
+ 8 public APIs: 3 protocol builders + 3 output formatters + 1 redaction + 1 utility.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
@@ -16,11 +16,6 @@ from typing import Any
16
16
  # ═══════════════════════════════════════════
17
17
 
18
18
 
19
- def build_json_startup(config: Any, args: Any, env: Any) -> dict:
20
- """Build {code: "startup", config, args, env}."""
21
- return {"code": "startup", "config": config, "args": args, "env": env}
22
-
23
-
24
19
  def build_json_ok(result: Any, trace: Any = None) -> dict:
25
20
  """Build {code: "ok", result, trace?}."""
26
21
  m: dict = {"code": "ok", "result": result}
@@ -1,6 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-first-data
3
+ Version: 0.4.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/cmnspore/agent-first-data
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
1
10
  # agent-first-data
2
11
 
3
- **Agent-First Data (AFD)** — Suffix-driven output formatting and protocol templates for AI agents.
12
+ **Agent-First Data (AFDATA)** — Suffix-driven output formatting and protocol templates for AI agents.
4
13
 
5
14
  The field name is the schema. Agents read `latency_ms` and know milliseconds, `api_key_secret` and know to redact, no external schema needed.
6
15
 
@@ -18,24 +27,39 @@ A backup tool invoked from the CLI — flags, env vars, and config all use the s
18
27
  API_KEY_SECRET=sk-1234 cloudback --timeout-s 30 --max-file-size-bytes 10737418240 /data/backup.tar.gz
19
28
  ```
20
29
 
21
- The tool reads env vars, flags, and config — all with AFD suffixes — and emits a startup message:
30
+ For CLI diagnostics, enable log categories explicitly:
31
+
32
+ ```bash
33
+ --log startup,request,progress,retry,redirect
34
+ --verbose # shorthand for all categories
35
+ ```
36
+
37
+ Without these flags, startup diagnostics should stay off by default.
38
+
39
+ The tool reads env vars, flags, and config — all with AFDATA suffixes — and can emit a startup diagnostic event:
22
40
 
23
41
  ```python
24
42
  from agent_first_data import *
25
43
  import os
26
44
 
27
- startup = build_json_startup(
28
- {"timeout_s": 30, "max_file_size_bytes": 10737418240},
29
- {"input_path": "/data/backup.tar.gz"},
30
- {"API_KEY_SECRET": os.environ.get("API_KEY_SECRET")},
45
+ startup = build_json(
46
+ "log",
47
+ {
48
+ "event": "startup",
49
+ "config": {"timeout_s": 30, "max_file_size_bytes": 10737418240},
50
+ "args": {"input_path": "/data/backup.tar.gz"},
51
+ "env": {"API_KEY_SECRET": os.environ.get("API_KEY_SECRET")},
52
+ },
53
+ trace=None,
31
54
  )
32
55
  ```
33
56
 
34
57
  Three output formats, same data:
35
58
 
36
59
  ```
37
- JSON: {"code":"startup","args":{"input_path":"/data/backup.tar.gz"},"config":{"max_file_size_bytes":10737418240,"timeout_s":30},"env":{"API_KEY_SECRET":"***"}}
38
- YAML: code: "startup"
60
+ JSON: {"code":"log","event":"startup","args":{"input_path":"/data/backup.tar.gz"},"config":{"max_file_size_bytes":10737418240,"timeout_s":30},"env":{"API_KEY_SECRET":"***"}}
61
+ YAML: code: "log"
62
+ event: "startup"
39
63
  args:
40
64
  input_path: "/data/backup.tar.gz"
41
65
  config:
@@ -43,23 +67,20 @@ YAML: code: "startup"
43
67
  timeout: "30s"
44
68
  env:
45
69
  API_KEY: "***"
46
- Plain: args.input_path=/data/backup.tar.gz code=startup config.max_file_size=10.0GB config.timeout=30s env.API_KEY=***
70
+ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_file_size=10.0GB config.timeout=30s env.API_KEY=***
47
71
  ```
48
72
 
49
73
  `--timeout-s` → `timeout_s` → `timeout: 30s`. `API_KEY_SECRET` → `API_KEY: "***"`. The suffix is the schema.
50
74
 
51
75
  ## API Reference
52
76
 
53
- Total: **9 public APIs** + **AFD logging** (4 protocol builders + 3 output functions + 1 internal + 1 utility)
77
+ Total: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
54
78
 
55
79
  ### Protocol Builders (returns dict)
56
80
 
57
- Build AFD protocol structures. Return dict objects for API responses.
81
+ Build AFDATA protocol structures. Return dict objects for API responses.
58
82
 
59
83
  ```python
60
- # Startup (configuration)
61
- build_json_startup(config: Any, args: Any, env: Any) -> dict
62
-
63
84
  # Success (result)
64
85
  build_json_ok(result: Any, trace: Any = None) -> dict
65
86
 
@@ -77,10 +98,15 @@ build_json(code: str, fields: Any, trace: Any = None) -> dict
77
98
  from agent_first_data import *
78
99
 
79
100
  # Startup
80
- startup = build_json_startup(
81
- {"api_key_secret": "sk-123", "timeout_s": 30},
82
- {"config_path": "config.yml"},
83
- {"RUST_LOG": "info"},
101
+ startup = build_json(
102
+ "log",
103
+ {
104
+ "event": "startup",
105
+ "config": {"api_key_secret": "sk-123", "timeout_s": 30},
106
+ "args": {"config_path": "config.yml"},
107
+ "env": {"RUST_LOG": "info"},
108
+ },
109
+ trace=None,
84
110
  )
85
111
 
86
112
  # Success (always include trace)
@@ -161,6 +187,41 @@ assert parse_size("1.5K") == 1536
161
187
  assert parse_size("512") == 512
162
188
  ```
163
189
 
190
+ ### CLI Helpers (for tools built on AFDATA)
191
+
192
+ Shared helpers that prevent flag-parsing drift between CLI tools. Use these instead of reimplementing `--output` and `--log` handling in each tool.
193
+
194
+ ```python
195
+ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
196
+
197
+ cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
198
+ cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
199
+ cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
200
+ build_cli_error(message: str) -> dict # {code:"error", error_code:"invalid_request", retryable:False, trace:{duration_ms:0}}
201
+ ```
202
+
203
+ **Canonical pattern** — parse all flags before doing work, emit JSONL errors to stdout:
204
+
205
+ ```python
206
+ import sys
207
+ from agent_first_data import (
208
+ OutputFormat, cli_parse_output, cli_parse_log_filters,
209
+ cli_output, build_cli_error, output_json,
210
+ )
211
+
212
+ try:
213
+ fmt = cli_parse_output(args.output)
214
+ except ValueError as e:
215
+ print(output_json(build_cli_error(str(e))))
216
+ sys.exit(2)
217
+
218
+ log = cli_parse_log_filters(args.log.split(",") if args.log else [])
219
+ # ... do work ...
220
+ print(cli_output(result, fmt))
221
+ ```
222
+
223
+ See `examples/agent_cli.py` for the complete working example (`pytest examples/agent_cli.py`).
224
+
164
225
  ## Usage Examples
165
226
 
166
227
  ### Example 1: REST API
@@ -187,14 +248,20 @@ async def get_user(user_id: int):
187
248
  from agent_first_data import *
188
249
 
189
250
  # 1. Startup
190
- startup = build_json_startup(
191
- {"api_key_secret": "sk-sensitive-key", "timeout_s": 30},
192
- {"input_path": "data.json"},
193
- {"RUST_LOG": "info"},
251
+ startup = build_json(
252
+ "log",
253
+ {
254
+ "event": "startup",
255
+ "config": {"api_key_secret": "sk-sensitive-key", "timeout_s": 30},
256
+ "args": {"input_path": "data.json"},
257
+ "env": {"RUST_LOG": "info"},
258
+ },
259
+ trace=None,
194
260
  )
195
261
  print(output_yaml(startup))
196
262
  # ---
197
- # code: "startup"
263
+ # code: "log"
264
+ # event: "startup"
198
265
  # args:
199
266
  # input_path: "data.json"
200
267
  # config:
@@ -244,6 +311,7 @@ result = build_json_ok(
244
311
  )
245
312
 
246
313
  # Print JSONL to stdout (secrets redacted, one JSON object per line)
314
+ # Channel policy: machine-readable protocol/log events must not use stderr.
247
315
  print(output_json(result))
248
316
  # {"code":"ok","result":{"status":"success"},"trace":{"api_key_secret":"***","duration_ms":250}}
249
317
  ```
@@ -285,23 +353,23 @@ print(output_plain(data))
285
353
  # api_key=*** cache_ttl=3600s count=42 created_at=2025-02-07T00:00:00.000Z file_size=5.0MB payment=50000000msats price=$99.99 request_timeout=5.0s success_rate=95.5% user_name=alice
286
354
  ```
287
355
 
288
- ## AFD Logging
356
+ ## AFDATA Logging
289
357
 
290
- AFD-compliant structured logging via Python's `logging` module. Every log line is formatted using the library's own `output_json`/`output_plain`/`output_yaml` functions. Span fields are carried via `contextvars` (async-safe), automatically flattened into each log line.
358
+ AFDATA-compliant structured logging via Python's `logging` module. Every log line is formatted using the library's own `output_json`/`output_plain`/`output_yaml` functions. Span fields are carried via `contextvars` (async-safe), automatically flattened into each log line.
291
359
 
292
360
  ### API
293
361
 
294
362
  ```python
295
363
  from agent_first_data import init_logging_json, init_logging_plain, init_logging_yaml
296
- from agent_first_data.afd_logging import AfdHandler, get_logger, span
364
+ from agent_first_data.afdata_logging import AfdataHandler, get_logger, span
297
365
 
298
- # Convenience initializers — set up the root logger with AFD output to stdout
366
+ # Convenience initializers — set up the root logger with AFDATA output to stdout
299
367
  init_logging_json(level="INFO") # Single-line JSONL (secrets redacted, original keys)
300
368
  init_logging_plain(level="INFO") # Single-line logfmt (keys stripped, values formatted)
301
369
  init_logging_yaml(level="INFO") # Multi-line YAML (keys stripped, values formatted)
302
370
 
303
371
  # Low-level — create a handler for custom logger stacks
304
- AfdHandler(format="json") # format: "json" | "plain" | "yaml"
372
+ AfdataHandler(format="json") # format: "json" | "plain" | "yaml"
305
373
 
306
374
  # Logger with default fields (returns logging.LoggerAdapter)
307
375
  get_logger(name, **fields)
@@ -382,8 +450,8 @@ The `code` field defaults to the log level. Override with an explicit field:
382
450
  from agent_first_data import get_logger
383
451
 
384
452
  logger = get_logger("myapp")
385
- logger.info("Server ready", extra={"code": "startup"})
386
- # {"timestamp_epoch_ms":...,"message":"Server ready","target":"myapp","code":"startup"}
453
+ logger.info("Server ready", extra={"code": "log", "event": "startup"})
454
+ # {"timestamp_epoch_ms":...,"message":"Server ready","target":"myapp","code":"log","event":"startup"}
387
455
  ```
388
456
 
389
457
  ### Output Fields
@@ -401,7 +469,7 @@ Every log line contains:
401
469
 
402
470
  ### Log Output Formats
403
471
 
404
- All three formats use the library's own output functions, so AFD suffix processing applies to log fields too:
472
+ All three formats use the library's own output functions, so AFDATA suffix processing applies to log fields too:
405
473
 
406
474
  | Format | Function | Keys | Values | Use case |
407
475
  |:-------|:---------|:-----|:-------|:---------|
@@ -435,8 +503,8 @@ All formats automatically redact `_secret` fields.
435
503
 
436
504
  This package is part of the [agent-first-data](https://github.com/cmnspore/agent-first-data) repository, which also contains:
437
505
 
438
- - **`spec/`** — Full AFD specification with suffix definitions, protocol format rules, and cross-language test fixtures
439
- - **`skills/`** — Claude Code skill for AI agents working with AFD conventions
506
+ - **`spec/`** — Full AFDATA specification with suffix definitions, protocol format rules, and cross-language test fixtures
507
+ - **`skills/`** — AI coding agent skill for working with AFDATA conventions
440
508
 
441
509
  To run tests, clone the full repository (tests use shared cross-language fixtures from `spec/fixtures/`):
442
510
 
@@ -1,11 +1,14 @@
1
1
  README.md
2
2
  pyproject.toml
3
3
  agent_first_data/__init__.py
4
- agent_first_data/afd_logging.py
4
+ agent_first_data/afdata_logging.py
5
+ agent_first_data/cli.py
5
6
  agent_first_data/format.py
6
7
  agent_first_data.egg-info/PKG-INFO
7
8
  agent_first_data.egg-info/SOURCES.txt
8
9
  agent_first_data.egg-info/dependency_links.txt
9
10
  agent_first_data.egg-info/top_level.txt
10
- tests/test_afd_logging.py
11
- tests/test_format.py
11
+ tests/test_afdata_logging.py
12
+ tests/test_cli.py
13
+ tests/test_format.py
14
+ tests/test_no_stderr_policy.py
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.2.4"
4
- description = "Agent-First Data (AFD) — suffix-driven output formatting and protocol templates for AI agents"
3
+ version = "0.4.0"
4
+ description = "Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates for AI agents"
5
5
  license = "MIT"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.9"
@@ -1,4 +1,4 @@
1
- """Tests for AFD logging module."""
1
+ """Tests for AFDATA logging module."""
2
2
 
3
3
  import json
4
4
  import logging
@@ -6,7 +6,7 @@ import sys
6
6
  from io import StringIO
7
7
  from unittest.mock import patch
8
8
 
9
- from agent_first_data.afd_logging import AfdHandler, AfdJsonHandler, init_json, init_plain, init_yaml, span, get_logger
9
+ from agent_first_data.afdata_logging import AfdataHandler, AfdataJsonHandler, init_json, init_plain, init_yaml, span, get_logger
10
10
 
11
11
 
12
12
  def capture_log(fn):
@@ -20,9 +20,9 @@ def capture_log(fn):
20
20
 
21
21
 
22
22
  def make_logger(name="test"):
23
- """Create a fresh logger with AfdJsonHandler."""
23
+ """Create a fresh logger with AfdataJsonHandler."""
24
24
  logger = logging.getLogger(name)
25
- logger.handlers = [AfdJsonHandler()]
25
+ logger.handlers = [AfdataJsonHandler()]
26
26
  logger.setLevel(logging.DEBUG)
27
27
  return logger
28
28
 
@@ -108,15 +108,16 @@ class TestCodeOverride:
108
108
  logger = make_logger("test_code")
109
109
  adapter = get_logger("test_code")
110
110
 
111
- m = capture_log(lambda: adapter.info("ready", extra={"code": "startup"}))
112
- assert m["code"] == "startup"
111
+ m = capture_log(lambda: adapter.info("ready", extra={"code": "log", "event": "startup"}))
112
+ assert m["code"] == "log"
113
+ assert m["event"] == "startup"
113
114
 
114
115
 
115
116
  class TestGetLogger:
116
117
  def test_default_fields(self):
117
- # Ensure root logger has AfdJsonHandler
118
+ # Ensure root logger has AfdataJsonHandler
118
119
  root = logging.getLogger()
119
- root.handlers = [AfdJsonHandler()]
120
+ root.handlers = [AfdataJsonHandler()]
120
121
  root.setLevel(logging.DEBUG)
121
122
 
122
123
  adapter = get_logger("test_adapter", component="myservice")
@@ -137,7 +138,7 @@ def capture_raw(fn):
137
138
  class TestPlainFormat:
138
139
  def test_plain_output(self):
139
140
  logger = logging.getLogger("test_plain")
140
- logger.handlers = [AfdHandler(format="plain")]
141
+ logger.handlers = [AfdataHandler(format="plain")]
141
142
  logger.setLevel(logging.DEBUG)
142
143
 
143
144
  output = capture_raw(lambda: logger.info("hello"))
@@ -157,7 +158,7 @@ class TestPlainFormat:
157
158
  class TestYamlFormat:
158
159
  def test_yaml_output(self):
159
160
  logger = logging.getLogger("test_yaml")
160
- logger.handlers = [AfdHandler(format="yaml")]
161
+ logger.handlers = [AfdataHandler(format="yaml")]
161
162
  logger.setLevel(logging.DEBUG)
162
163
 
163
164
  output = capture_raw(lambda: logger.info("hello"))
@@ -0,0 +1,98 @@
1
+ """Tests for agent_first_data CLI helpers."""
2
+ import pytest
3
+ from agent_first_data import (
4
+ OutputFormat,
5
+ cli_parse_output,
6
+ cli_parse_log_filters,
7
+ cli_output,
8
+ build_cli_error,
9
+ output_json,
10
+ )
11
+
12
+
13
+ # ── cli_parse_output ──────────────────────────────────────────────────────────
14
+
15
+ def test_parse_output_all_formats():
16
+ assert cli_parse_output("json") is OutputFormat.JSON
17
+ assert cli_parse_output("yaml") is OutputFormat.YAML
18
+ assert cli_parse_output("plain") is OutputFormat.PLAIN
19
+
20
+
21
+ def test_parse_output_rejects_unknown():
22
+ with pytest.raises(ValueError):
23
+ cli_parse_output("xml")
24
+ with pytest.raises(ValueError):
25
+ cli_parse_output("JSON")
26
+ with pytest.raises(ValueError):
27
+ cli_parse_output("")
28
+
29
+
30
+ def test_parse_output_error_contains_value():
31
+ with pytest.raises(ValueError, match="toml"):
32
+ cli_parse_output("toml")
33
+ with pytest.raises(ValueError, match="json"):
34
+ cli_parse_output("toml")
35
+
36
+
37
+ # ── cli_parse_log_filters ─────────────────────────────────────────────────────
38
+
39
+ def test_parse_log_filters_trims_and_lowercases():
40
+ assert cli_parse_log_filters([" Query ", "ERROR"]) == ["query", "error"]
41
+
42
+
43
+ def test_parse_log_filters_deduplicates():
44
+ assert cli_parse_log_filters(["query", "error", "Query", "query"]) == ["query", "error"]
45
+
46
+
47
+ def test_parse_log_filters_removes_empty():
48
+ assert cli_parse_log_filters(["", "query", " "]) == ["query"]
49
+
50
+
51
+ def test_parse_log_filters_empty_list():
52
+ assert cli_parse_log_filters([]) == []
53
+
54
+
55
+ def test_parse_log_filters_preserves_order():
56
+ assert cli_parse_log_filters(["startup", "request", "retry"]) == ["startup", "request", "retry"]
57
+
58
+
59
+ # ── build_cli_error ───────────────────────────────────────────────────────────
60
+
61
+ def test_build_cli_error_required_fields():
62
+ v = build_cli_error("missing --sql")
63
+ assert v["code"] == "error"
64
+ assert v["error_code"] == "invalid_request"
65
+ assert v["error"] == "missing --sql"
66
+ assert v["retryable"] is False
67
+ assert v["trace"]["duration_ms"] == 0
68
+
69
+
70
+ def test_build_cli_error_is_valid_json():
71
+ import json
72
+ v = build_cli_error("oops")
73
+ s = output_json(v)
74
+ parsed = json.loads(s)
75
+ assert parsed["code"] == "error"
76
+
77
+
78
+ # ── cli_output ────────────────────────────────────────────────────────────────
79
+
80
+ def test_cli_output_dispatches_json():
81
+ v = {"code": "ok", "size_bytes": 1024}
82
+ out = cli_output(v, OutputFormat.JSON)
83
+ assert "size_bytes" in out # json: raw keys, no suffix processing
84
+ assert "\n" not in out
85
+
86
+
87
+ def test_cli_output_dispatches_yaml():
88
+ v = {"code": "ok", "size_bytes": 1024}
89
+ out = cli_output(v, OutputFormat.YAML)
90
+ assert out.startswith("---")
91
+ assert "size:" in out # yaml: suffix stripped
92
+
93
+
94
+ def test_cli_output_dispatches_plain():
95
+ v = {"code": "ok"}
96
+ out = cli_output(v, OutputFormat.PLAIN)
97
+ assert "\n" not in out
98
+ assert "code=ok" in out
@@ -1,10 +1,9 @@
1
- """Tests for AFD output formatting — driven by shared spec/fixtures."""
1
+ """Tests for AFDATA output formatting — driven by shared spec/fixtures."""
2
2
 
3
3
  import json
4
4
  import os
5
5
 
6
6
  from agent_first_data import (
7
- build_json_startup,
8
7
  build_json_ok,
9
8
  build_json_error,
10
9
  build_json,
@@ -52,8 +51,6 @@ def test_protocol_fixtures():
52
51
  result = build_json_error(args["message"])
53
52
  elif typ == "error_trace":
54
53
  result = build_json_error(args["message"], args["trace"])
55
- elif typ == "startup":
56
- result = build_json_startup(args["config"], args["args"], args["env"])
57
54
  elif typ == "status":
58
55
  result = build_json(args["code"], args.get("fields"))
59
56
  else:
@@ -0,0 +1,23 @@
1
+ """Policy test: runtime library sources must not emit protocol/log events to stderr."""
2
+
3
+ from pathlib import Path
4
+ import re
5
+
6
+
7
+ DISALLOWED = re.compile(
8
+ r"\bsys\.stderr\b|\bfile\s*=\s*sys\.stderr\b|\bstderr\.write\s*\(",
9
+ )
10
+
11
+
12
+ def test_no_stderr_usage_in_runtime_sources() -> None:
13
+ root = Path(__file__).resolve().parents[1] / "agent_first_data"
14
+ files = sorted(root.glob("*.py"))
15
+ assert files, "no python source files found"
16
+
17
+ violations: list[str] = []
18
+ for path in files:
19
+ for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
20
+ if DISALLOWED.search(line):
21
+ violations.append(f"{path.name}:{lineno}: {line.strip()}")
22
+
23
+ assert not violations, "stderr usage is disallowed:\n" + "\n".join(violations)