Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ section into the new version. Releases before 0.11.0 are not recorded here.

### Migration

- Stored format 16 adds optional `TaskStarted.traceParent`. Task execution and
queued follow-ups restore this recorded parent across runner contexts and
process restarts. Identical start retries keep their first parent; old rows
retain their previous ancestry. Custom Tasks rerun the extended conformance
checks. Trace IDs use valid nonzero hexadecimal IDs; invalid or disabled
tracing is omitted without failing task admission.

- Stored format 15 adds helper question and answer outboxes and the `waiting`
turn outcome. Exhaustive outcome/status matches handle `waiting`; it consumes
its admitted input and never produces a final `task.result`. Custom `Tasks`
Expand Down Expand Up @@ -1110,6 +1117,14 @@ stays inside its package, with nothing deprecated first:

### Added

- `LangSmithTelemetryLive` and `LangSmithTelemetryHttpLive` export traces over
Effect's OTLP exporter to one project or registered agent/environment, with
redacted credentials and one shared flusher. Native model operations own
inference usage; framework wrappers remain chains and planned tool batches
add no billed model run. Existing content-capture controls remain in force.
Local collector and fresh-process tests verify emitted protocol and ancestry;
live LangSmith ingestion has not been verified.

- Helpers can ask their parent without keeping a turn or worker open.
`ask_parent` yields a waiting turn; `answer_task` stores an immutable answer
and delivers it through the child's durable inbox for a later turn.
Expand Down
8 changes: 8 additions & 0 deletions api/core.api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ const SkillReferenceId
const SkillTree
const SkillTreeEntry
const SkillsActivatedEvent
const SpanId
const StatOptions
const StepEndedEvent
const StepLoopReferences
Expand Down Expand Up @@ -293,6 +294,8 @@ const ToolRefusedEvent
const ToolResultInterception
const ToolResultPart
const ToolStartedEvent
const TraceId
const TraceParent
const TransientTurnEvents
const TurnAdmissionOpen
const TurnBeginError
Expand Down Expand Up @@ -344,6 +347,7 @@ const entryId
const errorCodeOf
const escapeFrame
const estimateMessageTokens
const externalSpanOf
const fingerprintOf
const fitTarget
const formatModelSelection
Expand Down Expand Up @@ -433,6 +437,7 @@ const toolCallStatesOf
const toolInputOf
const toolInputsOf
const toolResultMedia
const traceParentOf
const turnAttributes
const turnBeginCodes
const turnBilledUsageOf
Expand Down Expand Up @@ -671,6 +676,7 @@ type SkillReference
type SkillReferenceId
type SkillTree
type SkillTreeEntry
type SpanId
type StatOptions
type StepResult
type StepStatus
Expand Down Expand Up @@ -705,6 +711,8 @@ type ToolMessage
type ToolRefusalReason
type ToolResultInterception
type ToolResultPart
type TraceId
type TraceParent
type TurnBeginError
type TurnContextBody
type TurnEndReason
Expand Down
9 changes: 9 additions & 0 deletions api/plugin-telemetry.api.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
const LangSmithAgentId
const LangSmithEnvironment
const LangSmithTarget
const LangSmithTelemetryHttpLive
const LangSmithTelemetryLive
const OtlpCompression
const OtlpProtocol
const OtlpTelemetryFromEnv
Expand All @@ -12,7 +17,11 @@ const TurnTelemetryLive
const flushTelemetry
const telemetryDefaults
const telemetryPlugin
interface LangSmithTelemetryOptions
interface OtlpTelemetryOptions
type LangSmithAgentId
type LangSmithEnvironment
type LangSmithTarget
type OtlpCompression
type OtlpProtocol
type OtlpTemporality
Expand Down
26 changes: 26 additions & 0 deletions docs/event-reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -9812,6 +9812,27 @@
"turn"
],
"additionalProperties": true
},
"traceParent": {
"type": "object",
"properties": {
"traceId": {
"type": "string",
"maxLength": 32
},
"spanId": {
"type": "string",
"maxLength": 16
},
"sampled": {
"type": "boolean"
}
},
"required": [
"traceId",
"spanId"
],
"additionalProperties": true
}
},
"required": [
Expand Down Expand Up @@ -9847,6 +9868,11 @@
"name": "startedBy",
"type": "{ runId, turn }",
"required": false
},
{
"name": "traceParent",
"type": "{ traceId, spanId, sampled }",
"required": false
}
]
},
Expand Down
1 change: 1 addition & 0 deletions docs/event-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ Feed defaults (`renderFeedDefaults`, in milliseconds):
| `mode` | `"fork" \| "spawn"` |
| `meta` | `object` |
| `startedBy` | `{ runId, turn }` (optional) |
| `traceParent` | `{ traceId, spanId, sampled }` (optional) |

