Skip to content

Commit c6b4be6

Browse files
feat(weave_ts): add Turn.setAttribute and Turn.addEvent (#6954)
1 parent dac57f8 commit c6b4be6

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

sdks/node/src/__tests__/genai/turn.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,70 @@ describe('Turn', () => {
5151
expect(span.events.some(e => e.name === 'exception')).toBe(true);
5252
});
5353
});
54+
55+
describe('Turn.setAttribute', () => {
56+
setupGenAITestEnvironment();
57+
const getExporter = setupExporterPerTest();
58+
59+
it('writes an attribute to the underlying span', () => {
60+
const turn = Turn.create({});
61+
turn.setAttribute('weave.cost.usd', 0.42);
62+
turn.end();
63+
const spans = getExporter().getFinishedSpans();
64+
expect(findSpan(spans, 'invoke_agent').attributes['weave.cost.usd']).toBe(
65+
0.42
66+
);
67+
});
68+
69+
it('returns this for chaining', () => {
70+
const turn = Turn.create({});
71+
expect(turn.setAttribute('k', 'v')).toBe(turn);
72+
turn.end();
73+
});
74+
75+
it('is a no-op after end()', () => {
76+
const turn = Turn.create({});
77+
turn.end();
78+
turn.setAttribute('after.end', 'x');
79+
const spans = getExporter().getFinishedSpans();
80+
expect(
81+
findSpan(spans, 'invoke_agent').attributes['after.end']
82+
).toBeUndefined();
83+
});
84+
});
85+
86+
describe('Turn.addEvent', () => {
87+
setupGenAITestEnvironment();
88+
const getExporter = setupExporterPerTest();
89+
90+
it('writes a named event with attributes', () => {
91+
const turn = Turn.create({});
92+
turn.addEvent('context_compacted', {items_before: 50, items_after: 10});
93+
turn.end();
94+
const spans = getExporter().getFinishedSpans();
95+
const ev = findSpan(spans, 'invoke_agent').events.find(
96+
e => e.name === 'context_compacted'
97+
);
98+
expect(ev?.attributes).toMatchObject({items_before: 50, items_after: 10});
99+
});
100+
101+
it('returns this for chaining', () => {
102+
const turn = Turn.create({});
103+
expect(turn.addEvent('e')).toBe(turn);
104+
turn.end();
105+
const spans = getExporter().getFinishedSpans();
106+
expect(
107+
findSpan(spans, 'invoke_agent').events.find(e => e.name === 'e')
108+
).toBeDefined();
109+
});
110+
111+
it('is a no-op after end()', () => {
112+
const turn = Turn.create({});
113+
turn.end();
114+
turn.addEvent('after.end');
115+
const spans = getExporter().getFinishedSpans();
116+
expect(
117+
findSpan(spans, 'invoke_agent').events.find(e => e.name === 'after.end')
118+
).toBeUndefined();
119+
});
120+
});

sdks/node/src/genai/turn.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import {
2+
type AttributeValue,
3+
type Attributes,
24
type Context,
35
ROOT_CONTEXT,
46
type Span,
57
SpanKind,
68
SpanStatusCode,
9+
type TimeInput,
710
trace,
811
} from '@opentelemetry/api';
912

@@ -124,6 +127,34 @@ export class Turn {
124127
});
125128
}
126129

130+
/**
131+
* Set a single attribute on the Turn span. Useful for stamping running
132+
* totals (e.g. cumulative cost, token usage) or other metadata that becomes
133+
* known mid-turn. No-op after `end()`. Mirrors OTel `Span.setAttribute`.
134+
*
135+
* @example
136+
* turn.setAttribute('gen_ai.usage.input_tokens', totalInputTokens);
137+
*/
138+
setAttribute(key: string, value: AttributeValue): this {
139+
if (this._ended) return this;
140+
this.span.setAttribute(key, value);
141+
return this;
142+
}
143+
144+
/**
145+
* Add a named event to the Turn span. Useful for marking non-span moments
146+
* such as context compaction, tool-loop detection, or guardrail trips.
147+
* No-op after `end()`. Mirrors OTel `Span.addEvent`.
148+
*
149+
* @example
150+
* turn.addEvent('context_compacted', {removedMessages: 12});
151+
*/
152+
addEvent(name: string, attributes?: Attributes, startTime?: TimeInput): this {
153+
if (this._ended) return this;
154+
this.span.addEvent(name, attributes, startTime);
155+
return this;
156+
}
157+
127158
/** Close the Turn span. Idempotent. Pass `error` to mark it as failed. */
128159
end(opts?: {error?: Error}): void {
129160
if (this._ended) {

0 commit comments

Comments
 (0)