weflayr 0.10.3 → 0.12.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weflayr",
3
- "version": "0.10.3",
3
+ "version": "0.12.0",
4
4
  "description": "Weflayr Node.js SDK — instrument any LLM client via JS Proxy",
5
5
  "main": "src/index.js",
6
6
  "types": "index.d.ts",
package/src/instrument.js CHANGED
@@ -94,7 +94,7 @@ function _wrapFn(fn, methodName, methodConfig) {
94
94
 
95
95
  if (methodConfig.streamPath && result && _isAsyncIterable(result[methodConfig.streamPath])) {
96
96
  _sendEvent('stream_start', { event_id: eventId, method: methodName, tags });
97
- const wrappedStream = _wrapStream(result[methodConfig.streamPath], methodName, tags, requestArgs, settings, startTime, methodConfig, eventId);
97
+ const wrappedStream = _wrapStream(result[methodConfig.streamPath], methodName, tags, requestArgs, settings, startTime, methodConfig, eventId, result);
98
98
  return { ...result, [methodConfig.streamPath]: wrappedStream };
99
99
  }
100
100
 
@@ -128,9 +128,12 @@ function _wrapFn(fn, methodName, methodConfig) {
128
128
  };
129
129
  }
130
130
 
131
- async function* _wrapStream(stream, methodName, tags, requestArgs, settings, startTime, methodConfig, eventId) {
131
+ // fullResult is only set for the streamPath case it's the full SDK result object
132
+ // (e.g. Mastra's StreamTextResult) so streamMiddleware factories can close over it
133
+ // and await properties like result.usage after the stream drains.
134
+ async function* _wrapStream(stream, methodName, tags, requestArgs, settings, startTime, methodConfig, eventId, fullResult) {
132
135
  let lastChunk = null;
133
- const accumulator = methodConfig.streamMiddleware ? methodConfig.streamMiddleware() : null;
136
+ const accumulator = methodConfig.streamMiddleware ? methodConfig.streamMiddleware(fullResult) : null;
134
137
 
135
138
  try {
136
139
  for await (const chunk of stream) {
@@ -141,16 +144,17 @@ async function* _wrapStream(stream, methodName, tags, requestArgs, settings, sta
141
144
  method: methodName,
142
145
  tags,
143
146
  elapsed_ms: Date.now() - startTime,
144
- ...accumulator.finalize(),
147
+ ...(await accumulator.finalize()),
145
148
  });
146
149
  }
147
150
  yield chunk;
148
151
  }
149
152
 
150
- const middlewareData = accumulator
151
- ? accumulator.finalize()
153
+ const rawMiddlewareData = accumulator
154
+ ? (await accumulator.finalize()) || {}
152
155
  : (methodConfig.middleware && lastChunk ? methodConfig.middleware(requestArgs, lastChunk) || {}
153
156
  : {});
157
+ const middlewareData = _applyContentPolicy(rawMiddlewareData, settings);
154
158
 
155
159
  _sendEvent('after', {
156
160
  event_id: eventId,
@@ -26,6 +26,42 @@
26
26
  }
27
27
  return OpenAI;
28
28
  }
29
+ if (request === '@mastra/core') {
30
+ function Agent(opts) {
31
+ this._name = opts.name;
32
+ }
33
+ Agent.prototype.generate = async function(_messages, _options) {
34
+ return {
35
+ text: 'Hello from the Mastra fake agent.',
36
+ modelId: 'gpt-4o-mini',
37
+ finishReason: 'stop',
38
+ usage: { promptTokens: 10, completionTokens: 15, totalTokens: 25 },
39
+ request: { body: '{"messages":[{"role":"user","content":"Hello"}]}' },
40
+ response: {
41
+ id: 'fake-res',
42
+ modelId: 'gpt-4o-mini',
43
+ messages: [{ role: 'assistant', content: [{ type: 'text', text: 'Hello.' }] }],
44
+ body: { choices: [{ message: { role: 'assistant', content: 'Hello.' } }] },
45
+ },
46
+ steps: [{ text: 'Hello.', request: { body: '...' }, finishReason: 'stop' }],
47
+ };
48
+ };
49
+ // stream() returns a StreamTextResult-like object (NOT directly iterable).
50
+ // usage and response are DelayedPromise wrappers matching real Mastra behaviour.
51
+ Agent.prototype.stream = function(_messages, _options) {
52
+ return {
53
+ textStream: (async function* () {
54
+ yield 'Hello'; yield ' Mastra'; yield ' stream.';
55
+ })(),
56
+ usage: { status: { type: 'resolved', value: { promptTokens: 10, completionTokens: 15, totalTokens: 25 } } },
57
+ response: { status: { type: 'resolved', value: { modelId: 'gpt-4o-mini', id: 'fake-stream-res' } } },
58
+ };
59
+ };
60
+ return { Agent };
61
+ }
62
+ if (request === '@ai-sdk/openai') {
63
+ return { openai: (modelId) => ({ _provider: 'openai', modelId }) };
64
+ }
29
65
  if (request === 'weflayr') {
30
66
  return _origLoad.call(this, path.resolve(__dirname, '../src/index.js'), parent, isMain);
31
67
  }
@@ -580,3 +616,191 @@ test('snippet propagate_runtime: propagated tag in before and after', async () =
580
616
  assert.equal(after.tags.customer_id, 'acme-corp');
581
617
  });
