agent-first-data 0.16.0__tar.gz → 0.16.1__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.
Files changed (24) hide show
  1. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/PKG-INFO +1 -1
  2. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/assets/protocol-v1.schema.json +2 -19
  3. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/assets/skills/agent-first-data/references/rules.md +3 -3
  4. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/cli.py +4 -10
  5. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/format.py +18 -139
  6. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data.egg-info/PKG-INFO +1 -1
  7. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/pyproject.toml +1 -1
  8. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/tests/test_cli.py +10 -3
  9. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/tests/test_format.py +9 -12
  10. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/README.md +0 -0
  11. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/__init__.py +0 -0
  12. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/assets/registry.json +0 -0
  13. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/assets/skills/agent-first-data/SKILL.md +0 -0
  14. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/assets/skills/agent-first-data/references/protocol-v1.schema.json +0 -0
  15. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/assets/skills/agent-first-data/references/registry.json +0 -0
  16. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/skill.py +0 -0
  17. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data/stream_redirect.py +0 -0
  18. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data.egg-info/SOURCES.txt +0 -0
  19. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data.egg-info/dependency_links.txt +0 -0
  20. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/agent_first_data.egg-info/top_level.txt +0 -0
  21. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/setup.cfg +0 -0
  22. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/tests/test_no_stderr_policy.py +0 -0
  23. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/tests/test_skill.py +0 -0
  24. {agent_first_data-0.16.0 → agent_first_data-0.16.1}/tests/test_stream_redirect.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.16.0
3
+ Version: 0.16.1
4
4
  Summary: A naming convention that lets AI agents understand your data without being told what it means.
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
@@ -47,13 +47,7 @@
47
47
  "type": "object",
48
48
  "properties": {
49
49
  "kind": {"const": "progress"},
50
- "progress": {
51
- "type": "object",
52
- "properties": {
53
- "message": {"type": "string", "minLength": 1}
54
- },
55
- "additionalProperties": true
56
- },
50
+ "progress": true,
57
51
  "trace": {"$ref": "#/$defs/trace"}
58
52
  },
59
53
  "required": ["kind", "progress"],
@@ -63,18 +57,7 @@
63
57
  "type": "object",
64
58
  "properties": {
65
59
  "kind": {"const": "log"},
66
- "log": {
67
- "type": "object",
68
- "properties": {
69
- "message": {"type": "string", "minLength": 1},
70
- "level": {"enum": ["debug", "info", "warn", "error"]},
71
- "code": false
72
- },
73
- "not": {
74
- "required": ["code"]
75
- },
76
- "additionalProperties": true
77
- },
60
+ "log": true,
78
61
  "trace": {"$ref": "#/$defs/trace"}
79
62
  },
80
63
  "required": ["kind", "log"],
@@ -258,7 +258,7 @@ Startup payload fields are tool-defined; `config` is recommended, while `version
258
258
 
259
259
  All use `kind` plus the same-named payload and optional `trace`. Do not split protocol events across `stdout` and `stderr`.
260
260
 
261
- Base validators enforce mandatory envelope/error/lifecycle rules. Strict validators additionally require object-valued `trace` on every event, complete log fields (`message`, `level`; `code` field MUST NOT appear in log payload), `retryable` on every error, and a non-empty progress `message`. Use strict validation before claiming compliance with all recommendations.
261
+ Base validators enforce mandatory envelope/error/lifecycle rules. Strict validators additionally require object-valued `trace` on every event and `retryable` on every error. Log and progress payloads remain tool-defined in both profiles. Use strict validation before claiming compliance with all recommendations.
262
262
 
263
263
  ---
264
264
 
@@ -298,11 +298,11 @@ let span = info_span!("request", request_id = %uuid);
298
298
  let _guard = span.enter();
299
299
  ```
300
300
 
301
- For Go, Python, and TypeScript: emit span fields explicitly in each log event via `.field()` or equivalent event construction.
301
+ For Go, Python, and TypeScript: emit span fields explicitly in each tool-defined log payload.
302
302
 
303
303
  ### Output fields
304
304
 
305
- Every log line contains `message`, `level` (debug/info/warn/error), plus span fields and event fields. Log payloads MUST NOT contain a `code` field. Do not use the log level as `code`; `code:"error"` is reserved for terminal protocol errors. Projects that need timestamps add them as extension fields (`timestamp_epoch_ms`) via emitter defaults or per-call `.field()`.
305
+ Log payloads have no required or reserved fields. Traditional logging adapters commonly add `message` and `level` (debug/info/warn/error), plus span and event fields, but those names and meanings are adapter conventions rather than AFDATA protocol requirements. Projects that need timestamps add `timestamp_epoch_ms` explicitly.
306
306
 
