@hue-run/sdk 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CLI.md +246 -52
  2. package/README.md +69 -14
  3. package/WORLDS.md +6 -3
  4. package/dist/ai-sdk.d.ts +23 -1
  5. package/dist/ai-sdk.js +60 -2
  6. package/dist/cli/import-formats/adapter.d.ts +80 -0
  7. package/dist/cli/import-formats/adapter.js +1 -0
  8. package/dist/cli/import-formats/agent-events.d.ts +5 -0
  9. package/dist/cli/import-formats/agent-events.js +16 -0
  10. package/dist/cli/import-formats/atif.d.ts +3 -0
  11. package/dist/cli/import-formats/atif.js +15 -0
  12. package/dist/cli/import-formats/braintrust.d.ts +2 -0
  13. package/dist/cli/import-formats/braintrust.js +222 -0
  14. package/dist/cli/import-formats/containers.d.ts +32 -0
  15. package/dist/cli/import-formats/containers.js +399 -0
  16. package/dist/cli/import-formats/datadog.d.ts +2 -0
  17. package/dist/cli/import-formats/datadog.js +264 -0
  18. package/dist/cli/import-formats/helicone.d.ts +2 -0
  19. package/dist/cli/import-formats/helicone.js +161 -0
  20. package/dist/cli/import-formats/identity.d.ts +112 -0
  21. package/dist/cli/import-formats/identity.js +594 -0
  22. package/dist/cli/import-formats/jaeger.d.ts +3 -0
  23. package/dist/cli/import-formats/jaeger.js +266 -0
  24. package/dist/cli/import-formats/langfuse.d.ts +7 -0
  25. package/dist/cli/import-formats/langfuse.js +510 -0
  26. package/dist/cli/import-formats/langsmith.d.ts +2 -0
  27. package/dist/cli/import-formats/langsmith.js +244 -0
  28. package/dist/cli/import-formats/messages.d.ts +28 -0
  29. package/dist/cli/import-formats/messages.js +301 -0
  30. package/dist/cli/import-formats/mlflow.d.ts +2 -0
  31. package/dist/cli/import-formats/mlflow.js +260 -0
  32. package/dist/cli/import-formats/openai-agents.d.ts +2 -0
  33. package/dist/cli/import-formats/openai-agents.js +221 -0
  34. package/dist/cli/import-formats/opik.d.ts +2 -0
  35. package/dist/cli/import-formats/opik.js +193 -0
  36. package/dist/cli/import-formats/otel-family.d.ts +5 -0
  37. package/dist/cli/import-formats/otel-family.js +17 -0
  38. package/dist/cli/import-formats/otel-spans.d.ts +94 -0
  39. package/dist/cli/import-formats/otel-spans.js +262 -0
  40. package/dist/cli/import-formats/otlp-protobuf.d.ts +35 -0
  41. package/dist/cli/import-formats/otlp-protobuf.js +486 -0
  42. package/dist/cli/import-formats/otlp.d.ts +4 -0
  43. package/dist/cli/import-formats/otlp.js +15 -0
  44. package/dist/cli/import-formats/phoenix.d.ts +3 -0
  45. package/dist/cli/import-formats/phoenix.js +256 -0
  46. package/dist/cli/import-formats/raindrop.d.ts +21 -0
  47. package/dist/cli/import-formats/raindrop.js +255 -0
  48. package/dist/cli/import-formats/reader.d.ts +56 -0
  49. package/dist/cli/import-formats/reader.js +364 -0
  50. package/dist/cli/import-formats/registry.d.ts +36 -0
  51. package/dist/cli/import-formats/registry.js +71 -0
  52. package/dist/cli/import-formats/row-platform-fields.d.ts +32 -0
  53. package/dist/cli/import-formats/row-platform-fields.js +76 -0
  54. package/dist/cli/import-formats/rows.d.ts +165 -0
  55. package/dist/cli/import-formats/rows.js +638 -0
  56. package/dist/cli/import-formats/transcripts.d.ts +25 -0
  57. package/dist/cli/import-formats/transcripts.js +498 -0
  58. package/dist/cli/import-formats/weave.d.ts +2 -0
  59. package/dist/cli/import-formats/weave.js +159 -0
  60. package/dist/cli/import-formats/zipkin.d.ts +2 -0
  61. package/dist/cli/import-formats/zipkin.js +219 -0
  62. package/dist/cli/import.d.ts +1 -1
  63. package/dist/cli/import.js +4 -2
  64. package/dist/cli/traces-import-convert.d.ts +65 -33
  65. package/dist/cli/traces-import-convert.js +211 -214
  66. package/dist/cli/traces-import-prepare.d.ts +26 -22
  67. package/dist/cli/traces-import-prepare.js +51 -122
  68. package/dist/cli/traces-import.d.ts +4 -4
  69. package/dist/cli/traces-import.js +63 -11
  70. package/dist/transport.d.ts +12 -0
  71. package/dist/transport.js +139 -24
  72. package/dist/version.d.ts +1 -1
  73. package/dist/version.js +1 -1
  74. package/package.json +2 -1
package/CLI.md CHANGED
@@ -42,7 +42,7 @@ project manifest are refused because managers can update ancestor locks; Python
42
42
  The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
43
43
  `capture_content=False`. For a supported application, setup installs the dependency and adds the
44
44
  managed import and middleware registration to the existing entrypoint; an unreferenced helper is
45
- not a completed integration. TypeScript uses `@hue-run/sdk@0.21.1`, `@opentelemetry/api@1.9.1` and
45
+ not a completed integration. TypeScript uses `@hue-run/sdk@0.23.0`, `@opentelemetry/api@1.9.1` and
46
46
  `@opentelemetry/context-async-hooks@2.11.0`; Python setup uses its separately tested package pin.
47
47
  Content capture requires an ordinary account-managed key and a later explicit application decision.
48
48
 
@@ -381,8 +381,8 @@ and Hue never receives or stores them. The command is part of the `hue` executab
381
381
  # LANGFUSE_PUBLIC_KEY=pk-lf-...
382
382
  # LANGFUSE_SECRET_KEY=sk-lf-...
383
383
  # LANGFUSE_BASE_URL=https://cloud.langfuse.com
