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.
- {agent_first_data-0.2.4/agent_first_data.egg-info → agent_first_data-0.4.0}/PKG-INFO +94 -35
- agent_first_data-0.2.4/PKG-INFO → agent_first_data-0.4.0/README.md +92 -42
- {agent_first_data-0.2.4 → agent_first_data-0.4.0}/agent_first_data/__init__.py +19 -8
- agent_first_data-0.2.4/agent_first_data/afd_logging.py → agent_first_data-0.4.0/agent_first_data/afdata_logging.py +15 -15
- agent_first_data-0.4.0/agent_first_data/cli.py +92 -0
- {agent_first_data-0.2.4 → agent_first_data-0.4.0}/agent_first_data/format.py +2 -7
- agent_first_data-0.2.4/README.md → agent_first_data-0.4.0/agent_first_data.egg-info/PKG-INFO +101 -33
- {agent_first_data-0.2.4 → agent_first_data-0.4.0}/agent_first_data.egg-info/SOURCES.txt +6 -3
- {agent_first_data-0.2.4 → agent_first_data-0.4.0}/pyproject.toml +2 -2
- agent_first_data-0.2.4/tests/test_afd_logging.py → agent_first_data-0.4.0/tests/test_afdata_logging.py +11 -10
- agent_first_data-0.4.0/tests/test_cli.py +98 -0
- {agent_first_data-0.2.4 → agent_first_data-0.4.0}/tests/test_format.py +1 -4
- agent_first_data-0.4.0/tests/test_no_stderr_policy.py +23 -0
- {agent_first_data-0.2.4 → agent_first_data-0.4.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
- {agent_first_data-0.2.4 → agent_first_data-0.4.0}/agent_first_data.egg-info/top_level.txt +0 -0
- {agent_first_data-0.2.4 → agent_first_data-0.4.0}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-first-data
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: 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
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 (
|
|
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
|
-
|
|
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 =
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
|
|
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: "
|
|
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: **
|
|
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
|
|
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 =
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
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 =
|
|
200
|
-
|
|
201
|
-
{
|
|
202
|
-
|
|
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: "
|
|
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
|
-
##
|
|
356
|
+
## AFDATA Logging
|
|
298
357
|
|
|
299
|
-
|
|
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.
|
|
364
|
+
from agent_first_data.afdata_logging import AfdataHandler, get_logger, span
|
|
306
365
|
|
|
307
|
-
# Convenience initializers — set up the root logger with
|
|
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
|
-
|
|
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
|
|
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
|
|
448
|
-
- **`skills/`** —
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
|
|
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: "
|
|
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: **
|
|
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
|
|
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 =
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
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 =
|
|
200
|
-
|
|
201
|
-
{
|
|
202
|
-
|
|
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: "
|
|
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
|
-
##
|
|
347
|
+
## AFDATA Logging
|
|
298
348
|
|
|
299
|
-
|
|
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.
|
|
355
|
+
from agent_first_data.afdata_logging import AfdataHandler, get_logger, span
|
|
306
356
|
|
|
307
|
-
# Convenience initializers — set up the root logger with
|
|
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
|
-
|
|
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
|
|
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
|
|
448
|
-
- **`skills/`** —
|
|
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 (
|
|
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.
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
"
|
|
36
|
-
"
|
|
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
|
-
"""
|
|
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.
|
|
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("
|
|
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
|
|
42
|
-
"""Logging handler that outputs
|
|
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, "
|
|
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
|
-
|
|
91
|
+
AfdataJsonHandler = AfdataHandler
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
class
|
|
95
|
-
"""Logger adapter that passes extra fields to
|
|
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"] = {"
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
1
|
+
"""AFDATA output formatting and protocol templates.
|
|
2
2
|
|
|
3
|
-
|
|
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}
|
agent_first_data-0.2.4/README.md → agent_first_data-0.4.0/agent_first_data.egg-info/PKG-INFO
RENAMED
|
@@ -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 (
|
|
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
|
-
|
|
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 =
|
|
28
|
-
|
|
29
|
-
{
|
|
30
|
-
|
|
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: "
|
|
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: **
|
|
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
|
|
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 =
|
|
81
|
-
|
|
82
|
-
{
|
|
83
|
-
|
|
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 =
|
|
191
|
-
|
|
192
|
-
{
|
|
193
|
-
|
|
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: "
|
|
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
|
-
##
|
|
356
|
+
## AFDATA Logging
|
|
289
357
|
|
|
290
|
-
|
|
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.
|
|
364
|
+
from agent_first_data.afdata_logging import AfdataHandler, get_logger, span
|
|
297
365
|
|
|
298
|
-
# Convenience initializers — set up the root logger with
|
|
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
|
-
|
|
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
|
|
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
|
|
439
|
-
- **`skills/`** —
|
|
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/
|
|
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/
|
|
11
|
-
tests/
|
|
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.
|
|
4
|
-
description = "Agent-First Data (
|
|
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
|
|
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.
|
|
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
|
|
23
|
+
"""Create a fresh logger with AfdataJsonHandler."""
|
|
24
24
|
logger = logging.getLogger(name)
|
|
25
|
-
logger.handlers = [
|
|
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"] == "
|
|
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
|
|
118
|
+
# Ensure root logger has AfdataJsonHandler
|
|
118
119
|
root = logging.getLogger()
|
|
119
|
-
root.handlers = [
|
|
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 = [
|
|
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 = [
|
|
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
|
|
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)
|
{agent_first_data-0.2.4 → agent_first_data-0.4.0}/agent_first_data.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|