582
618
 
619
+ // ---------------------------------------------------------------------------
620
+ // Mastra integration — documentation snippets
621
+ // @mastra/core and @ai-sdk/openai are faked by Module._load at top of file.
622
+ // ---------------------------------------------------------------------------
623
+
624
+ test('snippet mastra_setup: wraps Mastra Agent, sends before+after events on generate()', async () => {
625
+ freshSDK();
626
+ setSnippetEnv();
627
+
628
+ const sent = await captureHttp(async () => {
629
+ // SNIPPET_START: mastra_setup
630
+ const { weflayr_setup, weflayr_instrument } = require('weflayr');
631
+ const { Agent } = require('@mastra/core');
632
+ const { openai } = require('@ai-sdk/openai');
633
+
634
+ weflayr_setup({
635
+ intake_url: process.env.WEFLAYR_INTAKE_URL,
636
+ client_id: process.env.WEFLAYR_CLIENT_ID,
637
+ client_secret: process.env.WEFLAYR_CLIENT_SECRET,
638
+ methods: [{ call: 'generate' }],
639
+ });
640
+
641
+ const agent = weflayr_instrument(
642
+ new Agent({
643
+ name: 'my-agent',
644
+ model: openai('gpt-4o-mini'),
645
+ instructions: 'You are a helpful assistant.',
646
+ })
647
+ );
648
+ // SNIPPET_END: mastra_setup
649
+
650
+ await agent.generate([{ role: 'user', content: 'Hello' }]);
651
+ });
652
+
653
+ assert.ok(sent.some(e => e.event_type === 'before'), 'before event sent');
654
+ assert.ok(sent.some(e => e.event_type === 'after'), 'after event sent');
655
+ assert.equal(sent[0].method, 'generate');
656
+ });
657
+
658
+ test('snippet mastra_tags: per-call tags stripped from generate args, appear in events', async () => {
659
+ const { weflayr_setup, weflayr_instrument } = freshSDK();
660
+ setSnippetEnv();
661
+
662
+ const { Agent } = require('@mastra/core');
663
+ const { openai } = require('@ai-sdk/openai');
664
+ weflayr_setup({ ...TEST_CREDS, methods: [{ call: 'generate' }] });
665
+ const agent = weflayr_instrument(new Agent({ name: 'test-agent', model: openai('gpt-4o-mini'), instructions: '...' }));
666
+
667
+ const sent = await captureHttp(async () => {
668
+ // SNIPPET_START: mastra_tags
669
+ await agent.generate(
670
+ [{ role: 'user', content: 'Hello' }],
671
+ { __weflayr_tags: { feature: 'chat', customer_id: '42', env: 'prod' } },
672
+ );
673
+ // SNIPPET_END: mastra_tags
674
+ });
675
+
676
+ const before = sent.find(e => e.event_type === 'before');
677
+ assert.ok(before, 'before event sent');
678
+ assert.deepEqual(before.tags, { feature: 'chat', customer_id: '42', env: 'prod' });
679
+ });
680
+
681
+ test('snippet mastra_content_privacy: text/request/steps/response content stripped from events', async () => {
682
+ freshSDK();
683
+ setSnippetEnv();
684
+
685
+ const sent = await captureHttp(async () => {
686
+ // SNIPPET_START: mastra_content_privacy
687
+ const { weflayr_setup, weflayr_instrument } = require('weflayr');
688
+ const { Agent } = require('@mastra/core');
689
+ const { openai } = require('@ai-sdk/openai');
690
+
691
+ weflayr_setup({
692
+ intake_url: process.env.WEFLAYR_INTAKE_URL,
693
+ client_id: process.env.WEFLAYR_CLIENT_ID,
694
+ client_secret: process.env.WEFLAYR_CLIENT_SECRET,
695
+ methods: [{ call: 'generate' }],
696
+ ignore_fields: (data) => {
697
+ delete data.text;
698
+ delete data.request;
699
+ delete data.steps;
700
+ if (data.response) {
701
+ delete data.response.body;
702
+ delete data.response.messages;
703
+ }
704
+ return data;
705
+ },
706
+ });
707
+
708
+ const agent = weflayr_instrument(
709
+ new Agent({
710
+ name: 'my-agent',
711
+ model: openai('gpt-4o-mini'),
712
+ instructions: 'You are a helpful assistant.',
713
+ })
714
+ );
715
+ // SNIPPET_END: mastra_content_privacy
716
+
717
+ await agent.generate([{ role: 'user', content: 'Hello' }]);
718
+ });
719
+
720
+ const after = sent.find(e => e.event_type === 'after');
721
+ assert.ok(after, 'after event sent');
722
+ assert.ok(!after.response || !('text' in after.response), 'text stripped');
723
+ assert.ok(!after.response || !('request' in after.response), 'request stripped');
724
+ assert.ok(!after.response || !('steps' in after.response), 'steps stripped');
725
+ assert.ok(!after.response?.response?.body, 'response.body stripped');
726
+ assert.ok(!after.response?.response?.messages, 'response.messages stripped');
727
+ });
728
+
729
+ test('snippet mastra_stream: stream_start + after emitted, model/usage in after via streamMiddleware', async () => {
730
+ freshSDK();
731
+ setSnippetEnv();
732
+
733
+ const sent = await captureHttp(async () => {
734
+ // SNIPPET_START: mastra_stream
735
+ const { weflayr_setup, weflayr_instrument } = require('weflayr');
736
+ const { Agent } = require('@mastra/core');
737
+ const { openai } = require('@ai-sdk/openai');
738
+
739
+ weflayr_setup({
740
+ intake_url: process.env.WEFLAYR_INTAKE_URL,
741
+ client_id: process.env.WEFLAYR_CLIENT_ID,
742
+ client_secret: process.env.WEFLAYR_CLIENT_SECRET,
743
+ methods: [
744
+ { call: 'generate' },
745
+ {
746
+ call: 'stream',
747
+ streamPath: 'textStream',
748
+ streamMiddleware: (fullResult) => ({
749
+ onChunk: () => false,
750
+ async finalize() {
751
+ const rawUsage = await fullResult.usage;
752
+ const rawResponse = await fullResult.response;
753
+ const response = rawResponse?.status?.value ?? rawResponse;
754
+ return {
755
+ usage: rawUsage,
756
+ model: response?.modelId ?? null,
757
+ };
758
+ },
759
+ }),
760
+ },
761
+ ],
762
+ ignore_fields: (data) => {
763
+ delete data.text;
764
+ delete data.request;
765
+ delete data.steps;
766
+ if (data.response) {
767
+ delete data.response.body;
768
+ delete data.response.messages;
769
+ }
770
+ return data;
771
+ },
772
+ });
773
+
774
+ const agent = weflayr_instrument(
775
+ new Agent({
776
+ name: 'my-agent',
777
+ model: openai('gpt-4o-mini'),
778
+ instructions: 'You are a helpful assistant.',
779
+ })
780
+ );
781
+
782
+ const streamResult = await agent.stream(
783
+ [{ role: 'user', content: 'Hello' }],
784
+ { __weflayr_tags: { feature: 'chat', customer_id: '42' } },
785
+ );
786
+
787
+ let text = '';
788
+ for await (const chunk of streamResult.textStream) {
789
+ text += chunk;
790
+ }
791
+ // SNIPPET_END: mastra_stream
792
+ });
793
+
794
+ const types = sent.map(e => e.event_type);
795
+ assert.ok(types.includes('before'), 'before event sent');
796
+ assert.ok(types.includes('stream_start'), 'stream_start event sent');
797
+ assert.ok(types.includes('after'), 'after event sent');
798
+
799
+ const after = sent.find(e => e.event_type === 'after');
800
+ assert.equal(after.method, 'stream');
801
+ assert.equal(after.model, 'gpt-4o-mini');
802
+ assert.ok(after.usage, 'usage present in after event');
803
+ assert.equal(after.tags.feature, 'chat');
804
+ assert.equal(after.tags.customer_id, '42');
805
+ });
806
+