307
307
  Log redaction is **by field name** (the same `_secret`/`_url` rule as all output), applied when the line is emitted. Name the secret field — `info!(api_key_secret = %key)` — rather than logging a whole object by its `Debug`/string rendering, which hides the inner field names from redaction. For structured/nested secret-bearing data, build a value, apply redaction through output formatters or redacted_value(), then emit via the standard formatting helpers — do not pass the struct to a `?`/`%`-rendered log field.
308
308
 
@@ -168,14 +168,8 @@ class CliEmitter:
168
168
  # Apply log fields provider if this is a log event
169
169
  if kind == "log" and self._log_fields_provider is not None:
170
170
  provider_fields = self._log_fields_provider()
171
- if provider_fields:
172
- # Check for reserved fields from provider
173
- log_payload = envelope.get("log", {})
174
- for reserved in ("message", "level", "code"):
175
- if reserved in provider_fields:
176
- raise RuntimeError(
177
- f"log fields provider cannot write reserved field '{reserved}'"
178
- )
171
+ log_payload = envelope.get("log")
172
+ if provider_fields and isinstance(log_payload, dict):
179
173
  # Merge provider fields, explicit fields take precedence
180
174
  merged_log = dict(provider_fields)
181
175
  merged_log.update(log_payload)
@@ -211,7 +205,7 @@ class CliEmitter:
211
205
  if not message or not isinstance(message, str):
212
206
  raise ValueError("message must be a non-empty string")
213
207
  # Use the builder to get default trace: {}
214
- event = json_progress(message).build()
208
+ event = json_progress({"message": message}).build()
215
209
  self.emit(event)
216
210
 
217
211
  def emit_log(self, level: LogLevel | str, message: str) -> None:
@@ -220,7 +214,7 @@ class CliEmitter:
220
214
  level = LogLevel(level)
221
215
  if not message or not isinstance(message, str):
222
216
  raise ValueError("message must be a non-empty string")
223
- event = json_log(level, message).build()
217
+ event = json_log({"level": level.value, "message": message}).build()
224
218
  self.emit(event)
225
219
 
226
220
 
@@ -183,50 +183,11 @@ class ErrorBuilder:
183
183
  class ProgressBuilder:
184
184
  """Fluent builder for progress events."""
185
185
 
186
- def __init__(self, message: str) -> None:
187
- self._message = message
186
+ def __init__(self, payload: Any) -> None:
187
+ self._payload = payload
188
188
  self._trace: dict = {}
189
- self._fields: dict[str, Any] = {}
190
189
  self._errors: list[str] = []
191
190
 
192
- def field(self, name: str, value: Any) -> ProgressBuilder:
193
- """Add a single extension field."""
194
- if name == "message":
195
- self._errors.append(f"cannot override reserved field '{name}'")
196
- else:
197
- self._fields[name] = value
198
- return self
199
-
200
- def fields(self, mapping: Mapping[str, Any]) -> ProgressBuilder:
201
- """Add multiple extension fields from a mapping."""
202
- if not isinstance(mapping, Mapping):
203
- self._errors.append("fields must be a mapping")
204
- else:
205
- for k, v in mapping.items():
206
- if k == "message":
207
- self._errors.append(f"cannot override reserved field '{k}'")
208
- else:
209
- self._fields[k] = v
210
- return self
211
-
212
- def extend(self, value: Any) -> ProgressBuilder:
213
- """Add extension fields from a dataclass or mapping."""
214
- if hasattr(value, "__dataclass_fields__"):
215
- from dataclasses import asdict
216
- mapping = asdict(value)
217
- elif isinstance(value, Mapping):
218
- mapping = value
219
- else:
220
- self._errors.append("extend() requires a dataclass or mapping")
221
- return self
222
-
223
- for k, v in mapping.items():
224
- if k == "message":
225
- self._errors.append(f"cannot override reserved field '{k}'")
226
- else:
227
- self._fields[k] = v
228
- return self
229
-
230
191
  def trace(self, obj: Any) -> ProgressBuilder:
231
192
  """Set trace context."""
232
193
  if not isinstance(obj, dict):
@@ -239,11 +200,9 @@ class ProgressBuilder:
239
200
  """Build and return the Event, raising EventBuildError if there are errors."""
240
201
  if self._errors:
241
202
  raise EventBuildError("; ".join(self._errors))
242
- progress = {"message": self._message}
243
- progress.update(self._fields)
244
203
  envelope = {
245
204
  "kind": "progress",
246
- "progress": progress,
205
+ "progress": self._payload,
247
206
  "trace": self._trace,
248
207
  }
249
208
  return Event(envelope)
@@ -252,53 +211,11 @@ class ProgressBuilder:
252
211
  class LogBuilder:
253
212
  """Fluent builder for log events."""
254
213
 
255
- def __init__(self, level: LogLevel | str, message: str) -> None:
256
- if isinstance(level, str):
257
- level = LogLevel(level)
258
- self._level = level
259
- self._message = message
214
+ def __init__(self, payload: Any) -> None:
215
+ self._payload = payload
260
216
  self._trace: dict = {}
261
- self._fields: dict[str, Any] = {}
262
217
  self._errors: list[str] = []
263
218
 
264
- def field(self, name: str, value: Any) -> LogBuilder:
265
- """Add a single extension field."""
266
- if name in ("message", "level", "code"):
267
- self._errors.append(f"cannot override reserved field '{name}'")
268
- else:
269
- self._fields[name] = value
270
- return self
271
-
272
- def fields(self, mapping: Mapping[str, Any]) -> LogBuilder:
273
- """Add multiple extension fields from a mapping."""
274
- if not isinstance(mapping, Mapping):
275
- self._errors.append("fields must be a mapping")
276
- else:
277
- for k, v in mapping.items():
278
- if k in ("message", "level", "code"):
279
- self._errors.append(f"cannot override reserved field '{k}'")
280
- else:
281
- self._fields[k] = v
282
- return self
283
-
284
- def extend(self, value: Any) -> LogBuilder:
285
- """Add extension fields from a dataclass or mapping."""
286
- if hasattr(value, "__dataclass_fields__"):
287
- from dataclasses import asdict
288
- mapping = asdict(value)
289
- elif isinstance(value, Mapping):
290
- mapping = value
291
- else:
292
- self._errors.append("extend() requires a dataclass or mapping")
293
- return self
294
-
295
- for k, v in mapping.items():
296
- if k in ("message", "level", "code"):
297
- self._errors.append(f"cannot override reserved field '{k}'")
298
- else:
299
- self._fields[k] = v
300
- return self
301
-
302
219
  def trace(self, obj: Any) -> LogBuilder:
303
220
  """Set trace context."""
304
221
  if not isinstance(obj, dict):
@@ -311,11 +228,9 @@ class LogBuilder:
311
228
  """Build and return the Event, raising EventBuildError if there are errors."""
312
229
  if self._errors:
313
230
  raise EventBuildError("; ".join(self._errors))
314
- log = {"level": self._level.value, "message": self._message}
315
- log.update(self._fields)
316
231
  envelope = {
317
232
  "kind": "log",
318
- "log": log,
233
+ "log": self._payload,
319
234
  "trace": self._trace,
320
235
  }
321
236
  return Event(envelope)
@@ -331,23 +246,22 @@ def json_error(code: str, message: str) -> ErrorBuilder:
331
246
  return ErrorBuilder(code, message)
332
247
 
333
248
 
334
- def json_progress(message: str) -> ProgressBuilder:
249
+ def json_progress(payload: Any) -> ProgressBuilder:
335
250
  """Create a fluent progress builder."""
336
- return ProgressBuilder(message)
251
+ return ProgressBuilder(payload)
337
252
 
338
253
 
339
- def json_log(level: LogLevel | str, message: str) -> LogBuilder:
254
+ def json_log(payload: Any) -> LogBuilder:
340
255
  """Create a fluent log builder."""
341
- return LogBuilder(level, message)
256
+ return LogBuilder(payload)
342
257
 
343
258
 
344
259
  def validate_protocol_event(event: Any, *, strict: bool = True) -> None:
345
260
  """Validate one protocol v1 event envelope.
346
261
 
347
262
  With ``strict=True`` (the default), also enforces the recommended strict
348
- protocol profile (required trace, required error.retryable, log/progress
349
- message and level rules). Pass ``strict=False`` for the plain, lenient
350
- envelope-shape check only.
263
+ protocol profile (required trace and required error.retryable). Pass
264
+ ``strict=False`` for the plain, lenient envelope-shape check only.
351
265
  """
352
266
  if not isinstance(event, dict):
353
267
  raise ValueError("event must be a JSON object")