### `task.result`

Expand Down
27 changes: 24 additions & 3 deletions docs/framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,9 +499,12 @@ replace the turn's ids. An
inline bus subscriber's spans nest under the publisher's. A parent span in
`TurnInput.services` or `AgentConfig.services` is ignored: to put a turn
under another span, wrap the `Agent.turn` call in `Effect.withParentSpan`.
The in-process task runner is the exception: it starts each piece of work
on a new fiber with the services it was built with, so the work nests under
the span current where the runner was built. Host code that captures a
The in-process task runner starts each piece of work on a new fiber with
the services it was built with. A task stores its valid trace parent in
`task.started` and explicitly restores it for initial and queued follow-up
`task.run` spans, including after process restart. Identical start retries keep
the original parent. Old rows without one retain the runner's previous
construction ancestry. Host code that captures a
context to provide it later passes it through `replayableContext` too.

The memory and tasks plugins add spans where their work runs: `memory.open`
Expand All @@ -512,6 +515,24 @@ work). The task spans and the tasks plugin's logs carry `efferent.session.id`
(the parent session's) and `efferent.task.id`; the ids stay out of the log
messages.

The registry's `tool.<name>` span carries neutral `gen_ai.operation.name`
(`execute_tool`) and `gen_ai.tool.name`, plus `efferent.invocation.id` and the
available `efferent.tool.call.id`. The latter can identify a provider call or a
framework-minted call; it is not advertised as a provider-issued identifier.
An artificial model operation used to run a planned tool batch carries
`efferent.inference.synthetic`; it makes no inference request.

`LangSmithTelemetryLive` from `@xandreed/plugin-telemetry` binds the existing
OTLP exporter to LangSmith without another SDK. Its key is `Redacted`; choose
one project or registered agent/environment, and override the complete
`tracesUrl` for another region or self-hosted service. The layer exports traces;
compose generic OTLP metrics separately. Native model operations own inference
usage, while agent, engine and routing wrappers remain chains. Missing usage
stays absent; selected-turn totals and the billing ledger retain their separate
meanings. The observability guide shows configuration and capture controls.
Local collectors verify emitted OTLP and task ancestry; live LangSmith ingestion
has not been verified.

**Metrics** (Effect `Metric`s, exported with the spans' exporter):

| Metric | Attributes | Counts |
Expand Down
10 changes: 7 additions & 3 deletions docs/stored-format.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"format": 15,
"fingerprint": "59535da3bf8ca91efc3770e250f1ad7d3a987d8dd28067ad59c7f6f380be78c1",
"format": 16,
"fingerprint": "f8dd00e3fbd848c63c3802f9d896c2c8a599ae522c11b82f6d19bb405da370d4",
"shapes": {
"SessionHeader": "79de8009cc2069d1fa6a1bf3a2d5c787b81b41f13163cf56cc90fc5164a5d42a",
"SessionLogEvent": "8e1dc3ebb1b0e954accf1631e128bce5c4a874845bbbedfae8a58acb970332ad",
Expand Down Expand Up @@ -41,7 +41,7 @@
"record task.cancelled": "14149c0a6d2408fae8b6439ffca20b0b153bea6d0e29b253b8122dec3c1cbcc5",
"record task.questioned": "73cfe92a6570d95f15a77ea52fc7f7bb829a1dfab43ebfe2f57c4bfabf99fe48",
"record task.result": "ef9239abd9d58e33ef8efb660c5d318af28154314cd89da15e5e318897a388b1",
"record task.started": "c698122a42b8efd9873f168212a61ec5fb4c2ff46d2065a212f49954e0b34d59"
"record task.started": "d0df11bcbd6cbc8ce4e3e14c7de50aee35ca4dabfc3e37ffb820eee14f309f8f"
},
"history": [
{
Expand Down Expand Up @@ -103,6 +103,10 @@
{
"format": 15,
"fingerprint": "59535da3bf8ca91efc3770e250f1ad7d3a987d8dd28067ad59c7f6f380be78c1"
},
{
"format": 16,
"fingerprint": "f8dd00e3fbd848c63c3802f9d896c2c8a599ae522c11b82f6d19bb405da370d4"
}
]
}
7 changes: 7 additions & 0 deletions packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Shared schemas, service ports, message codecs and protocol helpers. Runtime
dependencies are Effect and the provider-neutral effect/ai contracts only.
No provider SDKs, filesystem IO, runtime graph, terminal, or host imports.

`domain/trace-parent.entity.ts` holds the branded nonzero 32/16-hex trace and
span identities and optional sampling decision of `TraceParent`. Its pure
`traceParentOf` normalizes valid span IDs to lowercase and omits invalid/custom
or tracing-disabled ancestry; `externalSpanOf` restores an external parent,
preserving explicit sampled false and Effect's default when it is absent. No
mutable span, annotations or captured Context is stored in this data.

`src/loop` holds the References a step reads (`CurrentPromptCacheKey`,
`CurrentModelCallPolicy`, `CurrentAgentStep`, `CurrentAgentRun`, `CurrentTraceContent` with
`replayableContext`, `CurrentPromptProvenance`). `src/internal` holds what
Expand Down
44 changes: 44 additions & 0 deletions packages/core/src/domain/trace-parent.entity.functions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, test } from "bun:test"
import { Effect, Option, Schema, Tracer } from "effect"
import { externalSpanOf, traceParentOf } from "./trace-parent.entity.functions.js"
import { SpanId, TraceId, TraceParent } from "./trace-parent.entity.js"

const traceId = "1234567890abcdef1234567890abcdef"
const spanId = "1234567890abcdef"
const decode = Schema.decodeUnknownOption(TraceParent)

describe("distributed trace parents", () => {
test("accepts uppercase IDs and an omitted or false sampling decision", () => {
const ids = { traceId: traceId.toUpperCase(), spanId: spanId.toUpperCase() }
const expected = { traceId: TraceId.make(ids.traceId), spanId: SpanId.make(ids.spanId) }
expect(decode(ids)).toEqual(Option.some(expected))
expect(decode({ ...ids, sampled: false })).toEqual(Option.some({ ...expected, sampled: false }))
})

test.each([
{ traceId: "0".repeat(32), spanId }, { traceId, spanId: "0".repeat(16) },
{ traceId: traceId.slice(1), spanId }, { traceId, spanId: `${spanId}0` },
{ traceId: "g".repeat(32), spanId }, { traceId, spanId: "noop" },
{ traceId: `${traceId}\n`, spanId }, { traceId, spanId: `${spanId}\n` },
])("rejects an invalid or zero identity (%j)", (ids) => {
expect(decode(ids)).toEqual(Option.none())
})

test("capture normalizes IDs, keeps false and omits invalid custom spans", () => {
expect(traceParentOf(Tracer.externalSpan({ traceId: traceId.toUpperCase(), spanId: spanId.toUpperCase(), sampled: false })))
.toEqual(Option.some({ traceId: TraceId.make(traceId), spanId: SpanId.make(spanId), sampled: false }))
expect(traceParentOf(Tracer.externalSpan({ traceId: "custom-trace", spanId: "custom-span" }))).toEqual(Option.none())
})

test("restores explicit false and the external span constructor's default for absent sampling", () => {
const ids = { traceId: TraceId.make(traceId), spanId: SpanId.make(spanId) }
const restored = externalSpanOf({ ...ids, sampled: false })
expect([restored.traceId, restored.spanId, restored.sampled]).toEqual([traceId, spanId, false])
expect(externalSpanOf(ids).sampled).toBe(true)
})

test("disabled tracing's current noop span is omitted even though the span exists", async () => {
const capture = Effect.fn("trace_parent.capture")(function* () { return traceParentOf(yield* Effect.currentParentSpan) })
expect(await Effect.runPromise(capture().pipe(Effect.withTracerEnabled(false)))).toEqual(Option.none())
})
})
13 changes: 13 additions & 0 deletions packages/core/src/domain/trace-parent.entity.functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Schema, Tracer } from "effect"
import type { Option } from "effect"
import { TraceParent } from "./trace-parent.entity.js"