384
- npx -y @hue-run/sdk@0.21.1 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
385
- npx -y @hue-run/sdk@0.21.1 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
384
+ npx -y @hue-run/sdk@0.23.0 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
385
+ npx -y @hue-run/sdk@0.23.0 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
386
386
  ```
387
387
 
388
388
  `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY` come from the environment or `--env-path`
@@ -433,10 +433,10 @@ instrumentation scope `@hue-run/sdk/import/langfuse`.
433
433
  **Environment and Worlds.** A span whose observation, or v1 trace, records an environment also
434
434
  carries it as recorded in `hue.import.source_environment`, which marks the trace as imported; one
435
435
  that records none carries neither attribute, and the resource never declares
436
- `deployment.environment.name`, since one import can mix environments. A trace adds to its user's
437
- World only when Langfuse recorded it in its `production` environment: Hue reads the root's
438
- `langfuse.environment` as the trace's deployment environment, and never takes an imported trace in
439
- which any span names another environment, such as Langfuse's `default`, as production history.
436
+ `deployment.environment.name`, since one import can mix environments. Ended traces with a resolved
437
+ user can contribute to that user's World with any environment value or without an environment
438
+ declaration. Traces linked to Hue runs, World instances, or run executions never contribute.
439
+ Hue's evaluation and simulation evidence and deliberately excluded traces also stay excluded.
440
440
 
441
441
  **Ids and resuming.** A Langfuse id that already is an OpenTelemetry id (32 lowercase hex digits for
442
442
  a trace, 16 for an observation, as Langfuse's OpenTelemetry SDKs write them) is kept; any other id,
@@ -503,11 +503,12 @@ stopped.
503
503
 
504
504
  ## Import a bundle of existing traces
505
505
 
506
- `hue traces import` brings historical traces into Hue from exports you already hold: OTLP/JSON
507
- trace exports (a `{"resourceSpans": […]}` document, a list of them, or one per line), complete
508
- Langfuse observation exports (the `data` or native `observations` envelope) and text ATIF v1.7 and
509
- v1.8 exports, embedded subagents included. It needs no project key. `prepare` runs on the machine
510
- that holds the exports and sends nothing; a Hue MCP connection then authorizes the delivery
506
+ `hue traces import` brings historical traces into Hue from exports you already hold: OpenTelemetry
507
+ exports (OTLP/JSON, binary OTLP and OpenTelemetry Collector files, Jaeger, Zipkin), the exports of
508
+ LLM observability tools (Langfuse, Braintrust, LangSmith, Arize Phoenix, MLflow, Opik, W&B Weave,
509
+ Datadog LLM Observability, Helicone, Raindrop events and spans), OpenAI Agents SDK traces, ATIF trajectories and
510
+ plain chat transcripts. It needs no project key. `prepare` runs on the machine that holds the
511
+ exports and sends nothing; a Hue MCP connection then authorizes the delivery
511
512
  (`prepare_trace_import`); `upload` delivers the bundle with that authorization, and
512
513
  `get_trace_import` reports what arrived.
513
514
 
@@ -518,39 +519,232 @@ npx -y @hue-run/sdk@latest traces import upload ./exports.hue-import --authoriza
518
519
 
519
520
  **Prepare.** `prepare <path>` reads a file, a directory tree or a ZIP archive (members are inflated
520
521
  in memory; encrypted members, unsafe paths and nested archives are listed, not read) and
521
- recognizes each file by its content, never by its name. Every trace becomes gzip OTLP/JSON batches
522
- of at most 3 MiB decoded and 900 KiB on the wire, written to `--out` (default `<path>.hue-import`,
523
- created with mode 0700) beside `manifest.json`, which declares each trace's id, span count, batches
524
- (by the SHA-256 of each gzip body), large values, history owner and origin. A text value over 1 MiB
525
- is stored as a value of its own and listed in `hue.blobs`, as the exporter lists the values it
526
- uploads. OTLP spans are kept as recorded; a trace or span id that is not an OpenTelemetry id (an
527
- exporter's UUID, for example) is mapped to one derived from it by SHA-256, with the original kept
528
- as `hue.import.original_trace_id` or `hue.import.original_span_id`. A Langfuse observation becomes
529
- one span keeping its whole record (`hue.import.source_record`) beside the `langfuse.*` and GenAI
530
- attributes Hue reads; an id that is no OpenTelemetry id is mapped as `hue import langfuse` maps it,
531
- so an export and a live import of the same trace agree. An ATIF export becomes one trace: an agent
532
- span for the run, a model span per agent step with the conversation so far, a tool span per call,
533
- each embedded subagent as its own agent span under the call that spawned it, and the export's
534
- `extra.user_id` as the trace's `user.id`; its spans carry `hue.import.format` `atif` under the
535
- instrumentation scope `hue.import.atif`, and the run's agent span is named `invoke_agent
536
- <agent.name>` from the export's own agent. ATIF records instants, not durations, so model and tool
537
- spans are points in time, and a tool's result is kept as text with no error status of the
538
- importer's making. The same trace in two files is one trace when the copies agree and a refusal
539
- when they differ; a trace with more than 100,000 spans, 512 batches or 10,000 values is refused and
540
- named, and an input of more than 25,000 traces, or whose manifest would exceed 32 MiB (4 MiB
541
- compressed), is refused whole. Nothing about where a file sat or what it was called reaches a span: a renamed, reordered
542
- or re-zipped copy of the bundle prepares to byte-identical batches and the same manifest digest.
543
-
544
- The report (`--json` prints it as one object) names `manifest_digest`, `totals` (traces, batches,
545
- spans, blobs, bytes) and `source`, which `prepare_trace_import` takes as given; the formats
546
- recognized and the traces each file held; the files it could not read, with the reason; the traces
547
- and spans it refused; the traces without a history owner (a root `user.id`, a Langfuse `userId`, an
548
- ATIF `extra.user_id`, or an explicit `hue.world.entity.namespace` and `hue.world.entity.id`), which
549
- import like any other trace but leave a gap in continuous history; the origin each source recorded
550
- (`production`, `evaluation`, `simulation` or `unknown`; evaluation and simulation traffic is
551
- imported and kept out of production history); and the traces whose vendor ids were mapped. An
552
- existing `--out` directory is accepted only while it holds nothing but an earlier preparation and
553
- no upload has started in it.
522
+ recognizes each file by its content, never by its name. A file may be gzip (every member) or zstd
523
+ (every frame, skippable frames passed over; zstd needs Node.js 22.15 or later) compressed, at most
524
+ 64 MiB decompressed, and hold one JSON document, a JSON array of records, JSON Lines or binary OTLP,
525
+ as UTF-8 or as UTF-16 after a byte order mark. Each line of JSON Lines, each record of a JSON array
526
+ that no format reads whole, and each message of a length-prefixed protobuf file is recognized on
527
+ its own. CSV and Parquet are not read: export JSON or JSON Lines.
528
+
529
+ Damage stays local. A gzip or zstd file that breaks off, or holds bytes after its last member or
530
+ frame, is read up to the break and the rest named in `files.notes`; one whose checksum fails is not
531
+ read. A line that is not UTF-8 is named and the other lines read. A bare `NaN`, `Infinity` or
532
+ `-Infinity` (Python's `json.dumps` writes them) is read as null and named in `files.notes`. A value
533
+ nested more than 512 levels deep is named and not read, and JSON text inside a record (metadata,
534
+ tool arguments) nested that deep is kept as the text it is. Past 1,000 records in a row from the
535
+ start of a JSON array that no format reads, the array is reported as unrecognized without reading
536
+ the rest.
537
+
538
+ Every trace becomes gzip OTLP/JSON batches of at most 3 MiB decoded and 900 KiB on the wire,
539
+ written to `--out` (default `<path>.hue-import`, created with mode 0700) beside `manifest.json`,
540
+ which declares each trace's id, span count, batches (by the SHA-256 of each gzip body), large
541
+ values, history owner and origin. A text value over 1 MiB is stored as a value of its own and
542
+ listed in `hue.blobs`, as the exporter lists the values it uploads. Nothing about where a file sat
543
+ or what it was called reaches a span: a renamed, reordered or re-zipped copy of the bundle prepares
544
+ to byte-identical batches and the same manifest digest.
545
+
546
+ **Formats.** Each value is read by the first format whose rule it matches, in the order
547
+ `hue traces --help` lists them; each rule asks for keys the other formats' records lack. The format
548
+ id is the key of the report's `formats`, and every span converted from a source record carries it
549
+ as `hue.import.format`; spans kept as recorded and spans of the documented Langfuse list carry
550
+ none. The last column is where the format finds the trace's end user (written as `user.id`) and
551
+ conversation (`gen_ai.conversation.id`); a tool span, tool row or tool call never names either.
552
+
553
+ | Format id | Source | Recognized by | User and conversation |
554
+ | --- | --- | --- | --- |
555
+ | `otlp_json` | OTLP/JSON trace export: one document, a list of them, or one per line | a `resourceSpans` (or `resource_spans`) list | the spans' own attributes, read as Hue reads them |
556
+ | `otlp_protobuf` | Binary OTLP: one protobuf `ExportTraceServiceRequest` (an OTLP/HTTP body, a `.pb` or `.binpb` file), or Collector file exporter output (`format: proto`, or `compression: zstd` around protobuf or JSON) | content that decodes as an OTLP trace request, or as messages each prefixed with its 4-byte big-endian length | the spans' own attributes |
557
+ | `jaeger_api_v3` | Jaeger API v3 answer and NDJSON stream | `{"result": {"resourceSpans": …}}` (or `result.spans`), the stream's `{"end": true}` and `{"error": {"httpCode": …}}` lines | the spans' own attributes |
558
+ | `jaeger` | Jaeger `/api/traces` answer, the UI's trace download, one trace, or a span with its process | a trace's `spans` holding a span with `spanID`, `operationName` and `startTime` | span tags, kept as attributes |
559
+ | `zipkin` | Zipkin v2 JSON: a list of spans, a list of such lists, one span per line, or one span | hex `traceId` and `id` with a field only a span records (`timestamp`, `duration`, `localEndpoint`, `kind`, `annotations`, `tags`), and none of `type`, `startTime`, `spanId`, `traceID`, `span_id` | span tags, kept as attributes |
560
+ | `phoenix_otlp` | Arize Phoenix `GET /v1/projects/{project}/spans/otlpv1`, or one such span per line | `trace_id`, `span_id` and `start_time_unix_nano` or `end_time_unix_nano`, without `context` or `root_span_id` | the spans' attributes, OpenInference `user.id` and `session.id` among them |
561
+ | `phoenix_spans` | Arize Phoenix `GET /v1/projects/{project}/spans` | `context.trace_id`, `context.span_id` and `start_time` or `span_kind` | the spans' attributes, OpenInference `user.id` and `session.id` among them |
562
+ | `datadog_llmobs` | Datadog LLM Observability export API answer, one span, or one per line | `"type": "span"` with `attributes.span_id` and `attributes.trace_id` | the `user_id`, `user.id` or `usr.id` tag, or `user_id` in `metadata`; `session_id` |
563
+ | `langfuse_observations` | The Langfuse observation list Hue documents | a `data` or `observations` list whose every record is an observation (`id`, `traceId`, `type`, `startTime`) without `projectId` | the observations' `userId` and `sessionId`, as `langfuse.user.id` and `langfuse.session.id` |
564
+ | `langfuse` | Everything Langfuse writes: `GET /api/public/traces/{id}`, v1 trace and observation pages, Observations API v2 pages, `GET /api/public/sessions/{id}`, UI downloads of the traces, observations and events tables, blob-storage files of the `traces`, `observations` and `observations_v2` tables | a trace (`id`, `timestamp`, `bookmarked`, `public`), an observation (`id`, `type`, `startTime`, `traceId`) or a blob-storage observation row (`start_time`, `trace_id`, `parent_observation_id`), alone or in `data`, `observations` or `traces` | the trace's `userId` and `sessionId`, else each observation's |
565
+ | `atif` | ATIF v1.7 and v1.8 exports, embedded subagents included | `schema_version` starting with `ATIF-` | `extra.user_id`; the run's `session_id` |
566
+ | `braintrust` | Braintrust span rows: a UI download, the fetch or query API's answer, `bt sync pull` JSON Lines or a cloud-storage export | `span_id` and `root_span_id`, alone or in `events` or `data` | `metadata` (`userId`, `user_id`; `session_id`, `thread_id`, `conversation_id`, …), Mastra's `resourceId`, an OpenAI Agents row's `group_id` |
567
+ | `langsmith_runs` | LangSmith runs: a `list_runs` dump, the runs query page or its v2 page, a run with its `child_runs`, bulk-export rows converted from Parquet to JSON Lines | `id`, a LangSmith `run_type` (`chain`, `llm`, `tool`, `embedding`, `retriever`, `prompt`, `parser`) and `start_time` or `dotted_order`, alone or in `runs` or `items` | run metadata (`userId`, `user_id`; `thread_id`, `session_id`, `conversation_id`, …) and the v2 `thread_id`; never the run's own `session_id` (the tracing project) or `ls_user_id` |
568
+ | `mlflow_traces` | MLflow `Trace.to_dict()` (`mlflow traces get`, `trace.to_json()`), a list or JSON Lines of them, `mlflow traces search --output json`, MLflow 2's form included | `info` with `trace_id` or `request_id`, and `data.spans`, alone or in `traces` | `mlflow.trace.user` and `mlflow.trace.session` metadata, else a span's `user.id` and `session.id`; never `mlflow.user` |
569
+ | `opik_traces` | The files `opik export … project …` writes, or spans alone (the spans API page or JSON Lines) | `trace` (with `id` and `start_time`) beside `spans`; or a span with `id`, `trace_id`, an Opik `type` (`general`, `tool`, `llm`, `guardrail`) and `start_time`, alone or in `content` | metadata (`userId`, `user_id`, …); the trace's `thread_id`, else its metadata's; never `created_by` |
570
+ | `weave_calls` | W&B Weave calls: the calls stream's JSON Lines, a list of calls, `{"calls": […]}` or `{"call": …}` | `id`, `trace_id`, `op_name` and `started_at` | `attributes` (`userId`, `user_id`, …); `thread_id`, else in `attributes`; never `wb_user_id` |
571
+ | `openai_agents` | OpenAI Agents SDK `Trace.export()` and `Span.export()` records, one per line or in a list, or the `{"data": […]}` body its exporter posts | `"object": "trace"` with `id`, or `"object": "trace.span"` with `trace_id` | the trace metadata's `userId` (or `user_id`); `group_id`, else a conversation field of the metadata |
572
+ | `raindrop` | Raindrop events: elements of a `POST /v1/events/track` payload, or a Query API `GET /v1/events` answer | `event` with `ai_data` (or with `event_id` and `user_id`), or `event_name` with `user_input` and `assistant_output`, alone or in `data` | `user_id`; `convo_id` |
573
+ | `raindrop_spans` | Raindrop's span rows: a Query API `GET /v1/traces?event_id=` answer, one row, or one per line | `trace_id`, `span_id`, `span_name`, `start_time_ns` and a Raindrop `span_type` (`INTERNAL`, `LLM_GENERATION`, `LLM_GENERATION_STREAM`, `TOOL_CALL`), alone or in `data` | the association attributes Raindrop's SDKs stamp on every span (`traceloop.association.properties.user_id` and `convo_id`, `ai.telemetry.metadata.raindrop.*`); the rows name no event, so `--user-key` names a user they lack |
574
+ | `helicone` | Helicone requests: `npx @helicone/export` JSON or JSON Lines (`--include-body` for the bodies), or the `query-clickhouse` API answer | `request_id` and `request_created_at` with `response_status`, `request_user_id` or `request_properties`, alone or in `data` | `request_user_id`, never `helicone_user` (the Helicone account); the `Helicone-Session-Id` property |
575
+ | `openai_chat` | An OpenAI Chat Completions request, or a line of a chat JSONL file (a fine-tuning or eval dataset) | a `messages` list holding a `{role}` message, without `object` and without anything only Anthropic sends | `safety_identifier`, else `user` |
576
+ | `openai_responses` | An OpenAI Responses `response` object, a request (with its `output` when it was logged with it), or a bare list of input and output items | `"object": "response"` with an `output` list; a value of request fields only, with an item list as `input`; or a list of Responses items | `safety_identifier`, else `user`; `conversation` |
577
+ | `anthropic_messages` | An Anthropic Messages request or response | a response (`"type": "message"`, `"role": "assistant"`, `content`, `model`), or a `messages` request carrying something only Anthropic sends (`system`, `anthropic_version`, `stop_sequences`, an `input_schema` tool, a `tool_use` or other Anthropic content block) and nothing only OpenAI sends | `metadata.user_id` |
578
+
579
+ **How each kind of format is converted.** OTLP/JSON, binary OTLP and Jaeger API v3 spans are kept
580
+ exactly as recorded, and the spans of one trace split between files of these formats are read as
581
+ one trace. A trace or span id that is not an OpenTelemetry id (an exporter's UUID, for example) is
582
+ mapped to one derived from it by SHA-256, with the original kept as `hue.import.original_trace_id`
583
+ or `hue.import.original_span_id`.
584
+
585
+ - Jaeger UI JSON, Zipkin v2 and Phoenix spans are written as the OTLP spans they were, as the
586
+ OpenTelemetry Collector converts Jaeger and Zipkin spans. Attributes keep their own names
587
+ (Phoenix's OpenInference attributes among them). The process or local endpoint is the resource.
588
+ The kind, status, scope, events and links come from the tags, references and annotations Jaeger
589
+ and Zipkin write them as, and the source span is kept whole as `hue.import.source_record`. A
590
+ 64-bit Jaeger or Zipkin trace id is zero-padded to 128 bits as the Collector pads it, the
591
+ original kept, and a Zipkin server span that shares its client's id becomes the client's child
592
+ under an id derived from it.
593
+ - A Langfuse observation of the documented list becomes one span keeping its whole record
594
+ (`hue.import.source_record`) beside the `langfuse.*` and GenAI attributes Hue reads; an id that
595
+ is no OpenTelemetry id is mapped as `hue import langfuse` maps it, so an export and a live import
596
+ of the same trace agree.
597
+ - An ATIF export becomes one trace: an agent span for the run (`invoke_agent <agent.name>`), a
598
+ model span per agent step with the conversation so far, a tool span per call, and each embedded
599
+ subagent as its own agent span under the call that spawned it, under the instrumentation scope
600
+ `hue.import.atif`. ATIF records instants, not durations, so model and tool spans are points in
601
+ time, and a tool's result is kept as text with no error status of the importer's making.
602
+ - Every other format is read as rows: each record becomes one span of its trace, written by one
603
+ emitter under the GenAI conventions Hue reads. A model call carries `gen_ai.operation.name`
604
+ `chat`, its model, provider, messages and token usage; a tool call `execute_tool` with the tool's
605
+ name, call id, arguments and result; an agent `invoke_agent`. Chat content in the common shapes
606
+ (OpenAI chat messages and choices, OpenAI Responses items, Anthropic content blocks, LangChain
607
+ messages, plain strings) becomes GenAI message parts; any other content is kept as `input.value`
608
+ and `output.value`. An error is an error status with its message. The trace's user and
609
+ conversation go on its root spans (never a tool's), and each span keeps its whole source record
610
+ as `hue.import.source_record`. An attribute the source recorded under its own name (a Raindrop
611
+ span's `gen_ai.*`, for example) is kept as recorded, with none of the same name added beside it,
612
+ unless it names a user or conversation that `--user-key` or `--session-key` replaces.
613
+ - A vendor's own evaluation recorded inside a production trace is read as such, not as the
614
+ agent's work: a Braintrust span with `purpose: "scorer"` or of type `score`, `automation`,
615
+ `facet`, `classifier`, `preprocessor` or `review` (online scoring adds them to production
616
+ traces), and a Langfuse `EVALUATOR` observation. That span and everything beneath it (a judge's
617
+ model call) carry no GenAI operation, model, messages, tool call or usage, their content kept as
618
+ `input.value` and `output.value`, and never name the trace's user or conversation, as a tool's
619
+ subtree never does. The trace's origin is unchanged.
620
+
621
+ Rules shared by the row formats:
622
+
623
+ - Ids: a vendor's 32-hex trace id and 16-hex span id are kept (MLflow's base64 ids are read as the
624
+ hex they encode); any other id is mapped by SHA-256, the original kept as
625
+ `hue.import.original_trace_id` or `hue.import.original_span_id`. A parent
626
+ the export does not hold leaves the span a root of its trace, the original kept as
627
+ `hue.import.original_parent_span_id`. A Raindrop event without an `event_id` and a transcript
628
+ without an id of its own are named by the SHA-256 of the record's canonical JSON, so the same
629
+ record in two files is one trace.
630
+ - Copies: a record exported more than once keeps its latest copy, by the field the format records
631
+ it in (Braintrust's `_xact_id`, Langfuse's `updatedAt`, Opik's `last_updated_at`, Helicone's
632
+ `updated_at`, the ended copy of a LangSmith run, a Weave call, an OpenAI Agents span or a
633
+ Raindrop event). Copies of a span with no such field must agree, or the trace is refused. A
634
+ record identical to one read elsewhere (a repeated line of a chat JSONL file, an overlap of two
635
+ exports) is read once, and the value that added nothing new is named in `files.notes`.
636
+ - Token counts follow the GenAI conventions: input includes cached input and output includes
637
+ reasoning, with the cached, cache-write and reasoning counts also kept on their own. Helicone
638
+ stores `prompt_tokens` without `prompt_cache_read_tokens` and `prompt_cache_write_tokens`, and
639
+ `completion_tokens` without `reasoning_tokens`, so those are added back. LangSmith's cache writes
640
+ are read under `cache_creation`, the v2 API's `cache_write`, or the sum of
641
+ `ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens` that its Anthropic wrapper records.
642
+ - Kinds a source recorded late: Weave records an op's kind (`attributes.weave.kind`) since January
643
+ 2026; an earlier call from an op Weave's integrations patch (`openai.chat.completions.create`,
644
+ `anthropic.Messages.create`, `litellm.completion` and the rest of its model-call ops; MCP
645
+ `call_tool` and CrewAI tools) is read as the kind those integrations give it now.
646
+ - Links: an MLflow span's `links` become span links, to a span of its own trace or of another.
647
+ - Origin: Braintrust experiment rows and dataset records (`dataset_id`), LangSmith runs with a
648
+ `reference_example_id`, MLflow traces tagged `mlflow.eval.requestId` or
649
+ `mlflow.trace.sourceScorer`, Opik traces from an experiment, evaluator, optimization or the
650
+ playground, Weave traces with an `Evaluation.*` op or a scorer as their root (a scorer applied
651
+ inside the application's own op, a guardrail, is a span of its production trace), Datadog
652
+ `experiment` spans and every span ddtrace tags with an `experiment_id`, Helicone requests of an
653
+ experiment or a Helicone evaluator (the `Helicone-Experiment-Id` or `Helicone-Evaluator`
654
+ property, matched without regard to case), and Langfuse's `sdk-experiment` and `langfuse-…`
655
+ environments (in both Langfuse formats) are declared evaluation traffic.
656
+ - Instants: Datadog's `start_ns` and `duration` are nanoseconds, and a `start_ns` too small to be
657
+ nanoseconds is read as milliseconds or microseconds by its size, the span's `duration` in the
658
+ same unit (`start_ns_read_as_ms` and `duration_read_as_ms`, or the `_us` pair). An ISO instant without a zone (LangSmith, Langfuse's ClickHouse
659
+ `YYYY-MM-DD HH:MM:SS.ffffff`) is UTC. Where a source records no instant, a fixed rule fills it
660
+ in, names it on the span (`hue.import.timing`) and counts it in the report (`traces.timing`):
661
+ - a span without a start starts at its end (`start_at_end`), else with its parent
662
+ (`start_from_parent`), else with the trace's earliest instant (`start_from_trace`);
663
+ - a span without an end ends with its last child (`end_from_children`), else at its start
664
+ (`end_at_start`);
665
+ - a trace that records no instant at all is placed at 2000-01-01T00:00:00Z, a millisecond per
666
+ record in source order (`untimed_trace`); plain transcripts record none except a Responses
667
+ object's `created_at`;
668
+ - a format that takes an instant from another field names that too: Braintrust's `created`
669
+ (`start_from_created`), a LangSmith run's `dotted_order` (`start_from_dotted_order`), a
670
+ Helicone request's `delay_ms` (`end_from_latency`) and a Raindrop span's `duration_ns`
671
+ (`end_from_duration`);
672
+ - an event recorded without a time (a Braintrust log row, an MLflow span event) is placed at its
673
+ span's start (`event_at_span_start`).
674
+
675
+ The same rules fill in the instants a Zipkin or Phoenix span was exported without.
676
+
677
+ **History owner.** A trace's owner is read from its spans as Hue reads it: the trace's local roots
678
+ (spans with no parent, or one outside the trace) first, then its other spans, never a tool span or
679
+ anything beneath one, and every span that names one must agree. An explicit
680
+ `hue.world.entity.namespace` and `hue.world.entity.id` wins over a user. A user is the first of
681
+ `user.id`, `enduser.id` and the other attributes and metadata fields Hue reads (OpenTelemetry,
682
+ Vercel AI SDK, OpenLLMetry, Langfuse, LangSmith, Braintrust, Mastra and OpenInference metadata),
683
+ except that `user.id` beside `user.account_uuid` names a Claude Code installation and is passed
684
+ over.
685
+
686
+ **`--user-key` and `--session-key`.** For an export whose user sits under a key of its own,
687
+ `--user-key <key>` names it, and `--session-key <key>` names the conversation the same way. The key
688
+ is one of:
689
+
690
+ - a dotted key such as `app.customer_id`, looked up as an attribute of each span or its resource,
691
+ then in the record's metadata (the exact key, then as a dotted path), then in the record itself;
692
+ - a JSONPath of members and indexes into the source record: `$.metadata.customer.id`,
693
+ `$.metadata["customer.id"]` or `$.tags[0]`;
694
+ - a JSON Pointer into the source record: `/metadata/customer/id`.
695
+
696
+ The key is read on the agent's spans as the owner is: the roots' value when they name one, else the
697
+ other spans', never a tool span's, and spans that disagree name nobody. For a row format (the
698
+ `langfuse` format among them) the key's value is written as the root's `user.id` (or
699
+ `gen_ai.conversation.id`) in place of the user the record names, and no span keeps an attribute it
700
+ recorded that Hue would read another user (or conversation) from, so the trace names the key's
701
+ value, or nobody when its spans disagree. A trace kept as recorded or converted span for span (OTLP,
702
+ Jaeger, Zipkin, Phoenix, the documented Langfuse list, ATIF) that names no owner gains one child
703
+ span of the agent's earliest local root (never a tool span), `hue.import.user` with the single
704
+ attribute `user.id` (or `hue.import.session` with `gen_ai.conversation.id`), so its recorded spans
705
+ keep their bytes and a trace also sent live does not conflict. An owner the spans already name is
706
+ kept, and a trace with no root but tool spans gains nothing.
707
+
708
+ **Limits and refusals.** A malformed record refuses its trace, never the file: an OTLP trace with a
709
+ malformed span (an attribute value of no OTLP shape, a protobuf span that does not decode) or whose
710
+ parents close on themselves is refused rather than imported without it, and the request's other
711
+ traces are read. A page of the documented Langfuse list holding an integer too large to represent
712
+ exactly refuses only the traces of the observations that hold one. The same trace in two files is
713
+ one trace when the copies agree and a refusal when they differ. A trace with more than 100,000
714
+ spans, 512 batches or 10,000 values is refused and named, as is a transcript whose turns together
715
+ repeat more than 64 MiB of conversation (each turn's model span holds the conversation before it).
716
+ An input of more than 25,000 traces, more than 256 MiB decompressed (every trace is held until all
717
+ are read; prepare a larger export in parts, each to its own `--out`), or whose manifest would exceed
718
+ 32 MiB (4 MiB compressed), is refused whole. The messages of one length-prefixed protobuf file share
719
+ the 64 MiB a file may decompress to.
720
+
721
+ **Report.** `prepare` reports what it read and what it could not, down to the line; `--json` prints
722
+ the report as one object:
723
+
724
+ - `manifest_digest`, `totals` (traces, batches, spans, blobs, bytes) and `source`, which
725
+ `prepare_trace_import` takes as given;
726
+ - `formats`: the traces prepared per format id;
727
+ - `files.supported`: each file read, with its format and the traces it held;
728
+ - `files.unsupported`: each file of which nothing could be read, with the reason (not JSON, a JSON
729
+ document of no recognized format, an OTLP logs or metrics export, an export page with no
730
+ records, an encrypted or nested ZIP member);
731
+ - `files.skipped`: each line of JSON Lines (`line`), record of a JSON array (`record`) or message
732
+ of a length-prefixed protobuf file (`line`) that could not be read, with the reason, in file
733
+ order; the rest of the file is read, and past 100 in one file the rest are counted in one entry;
734
+ - `files.notes`: what was read with a repair or read once though the input held it more than once
735
+ (a gzip or zstd file read up to a break, `NaN` read as null, a record identical to one read
736
+ elsewhere), with its `line` or `record`; past 100 in one file the rest are counted;
737
+ - `traces.refused`: each trace or record refused, with its `file`, `line`, `record` and `trace_id`
738
+ where known and the reason;
739
+ - `traces.timing`: the traces whose instants a rule supplied, per rule;
740
+ - `traces.without_identity`: the traces without a declared history owner, which import like any
741
+ other trace and can contribute to a World if Hue resolves their owner from the imported records;
742
+ - `traces.by_origin`: the origin each source recorded (`production`, `evaluation`, `simulation` or
743
+ `unknown`); evaluation and simulation traffic is imported and kept out of production history;
744
+ - `traces.remapped`: the traces and spans whose vendor ids were mapped.
745
+
746
+ An existing `--out` directory is accepted only while it holds nothing but an earlier preparation
747
+ and no upload has started in it.
554
748
 
555
749
  **Authorize.** Call `prepare_trace_import` over the Hue MCP with the report's `manifest_digest`,
556
750
  `totals` and `source`. It answers the import id, `upload_url` and an `authorization` (`hue_it_` and
@@ -698,7 +892,7 @@ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifac
698
892
  # Set project to an existing supported fixture; use the same directory on resume.
699
893
  project=/absolute/path/to/supported-fixture
700
894
  node packages/sdk-typescript/scripts/verify-setup-live.mjs \
701
- --archive .artifacts/typescript/hue-run-sdk-0.21.1.tgz \
895
+ --archive .artifacts/typescript/hue-run-sdk-0.23.0.tgz \
702
896
  --origin https://STAGING_ORIGIN \
703
897
  --project "$project" --command setup \
704
898
  --evidence .context/setup-staging-before-claim.json
@@ -709,7 +903,7 @@ the private local handoff and finish the real browser claim, then reconcile the
709
903
 
710
904
  ```sh