@@ -370,10 +284,6 @@ def validate_protocol_event(event: Any, *, strict: bool = True) -> None:
370
284
  raise ValueError("event.trace is required by the strict profile")
371
285
  if kind == "error":
372
286
  _validate_strict_error_payload(event["error"])
373
- elif kind == "log":
374
- _validate_strict_log_payload(event["log"])
375
- elif kind == "progress":
376
- _validate_strict_progress_payload(event["progress"])
377
287
 
378
288
 
379
289
  def _validate_error_payload(error: Any) -> None:
@@ -426,26 +336,6 @@ def _validate_strict_error_payload(error: Any) -> None:
426
336
  raise ValueError("event.error.retryable must be a boolean in the strict profile")
427
337
 
428
338
 
429
- def _validate_strict_log_payload(log: Any) -> None:
430
- if not isinstance(log, dict):
431
- raise ValueError("event.log must be a JSON object in the strict profile")
432
- if "code" in log:
433
- raise ValueError("event.log must not contain 'code' field in the strict profile")
434
- _require_non_empty_string(log, "message", "event.log")
435
- if "level" not in log:
436
- raise ValueError("event.log.level is required by the strict profile")
437
- if log.get("level") not in ("debug", "info", "warn", "error"):
438
- raise ValueError(
439
- "event.log.level must be one of debug, info, warn, error in the strict profile"
440
- )
441
-
442
-
443
- def _validate_strict_progress_payload(progress: Any) -> None:
444
- if not isinstance(progress, dict):
445
- raise ValueError("event.progress must be a JSON object in the strict profile")
446
- _require_non_empty_string(progress, "message", "event.progress")
447
-
448
-
449
339
  def _require_non_empty_string(payload: dict, field: str, path: str) -> None:
450
340
  value = payload.get(field)
451
341
  if not isinstance(value, str) or value == "":
@@ -487,22 +377,17 @@ class DecodedError:
487
377
 
488
378
  @dataclass(frozen=True)
489
379
  class DecodedProgress:
490
- """Decoded protocol v1 progress event. ``fields`` holds extension fields
491
- beyond message."""
380
+ """Decoded protocol v1 progress event."""
492
381
 
493
- message: str
494
- fields: dict[str, Any] = field(default_factory=dict)
382
+ progress: Any
495
383
  trace: dict | None = None
496
384
 
497
385
 
498
386
  @dataclass(frozen=True)
499
387
  class DecodedLog:
500
- """Decoded protocol v1 log event. ``fields`` holds extension fields
501
- beyond level/message."""
388
+ """Decoded protocol v1 log event."""
502
389
 
503
- level: LogLevel
504
- message: str
505
- fields: dict[str, Any] = field(default_factory=dict)
390
+ log: Any
506
391
  trace: dict | None = None
507
392
 
508
393
 
@@ -544,16 +429,10 @@ def decode_protocol_event(
544
429
  )
545
430
 
546
431
  if kind == "progress":
547
- progress = event["progress"]
548
- fields = {k: v for k, v in progress.items() if k != "message"}
549
- return DecodedProgress(message=progress["message"], fields=fields, trace=trace)
432
+ return DecodedProgress(progress=event["progress"], trace=trace)
550
433
 
551
434
  # kind == "log"
552
- log = event["log"]
553
- fields = {k: v for k, v in log.items() if k not in ("level", "message")}
554
- return DecodedLog(
555
- level=LogLevel(log["level"]), message=log["message"], fields=fields, trace=trace
556
- )
435
+ return DecodedLog(log=event["log"], trace=trace)
557
436
 
558
437
 
559
438
  # ═══════════════════════════════════════════
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.16.0
3
+ Version: 0.16.1
4
4
  Summary: A naming convention that lets AI agents understand your data without being told what it means.
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.16.0"
3
+ version = "0.16.1"
4
4
  description = "A naming convention that lets AI agents understand your data without being told what it means."
5
5
  license = "MIT"
6
6
  readme = "README.md"
@@ -140,7 +140,7 @@ def test_cli_output_dispatches_raw_yaml_with_options():
140
140
  def test_cli_emitter_writes_events_and_tracks_terminal():
141
141
  writer = StringIO()
142
142
  emitter = CliEmitter(writer, OutputFormat.JSON)
143
- emitter.emit(json_log(LogLevel.INFO, "startup").build())
143
+ emitter.emit(json_log({"level": "info", "message": "startup"}).build())
144
144
  emitter.emit(json_result({"rows": 2}).build())
145
145
  lines = writer.getvalue().splitlines()