const decodeParent = Schema.decodeUnknownOption(TraceParent)

/** Capture valid ancestry as lowercase IDs; invalid or tracing-disabled spans have no durable parent. @public */
export const traceParentOf = (span: Tracer.AnySpan): Option.Option<TraceParent> => decodeParent({
traceId: span.traceId.toLowerCase(), spanId: span.spanId.toLowerCase(), sampled: span.sampled,
})

/** Restore an immutable parent without capturing a span object or its contextual services. @public */
export const externalSpanOf = (parent: TraceParent): Tracer.ExternalSpan => Tracer.externalSpan(parent)
17 changes: 17 additions & 0 deletions packages/core/src/domain/trace-parent.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Schema } from "effect"

/** A nonzero 16-byte trace identity, represented as 32 hexadecimal digits. @public */
export const TraceId = Schema.String.check(Schema.isMaxLength(32), Schema.isPattern(/^(?!0{32}$)[0-9a-f]{32}$/i)).pipe(Schema.brand("TraceId"))
export type TraceId = typeof TraceId.Type

/** A nonzero 8-byte span identity, represented as 16 hexadecimal digits. @public */
export const SpanId = Schema.String.check(Schema.isMaxLength(16), Schema.isPattern(/^(?!0{16}$)[0-9a-f]{16}$/i)).pipe(Schema.brand("SpanId"))
export type SpanId = typeof SpanId.Type