711
905
  node packages/sdk-typescript/scripts/verify-setup-live.mjs \
712
- --archive .artifacts/typescript/hue-run-sdk-0.21.1.tgz \
906
+ --archive .artifacts/typescript/hue-run-sdk-0.23.0.tgz \
713
907
  --origin https://STAGING_ORIGIN \
714
908
  --project "$project" --command claim \
715
909
  --evidence .context/setup-staging-after-claim.json
@@ -751,10 +945,10 @@ otherwise. Never hand the agent Hue-native tools (`context.tools`, `context.mcp`
751
945
  production agent lacks. Pin the SDK, so the agent's repository gains no dependency:
752
946
 
753
947
  ```sh
754
- npx -y @hue-run/sdk@0.21.1 eval --case "Refund an eligible charge" --command "node agent.js"
755
- npx -y @hue-run/sdk@0.21.1 eval --case https://app.hue.run/projects/demo/scenarios/<id> --command "python agent.py" --timeout 120 --baseline <run id>
756
- npx -y @hue-run/sdk@0.21.1 eval --set "Billing regressions" --scorer-version <id> --command "node agent.js" --save-version
757
- npx -y @hue-run/sdk@0.21.1 eval --worker --command "node agent.js" --agent-key support-agent --revision <new-agent-revision>
948
+ npx -y @hue-run/sdk@0.23.0 eval --case "Refund an eligible charge" --command "node agent.js"
949
+ npx -y @hue-run/sdk@0.23.0 eval --case https://app.hue.run/projects/demo/scenarios/<id> --command "python agent.py" --timeout 120 --baseline <run id>
950
+ npx -y @hue-run/sdk@0.23.0 eval --set "Billing regressions" --scorer-version <id> --command "node agent.js" --save-version
951
+ npx -y @hue-run/sdk@0.23.0 eval --worker --command "node agent.js" --agent-key support-agent --revision <new-agent-revision>
758
952
  ```
759
953
 
760
954
  One invocation creates one run holding every selected case; it prints the run URL
@@ -792,7 +986,7 @@ export default function runMyAgent(inputs: JsonValue, context: SimulationTargetC
792
986
  ```
793
987
 
794
988
  ```sh
795
- npx -y @hue-run/sdk@0.21.1 eval --case "Refund an eligible charge" ./hue-agent.ts --content
989
+ npx -y @hue-run/sdk@0.23.0 eval --case "Refund an eligible charge" ./hue-agent.ts --content
796
990
  ```
797
991
 
798
992
  `--scenario` remains an alias for `--case` for existing scripts. Pass one selection flag.
package/README.md CHANGED
@@ -552,8 +552,8 @@ the Langfuse project's own keys (`LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, a
552
552
  `HUE_API_KEY`:
553
553
 
554
554
  ```sh
555
- npx -y @hue-run/sdk@0.21.1 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
556
- npx -y @hue-run/sdk@0.21.1 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
555
+ npx -y @hue-run/sdk@0.23.0 import langfuse --env-path .env.langfuse --from 2026-01-01 --dry-run
556
+ npx -y @hue-run/sdk@0.23.0 import langfuse --env-path .env.langfuse --from 2026-01-01 --media
557
557
  ```
558
558
 
559
559
  - It reads Langfuse Cloud and self-hosted v4 through the Observations API v2, and self-hosted v3
@@ -563,8 +563,11 @@ npx -y @hue-run/sdk@0.21.1 import langfuse --env-path .env.langfuse --from 2026-
563
563
  `langfuse.observation.*` and `langfuse.trace.*` attributes Hue reads, the user, session, release
564
564
  and environment, and the model and token counts as GenAI attributes. Hex ids are kept and other
565
565
  ids derived from them, so running the same command again resumes an import without duplicates.
566
- - Langfuse's environment is also sent, as recorded, as `hue.import.source_environment`; only
567
- traces Langfuse recorded in `production` add to a user's World.
566
+ - Langfuse's environment is also sent, as recorded, as `hue.import.source_environment`.
567
+ Ended traces with a resolved user can contribute to that user's World with any environment
568
+ value or without an environment declaration. Traces linked to Hue runs, World instances, or
569
+ run executions never contribute. Hue's evaluation and simulation evidence and deliberately
570
+ excluded traces also stay excluded.
568
571
  - Spans are exported by the SDK's transport with its content-capture default; `--no-content`
569
572
  sends metadata only.
570
573
  - With `--media`, files that Langfuse media references point to are downloaded and uploaded under
@@ -580,9 +583,7 @@ mapping, media handling, limits and exit codes.
580
583
 
581
584
  `hue traces import` brings exports you already hold into Hue in two steps, with no project key.
582
585
  `prepare` converts the bundle on your machine and sends nothing; a Hue MCP connection authorizes
583
- the delivery with `prepare_trace_import`; `upload` delivers it with that authorization. Sources
584
- are files, directories or ZIP archives of OTLP/JSON trace exports, complete Langfuse observation
585
- exports and text ATIF v1.7 and v1.8 exports, recognized by content:
586
+ the delivery with `prepare_trace_import`; `upload` delivers it with that authorization:
586
587
 
587
588
  ```sh
588
589
  npx -y @hue-run/sdk@latest traces import prepare ./exports --out ./exports.hue-import --json
@@ -591,18 +592,37 @@ npx -y @hue-run/sdk@latest traces import prepare ./exports --out ./exports.hue-i
591
592
  npx -y @hue-run/sdk@latest traces import upload ./exports.hue-import --authorization-file ./authorization.json --json
592
593
  ```
593
594
 
594
- - `prepare` writes each trace as gzip OTLP/JSON batches beside a manifest and reports the
595
- manifest's digest and totals, the formats recognized, the files it could not read, invalid vendor
596
- ids it mapped to OpenTelemetry ids, and the traces without a history owner (a root `user.id` or an
597
- explicit entity). Nothing about a file's name or place reaches a span.
595
+ - Sources are files, directories or ZIP archives, gzip or zstd compressed or not, holding JSON
596
+ documents, arrays, JSON Lines or binary OTLP. Each file is recognized by its content, never its
597
+ name, in these formats: OTLP/JSON, OTLP protobuf and OpenTelemetry Collector file exports,
598
+ Jaeger, Zipkin v2, Langfuse (API answers, UI downloads and blob-storage files), Braintrust span
599
+ rows, LangSmith runs, Arize Phoenix spans, MLflow traces, Opik exports, W&B Weave calls, Datadog
600
+ LLM Observability spans, OpenAI Agents SDK traces, Raindrop events and span rows, Helicone
601
+ requests, ATIF v1.7 and v1.8, and OpenAI chat, OpenAI Responses and Anthropic Messages
602
+ transcripts. One `prepare` reads up to 256 MiB decompressed; prepare a larger export in parts.
603
+ - `prepare` writes each trace as gzip OTLP/JSON batches beside a manifest. Spans of OTLP exports
604
+ (Jaeger API v3 answers included) are kept as recorded, and Jaeger, Zipkin and Phoenix spans
605
+ become the OTLP spans they were. Every other record becomes the GenAI spans Hue reads, with its
606
+ model and tool calls, token usage and errors, and keeps its whole source record. Each trace's end
607
+ user and conversation are read where the source names them, never from a tool's arguments or
608
+ result, and a converted record carries them as `user.id` and `gen_ai.conversation.id` on the
609
+ trace's root. `--user-key` and `--session-key` name them for an export that records them under
610
+ keys of its own.
611
+ - The report gives the manifest's digest and totals, the traces per format, the files it could not
612
+ read, each line or record of a file it could not read (the rest of the file is read), what it
613
+ read with a repair (a compressed file read up to a break, `NaN` read as null) or read once though
614
+ the input held it twice, the traces refused (a malformed record refuses only its trace), the instants filled in by a fixed rule,
615
+ invalid vendor ids mapped to OpenTelemetry ids, and the traces without a declared history owner,
616
+ read from the spans as Hue reads them. Nothing about a file's name or place reaches a span.
598
617
  - `upload` registers the manifest, sends each trace's large values and batches in order, two
599
618
  requests at a time, checks every span's receipt, reconciles and completes the import. Interrupted,
600
619
  run it again: it resumes from `progress.json` in the bundle. An expired authorization is renewed
601
620
  by calling `prepare_trace_import` again with the same digest and totals.
602
621
  - The authorization (`hue_it_…`) is read only from the file and never printed.
603
622
 
604
- See [Import a bundle of existing traces](CLI.md#import-a-bundle-of-existing-traces) for the
605
- formats, the report, what Hue refuses and the exit codes.
623
+ See [Import a bundle of existing traces](CLI.md#import-a-bundle-of-existing-traces) for how each
624
+ format is recognized and where its user and conversation come from, the report, what Hue refuses
625
+ and the exit codes.
606
626
 
607
627
  ## Sub-agents
608
628
 
@@ -732,6 +752,27 @@ but those integrations do not receive that call's events. To keep an existing
732
752
  OpenTelemetry exporter, attach Hue's transport to the same provider using the
733
753
  [existing-provider recipe](#existing-opentelemetry-providers).
734
754
 
755
+ The end user and conversation can also come from AI SDK 7's runtime context. `hueTelemetry(hue)`
756
+ records the call's `runtimeContext.userId` as `user.id`, and `runtimeContext.sessionId` (else
757
+ `threadId`) as `gen_ai.conversation.id`, on every span of the call; a value that is neither a
758
+ non-empty string nor a finite number is ignored. The returned options name those keys in `includeRuntimeContext`,
759
+ because AI SDK withholds runtime context from telemetry unless a key is named there; no other
760
+ runtime-context value is recorded. An enclosing `withSpan`'s `userId` or `sessionId` takes
761
+ precedence:
762
+
763
+ ```ts
764
+ const result = await generateText({
765
+ model,
766
+ prompt,
767
+ runtimeContext: { userId: user.id, sessionId: chat.id },
768
+ telemetry: hueTelemetry(hue),
769
+ });
770
+ ```
771
+
772
+ For other keys, pass `hueTelemetry(hue, { userKey: "accountId", sessionKey: ["chatId"] })`;
773
+ `false` records none from runtime context. An application that sets its own
774
+ `includeRuntimeContext` keeps these keys by spreading the returned one into it.
775
+
735
776
  Reuse the client across server requests. Await stream completion before flushing;
736
777
  returning a streaming `Response` does not mean its stream has finished. The
737
778
  [Next.js streaming recipe](https://docs.hue.run/integrations/opentelemetry#flush-streamed-responses-in-next-js)
@@ -1065,6 +1106,16 @@ own. Stop request production
1065
1106
  before shutdown so late spans cannot race it. A client does not own instrumented
1066
1107
  operations still running in the application.
1067
1108
 
1109
+ In Vercel Functions, the TypeScript SDK automatically registers admitted spans and logs with
1110
+ [`waitUntil`](https://vercel.com/docs/functions/functions-api-reference/vercel-functions-package#waituntil).
1111
+ The response or Workflow step can return while the existing batches and their media uploads finish
1112
+ in the background, within the function's lifetime limit. You can remove a per-step
1113
+ `await hue.flushSafe()` when its purpose is to prevent the function freezing before delivery.
1114
+ Keep an explicit flush when you need its acknowledgement report before continuing. Background
1115
+ delivery preserves capture, redaction, batching and failure reports; it never flushes or shuts down
1116
+ application-owned providers. In other hosts, keep the host's lifetime hook or await a flush before
1117
+ a short-lived process exits.
1118
+
1068
1119
  ## Live spans
1069
1120
 
1070
1121
  OpenTelemetry exports a span only when it ends, so a long streamed turn would otherwise stay
@@ -1078,7 +1129,11 @@ arrives.
1078
1129
  The placeholder then waits for the next batch export like any queued span: up to 500 ms until the
1079
1130
  next tick, then the 1 s batch delay when nothing else is queued, so it usually reaches Hue within
1080
1131
  about 1.5 s of its span starting. It goes out sooner when a batch is already scheduled, for example
1081
- because another span has just ended, and at once for a full batch or `flush()`. It goes out later
1132
+ because another span has just ended, and at once for a full batch or a flush with no completed
1133
+ spans. When a flush has completed spans to deliver, it defers queued placeholders to the next
1134
+ normal announcement after the drain and its uploads finish. This avoids an extra acknowledgement
1135
+ for a still-open parent while preserving delivery of its completed children. A parent that ends
1136
+ before that announcement sends only its completed span. A placeholder goes out later
1082
1137
  while an earlier export is still in flight, because the next batch is scheduled only after that
1083
1138
  export finishes. A placeholder whose span has ended by the time it is exported is not sent, so a
1084
1139
  short span may send none and appear in Hue only when it finishes.
package/WORLDS.md CHANGED
@@ -110,9 +110,12 @@ await hue.withSpan("assistant request", () =>
110
110
  await hue.shutdownSafe();
111
111
  ```
112
112
 
113
- An attached client uses your application's existing OpenTelemetry resource; configure production
114
- provenance there. Mark faithful production traffic accurately. Eval, simulation and excluded
115
- traffic cannot populate a live user's World. Historical imports require separate trusted admission.
113
+ An attached client uses your application's existing OpenTelemetry resource. Ended traces with a
114
+ resolved user can contribute to that user's World with any environment value or without an
115
+ environment declaration. Historical records sent over OTLP follow the same rule. The
116
+ [verified import route](https://docs.hue.run/guides/import-traces) retains the recorded origin and
117
+ follows the same rule after verification. Traces linked to Hue runs, World instances, or run executions never contribute.
118
+ Hue's evaluation and simulation evidence and deliberately excluded traces also stay excluded.
116
119
 
117
120
  Use the same fields with your framework's tool-span hook. A parent-only account label cannot
118
121
  resolve several accounts used by different tool calls. Account IDs and resource IDs stay separate;
package/dist/ai-sdk.d.ts CHANGED
@@ -1,12 +1,34 @@
1
1
  import type { TelemetryOptions } from "ai";
2
2
  import type { HueClient } from "./client.js";
3
+ /** Options for {@link hueTelemetry}. */
4
+ export interface HueTelemetryOptions {
5
+ /**
6
+ * The runtime-context key whose value names the end user, recorded as `user.id` on the call's
7
+ * spans. `"userId"` by default; `false` records no user from runtime context.
8
+ */
9
+ userKey?: string | false;
10
+ /**
11
+ * The runtime-context keys whose value names the conversation, recorded as
12
+ * `gen_ai.conversation.id` on the call's spans; the first key with a value wins.
13
+ * `["sessionId", "threadId"]` by default; `false` records no conversation from runtime context.
14
+ */
15
+ sessionKey?: string | readonly string[] | false;
16
+ }
3
17
  /**
4
18
  * Per-call telemetry for AI SDK 7: pass as an agent's or generation call's `telemetry` option.
5
19
  * Spans come from Hue's tracer, so they parent under `withSpan` and inherit session, user and
6
20
  * workspace identifiers; prompt and response recording follow `captureContent`. It does not
7
21
  * change global AI SDK integrations.
8
22
  *
23
+ * The end user and conversation can also come from the call's AI SDK runtime context: the value
24
+ * under `userKey` (`userId` by default) is recorded as `user.id`, and the first value under
25
+ * `sessionKey` (`sessionId`, then `threadId`) as `gen_ai.conversation.id`, on every span of the
26
+ * call. The returned `includeRuntimeContext` names those keys, which AI SDK otherwise withholds
27
+ * from telemetry; no other runtime-context value is recorded. An enclosing `withSpan`'s
28
+ * `userId` or `sessionId` takes precedence.
29
+ *
9
30
  * @throws TypeError when the client is enabled and the installed `ai` major version is below 7;
10
31
  * AI SDK 6 applications use `hueExperimentalTelemetry` from `@hue-run/sdk` instead.
32
+ * @throws TypeError when `userKey` or `sessionKey` names an empty key.
11
33
  */
12
- export declare function hueTelemetry(hue: HueClient): TelemetryOptions;
34
+ export declare function hueTelemetry(hue: HueClient, options?: HueTelemetryOptions): TelemetryOptions;