146
146
  assert len(lines) == 2
@@ -150,7 +150,7 @@ def test_cli_emitter_writes_events_and_tracks_terminal():
150
150
 
151
151
  def test_cli_emitter_framing_all_formats():
152
152
  events = [
153
- json_log(LogLevel.INFO, "startup").build(),
153
+ json_log({"level": "info", "message": "startup"}).build(),
154
154
  json_result({"rows": 2}).build(),
155
155
  ]
156
156
  for fmt in (OutputFormat.JSON, OutputFormat.PLAIN, OutputFormat.YAML):
@@ -238,13 +238,20 @@ def test_cli_emitter_convenience_methods():
238
238
  def test_cli_emitter_with_log_fields_provider():
239
239
  writer = StringIO()
240
240
  def log_fields():
241
- return {"source": "test"}
241
+ return {
242
+ "source": "test",
243
+ "code": "cache_miss",
244
+ "message": "provider default",
245
+ "level": "debug",
246
+ }
242
247
  emitter = CliEmitter(writer, OutputFormat.JSON, log_fields=log_fields)
243
248
  emitter.emit_log(LogLevel.INFO, "test message")
244
249
  lines = writer.getvalue().splitlines()
245
250
  parsed = json.loads(lines[0])
246
251
  assert parsed["log"]["source"] == "test"
252
+ assert parsed["log"]["code"] == "cache_miss"
247
253
  assert parsed["log"]["message"] == "test message"
254
+ assert parsed["log"]["level"] == "info"
248
255
 
249
256
 
250
257
  # ── version helpers ───────────────────────────────────────────────────────────
@@ -107,8 +107,8 @@ def _build_protocol_case_event(typ, args):
107
107
 
108
108
  args vocabulary: "result" (payload), "code"+"message" (error),
109
109
  "hint" (-> .hint()), "retryable" (bool -> .retryable_if()),
110
- "fields" (object -> .fields()), "trace" (object -> .trace()),
111
- "message" alone (progress), "level"+"message" (log).
110
+ "fields" (object -> .fields()), "trace" (object -> .trace()), and complete
111
+ progress/log payloads.
112
112
  """
113
113
  kind = typ.split("_", 1)[0]
114
114
 
@@ -121,13 +121,13 @@ def _build_protocol_case_event(typ, args):
121
121
  if "retryable" in args:
122
122
  builder = builder.retryable_if(args["retryable"])
123
123
  elif kind == "progress":
124
- builder = json_progress(args["message"])
124
+ builder = json_progress({"message": args["message"], **args.get("fields", {})})
125
125
  elif kind == "log":
126
- builder = json_log(LogLevel(args["level"]), args["message"])
126
+ builder = json_log({"level": args["level"], "message": args["message"], **args.get("fields", {})})
127
127
  else:
128
128
  raise ValueError(f"unknown fixture type: {typ}")
129
129
 
130
- if "fields" in args:
130
+ if "fields" in args and kind not in ("progress", "log"):
131
131
  builder = builder.fields(args["fields"])
132
132
  if "trace" in args:
133
133
  builder = builder.trace(args["trace"])
@@ -388,21 +388,18 @@ def test_decode_protocol_event_error():
388
388
 
389
389
 
390
390
  def test_decode_protocol_event_progress():
391
- event = json_progress("halfway").field("percent", 50).build()
391
+ event = json_progress({"message": "halfway", "percent": 50}).build()
392
392
  decoded = decode_protocol_event(output_json(event.to_dict()))
393
393
  assert isinstance(decoded, DecodedProgress)
394
- assert decoded.message == "halfway"
395
- assert decoded.fields == {"percent": 50}
394
+ assert decoded.progress == {"message": "halfway", "percent": 50}
396
395
  assert decoded.trace == {}
397
396
 
398
397
 
399
398
  def test_decode_protocol_event_log():
400
- event = json_log(LogLevel.WARN, "slow query").field("duration_ms", 900).build()
399
+ event = json_log({"level": "warn", "message": "slow query", "duration_ms": 900}).build()
401
400
  decoded = decode_protocol_event(output_json(event.to_dict()))
402
401
  assert isinstance(decoded, DecodedLog)
403
- assert decoded.level == LogLevel.WARN
404
- assert decoded.message == "slow query"
405
- assert decoded.fields == {"duration_ms": 900}
402
+ assert decoded.log == {"level": "warn", "message": "slow query", "duration_ms": 900}
406
403
  assert decoded.trace == {}
407
404
 
408
405