/** Serializable distributed ancestry; framework capture always writes the actual sampling decision. @public */
export const TraceParent = Schema.Struct({
traceId: TraceId,
spanId: SpanId,
sampled: Schema.optionalKey(Schema.Boolean),
})
export type TraceParent = typeof TraceParent.Type
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export type { MemoryFileSystem, MemorySeed } from "./workspace/file-system.memor
// loop
export { CurrentPromptCacheKey } from "./loop/cacheKey.js"
export { CurrentTraceContent, replayableContext } from "./loop/traceContent.js"
export { SpanId, TraceId, TraceParent } from "./domain/trace-parent.entity.js"
export { externalSpanOf, traceParentOf } from "./domain/trace-parent.entity.functions.js"

// the session log: the storage a host provides, its in-memory backend and its contract
export {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/ports/tasks.port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export class Tasks extends Context.Service<Tasks, {
/**
* Start a task. The child session is created once `task.started` is
* stored, so a start that is refused or not stored leaves no child.
* Valid current trace ancestry is stored once for execution and recovery;
* invalid or disabled tracing omits it without failing the task.
*/
readonly start: (parent: SessionAddress, input: {
/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/session/stored-format.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
*
* @public
*/
export const STORED_FORMAT: number = 15
export const STORED_FORMAT: number = 16
Loading
Loading