explorbot 0.1.9 → 0.1.11

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 (157) hide show
  1. package/README.md +27 -1
  2. package/bin/explorbot-cli.ts +86 -15
  3. package/boat/api-tester/src/ai/curler-tools.ts +3 -3
  4. package/boat/api-tester/src/ai/curler.ts +1 -1
  5. package/boat/api-tester/src/apibot.ts +2 -2
  6. package/boat/api-tester/src/config.ts +1 -1
  7. package/dist/bin/explorbot-cli.js +85 -14
  8. package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
  9. package/dist/boat/api-tester/src/apibot.js +2 -2
  10. package/dist/package.json +2 -2
  11. package/dist/rules/navigator/output.md +9 -0
  12. package/dist/rules/navigator/verification-actions.md +2 -0
  13. package/dist/src/action-result.js +23 -1
  14. package/dist/src/action.js +46 -38
  15. package/dist/src/ai/bosun.js +16 -2
  16. package/dist/src/ai/conversation.js +39 -0
  17. package/dist/src/ai/experience-compactor.js +235 -50
  18. package/dist/src/ai/historian/codeceptjs.js +109 -0
  19. package/dist/src/ai/historian/experience.js +320 -0
  20. package/dist/src/ai/historian/mixin.js +2 -0
  21. package/dist/src/ai/historian/playwright.js +145 -0
  22. package/dist/src/ai/historian/utils.js +18 -0
  23. package/dist/src/ai/historian.js +19 -398
  24. package/dist/src/ai/navigator.js +133 -80
  25. package/dist/src/ai/pilot.js +254 -13
  26. package/dist/src/ai/planner/subpages.js +1 -30
  27. package/dist/src/ai/planner.js +33 -13
  28. package/dist/src/ai/provider.js +55 -18
  29. package/dist/src/ai/rerunner.js +3 -3
  30. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  31. package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
  32. package/dist/src/ai/researcher/locators.js +1 -1
  33. package/dist/src/ai/researcher/sections.js +8 -1
  34. package/dist/src/ai/researcher.js +43 -41
  35. package/dist/src/ai/rules.js +26 -14
  36. package/dist/src/ai/tester.js +90 -26
  37. package/dist/src/ai/tools.js +18 -10
  38. package/dist/src/api/request-store.js +20 -0
  39. package/dist/src/api/xhr-capture.js +19 -3
  40. package/dist/src/browser-server.js +16 -3
  41. package/dist/src/command-handler.js +1 -1
  42. package/dist/src/commands/add-rule-command.js +12 -9
  43. package/dist/src/commands/base-command.js +20 -0
  44. package/dist/src/commands/clean-command.js +3 -2
  45. package/dist/src/commands/compact-command.js +138 -0
  46. package/dist/src/commands/context-command.js +7 -1
  47. package/dist/src/commands/drill-command.js +4 -1
  48. package/dist/src/commands/experience-command.js +104 -0
  49. package/dist/src/commands/explore-command.js +54 -19
  50. package/dist/src/commands/freesail-command.js +2 -0
  51. package/dist/src/commands/index.js +7 -3
  52. package/dist/src/commands/init-command.js +11 -10
  53. package/dist/src/commands/learn-command.js +1 -1
  54. package/dist/src/commands/navigate-command.js +4 -1
  55. package/dist/src/commands/plan-clear-command.js +4 -1
  56. package/dist/src/commands/plan-command.js +43 -4
  57. package/dist/src/commands/plan-edit-command.js +1 -1
  58. package/dist/src/commands/plan-load-command.js +4 -1
  59. package/dist/src/commands/plan-reload-command.js +4 -1
  60. package/dist/src/commands/plan-save-command.js +20 -8
  61. package/dist/src/commands/rerun-command.js +4 -0
  62. package/dist/src/commands/research-command.js +5 -2
  63. package/dist/src/commands/start-command.js +5 -1
  64. package/dist/src/commands/test-command.js +7 -1
  65. package/dist/src/components/App.js +15 -5
  66. package/dist/src/execution-controller.js +13 -2
  67. package/dist/src/experience-tracker.js +174 -83
  68. package/dist/src/explorbot.js +31 -22
  69. package/dist/src/explorer.js +12 -5
  70. package/dist/src/observability.js +50 -99
  71. package/dist/src/playwright-recorder.js +309 -0
  72. package/dist/src/reporter.js +17 -2
  73. package/dist/src/stats.js +2 -0
  74. package/dist/src/suite.js +1 -1
  75. package/dist/src/test-plan.js +12 -0
  76. package/dist/src/utils/aria.js +37 -1
  77. package/dist/src/utils/error-page.js +30 -7
  78. package/dist/src/utils/logger.js +1 -1
  79. package/dist/src/utils/next-steps.js +37 -0
  80. package/dist/src/utils/rules-loader.js +1 -1
  81. package/dist/src/utils/test-files.js +1 -1
  82. package/dist/src/utils/url-matcher.js +50 -0
  83. package/package.json +2 -2
  84. package/rules/navigator/output.md +9 -0
  85. package/rules/navigator/verification-actions.md +2 -0
  86. package/src/action-result.ts +26 -1
  87. package/src/action.ts +44 -37
  88. package/src/ai/bosun.ts +16 -2
  89. package/src/ai/conversation.ts +37 -0
  90. package/src/ai/experience-compactor.ts +270 -63
  91. package/src/ai/historian/codeceptjs.ts +130 -0
  92. package/src/ai/historian/experience.ts +383 -0
  93. package/src/ai/historian/mixin.ts +4 -0
  94. package/src/ai/historian/playwright.ts +169 -0
  95. package/src/ai/historian/utils.ts +23 -0
  96. package/src/ai/historian.ts +35 -468
  97. package/src/ai/navigator.ts +140 -85
  98. package/src/ai/pilot.ts +259 -14
  99. package/src/ai/planner/subpages.ts +1 -24
  100. package/src/ai/planner.ts +34 -14
  101. package/src/ai/provider.ts +52 -18
  102. package/src/ai/rerunner.ts +3 -3
  103. package/src/ai/researcher/deep-analysis.ts +1 -1
  104. package/src/ai/researcher/fingerprint-worker.ts +1 -1
  105. package/src/ai/researcher/locators.ts +2 -2
  106. package/src/ai/researcher/sections.ts +7 -1
  107. package/src/ai/researcher.ts +47 -42
  108. package/src/ai/rules.ts +27 -14
  109. package/src/ai/task-agent.ts +1 -1
  110. package/src/ai/tester.ts +94 -26
  111. package/src/ai/tools.ts +53 -29
  112. package/src/api/request-store.ts +22 -0
  113. package/src/api/xhr-capture.ts +21 -3
  114. package/src/browser-server.ts +17 -3
  115. package/src/command-handler.ts +1 -1
  116. package/src/commands/add-rule-command.ts +13 -9
  117. package/src/commands/base-command.ts +26 -1
  118. package/src/commands/clean-command.ts +4 -3
  119. package/src/commands/compact-command.ts +156 -0
  120. package/src/commands/context-command.ts +8 -2
  121. package/src/commands/drill-command.ts +5 -2
  122. package/src/commands/experience-command.ts +125 -0
  123. package/src/commands/explore-command.ts +58 -21
  124. package/src/commands/freesail-command.ts +2 -0
  125. package/src/commands/index.ts +7 -3
  126. package/src/commands/init-command.ts +11 -10
  127. package/src/commands/learn-command.ts +2 -2
  128. package/src/commands/navigate-command.ts +5 -2
  129. package/src/commands/plan-clear-command.ts +5 -2
  130. package/src/commands/plan-command.ts +47 -5
  131. package/src/commands/plan-edit-command.ts +2 -2
  132. package/src/commands/plan-load-command.ts +5 -2
  133. package/src/commands/plan-reload-command.ts +5 -2
  134. package/src/commands/plan-save-command.ts +20 -9
  135. package/src/commands/rerun-command.ts +5 -0
  136. package/src/commands/research-command.ts +6 -3
  137. package/src/commands/start-command.ts +6 -2
  138. package/src/commands/test-command.ts +8 -2
  139. package/src/components/App.tsx +16 -5
  140. package/src/config.ts +6 -1
  141. package/src/execution-controller.ts +14 -3
  142. package/src/experience-tracker.ts +198 -100
  143. package/src/explorbot.ts +33 -23
  144. package/src/explorer.ts +14 -5
  145. package/src/observability.ts +50 -109
  146. package/src/playwright-recorder.ts +305 -0
  147. package/src/reporter.ts +17 -3
  148. package/src/stats.ts +4 -0
  149. package/src/suite.ts +1 -1
  150. package/src/test-plan.ts +12 -0
  151. package/src/utils/aria.ts +38 -1
  152. package/src/utils/error-page.ts +32 -7
  153. package/src/utils/logger.ts +1 -1
  154. package/src/utils/next-steps.ts +51 -0
  155. package/src/utils/rules-loader.ts +1 -1
  156. package/src/utils/test-files.ts +1 -1
  157. package/src/utils/url-matcher.ts +43 -0
@@ -1,9 +1,6 @@
1
1
  import { existsSync, mkdirSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { ActionResult } from "./action-result.js";
4
- import { ApiClient } from "./api/api-client.js";
5
- import { RequestStore } from "./api/request-store.js";
6
- import { loadSpec } from "./api/spec-reader.js";
7
4
  import { Bosun } from "./ai/bosun.js";
8
5
  import { Captain } from "./ai/captain.js";
9
6
  import { ExperienceCompactor } from "./ai/experience-compactor.js";
@@ -14,11 +11,15 @@ import { Pilot } from "./ai/pilot.js";
14
11
  import { Planner } from "./ai/planner.js";
15
12
  import { AIProvider } from "./ai/provider.js";
16
13
  import { Quartermaster } from "./ai/quartermaster.js";
17
- import { Researcher } from "./ai/researcher.js";
18
14
  import { Rerunner } from "./ai/rerunner.js";
15
+ import { Researcher } from "./ai/researcher.js";
19
16
  import { Tester } from "./ai/tester.js";
20
17
  import { createAgentTools } from "./ai/tools.js";
18
+ import { ApiClient } from "./api/api-client.js";
19
+ import { RequestStore } from "./api/request-store.js";
20
+ import { loadSpec } from "./api/spec-reader.js";
21
21
  import { ConfigParser } from "./config.js";
22
+ import { ExperienceTracker } from "./experience-tracker.js";
22
23
  import Explorer from "./explorer.js";
23
24
  import { KnowledgeTracker } from "./knowledge-tracker.js";
24
25
  import { Plan } from "./test-plan.js";
@@ -34,6 +35,8 @@ export class ExplorBot {
34
35
  needsInput = false;
35
36
  currentPlan;
36
37
  planFeature;
38
+ lastPlanError = null;
39
+ lastSavedPlanPath = null;
37
40
  agents = {};
38
41
  constructor(options = {}) {
39
42
  this.options = options;
@@ -54,13 +57,11 @@ export class ExplorBot {
54
57
  return;
55
58
  }
56
59
  try {
57
- this.config = await this.configParser.loadConfig(this.options);
58
- this.provider = new AIProvider(this.config.ai);
59
- await this.provider.validateConnection();
60
+ await this.startProviderOnly();
60
61
  this.explorer = new Explorer(this.config, this.provider, this.options);
61
62
  await this.explorer.start();
62
63
  if (!this.options.incognito) {
63
- await this.agentExperienceCompactor().compactAllExperiences();
64
+ await this.agentExperienceCompactor().autocompact();
64
65
  }
65
66
  if (this.userResolveFn)
66
67
  this.explorer.setUserResolve(this.userResolveFn);
@@ -71,9 +72,16 @@ export class ExplorBot {
71
72
  process.exit(1);
72
73
  }
73
74
  }
75
+ async startProviderOnly() {
76
+ if (this.provider)
77
+ return;
78
+ this.config = await this.configParser.loadConfig(this.options);
79
+ this.provider = new AIProvider(this.config.ai);
80
+ await this.provider.validateConnection();
81
+ }
74
82
  async stop() {
75
83
  this.agents.quartermaster?.stop();
76
- await this.explorer.stop();
84
+ await this.explorer?.stop();
77
85
  }
78
86
  async visitInitialState() {
79
87
  const url = this.options.from || '/';
@@ -97,6 +105,12 @@ export class ExplorBot {
97
105
  }
98
106
  return new KnowledgeTracker();
99
107
  }
108
+ getExperienceTracker() {
109
+ if (this.explorer) {
110
+ return this.explorer.getStateManager().getExperienceTracker();
111
+ }
112
+ return new ExperienceTracker();
113
+ }
100
114
  getConfig() {
101
115
  return this.config;
102
116
  }
@@ -186,10 +200,7 @@ export class ExplorBot {
186
200
  return this.agents.captain;
187
201
  }
188
202
  agentExperienceCompactor() {
189
- return (this.agents.experienceCompactor ||= this.createAgent(({ ai, explorer }) => {
190
- const experienceTracker = explorer.getStateManager().getExperienceTracker();
191
- return new ExperienceCompactor(ai, experienceTracker);
192
- }));
203
+ return (this.agents.experienceCompactor ||= new ExperienceCompactor(this.provider, this.getExperienceTracker()));
193
204
  }
194
205
  agentQuartermaster() {
195
206
  const config = this.config.ai?.agents?.quartermaster;
@@ -204,10 +215,10 @@ export class ExplorBot {
204
215
  return this.agents.quartermaster;
205
216
  }
206
217
  agentHistorian() {
207
- return (this.agents.historian ||= this.createAgent(({ ai, explorer }) => {
218
+ return (this.agents.historian ||= this.createAgent(({ ai, explorer, config }) => {
208
219
  const experienceTracker = explorer.getStateManager().getExperienceTracker();
209
220
  const reporter = explorer.getReporter();
210
- return new Historian(ai, experienceTracker, reporter, explorer.getStateManager());
221
+ return new Historian(ai, experienceTracker, reporter, explorer.getStateManager(), config, explorer.getPlaywrightRecorder());
211
222
  }));
212
223
  }
213
224
  agentRerunner() {
@@ -293,21 +304,18 @@ export class ExplorBot {
293
304
  if (this.currentPlan) {
294
305
  planner.setPlan(this.currentPlan);
295
306
  }
307
+ this.lastPlanError = null;
296
308
  try {
297
309
  this.currentPlan = await planner.plan(feature, opts.style, opts.extend, opts.completedPlans);
298
310
  }
299
311
  catch (err) {
300
- tag('warning').log(`Planning failed: ${err instanceof Error ? err.message : err}`);
312
+ this.lastPlanError = err instanceof Error ? err : new Error(String(err));
313
+ tag('warning').log(`Planning failed: ${this.lastPlanError.message}`);
301
314
  if (!this.currentPlan)
302
315
  return undefined;
303
316
  return this.currentPlan;
304
317
  }
305
- const savedPath = this.savePlan();
306
- if (savedPath) {
307
- const relativePath = path.relative(process.cwd(), savedPath);
308
- tag('info').log(`Plan saved to: ${relativePath}`);
309
- tag('info').log(`Edit the plan file and run /plan:load ${relativePath} to reload it`);
310
- }
318
+ this.savePlan();
311
319
  return this.currentPlan;
312
320
  }
313
321
  getPlansDir() {
@@ -329,6 +337,7 @@ export class ExplorBot {
329
337
  const planFilename = filename || this.generatePlanFilename();
330
338
  const planPath = path.join(plansDir, planFilename);
331
339
  Plan.saveMultipleToMarkdown(plans, planPath);
340
+ this.lastSavedPlanPath = planPath;
332
341
  return planPath;
333
342
  }
334
343
  generatePlanFilename() {
@@ -8,12 +8,13 @@ import { createTest } from 'codeceptjs/lib/mocha/test';
8
8
  import { ActionResult } from "./action-result.js";
9
9
  import Action from './action.js';
10
10
  import { visuallyAnnotateContainers } from "./ai/researcher/coordinates.js";
11
+ import { RequestStore } from "./api/request-store.js";
12
+ import { XhrCapture } from "./api/xhr-capture.js";
11
13
  import { ConfigParser, outputPath } from './config.js';
12
14
  import { KnowledgeTracker } from './knowledge-tracker.js';
15
+ import { PlaywrightRecorder } from "./playwright-recorder.js";
13
16
  import { Reporter } from "./reporter.js";
14
17
  import { StateManager } from './state-manager.js';
15
- import { RequestStore } from "./api/request-store.js";
16
- import { XhrCapture } from "./api/xhr-capture.js";
17
18
  import { createDebug, log, tag } from './utils/logger.js';
18
19
  import { WebElement, extractElementData } from "./utils/web-element.js";
19
20
  const debugLog = createDebug('explorbot:explorer');
@@ -35,6 +36,7 @@ class Explorer {
35
36
  _activeTest = null;
36
37
  xhrCapture = null;
37
38
  requestStore = null;
39
+ playwrightRecorder = new PlaywrightRecorder();
38
40
  constructor(config, aiProvider, options) {
39
41
  this.config = config;
40
42
  this.aiProvider = aiProvider;
@@ -42,7 +44,7 @@ class Explorer {
42
44
  this.initializeContainer();
43
45
  this.stateManager = new StateManager({ incognito: this.options?.incognito });
44
46
  this.knowledgeTracker = new KnowledgeTracker();
45
- this.reporter = new Reporter(config.reporter);
47
+ this.reporter = new Reporter(config.reporter, this.stateManager);
46
48
  }
47
49
  initializeContainer() {
48
50
  try {
@@ -89,7 +91,7 @@ class Explorer {
89
91
  tag('substep').log(debugInfo);
90
92
  }
91
93
  const PlaywrightConfig = {
92
- timeout: 1000,
94
+ timeout: 3000,
93
95
  highlightElement: true,
94
96
  waitForAction: 500,
95
97
  ...playwrightConfig,
@@ -188,6 +190,7 @@ class Explorer {
188
190
  const hasSession = this.options?.session && existsSync(this.options.session);
189
191
  const contextOptions = hasSession ? { storageState: this.options.session } : undefined;
190
192
  await this.playwrightHelper._createContextPage(contextOptions);
193
+ await this.playwrightRecorder.start(this.playwrightHelper.browserContext);
191
194
  this.setupXhrCapture();
192
195
  if (hasSession) {
193
196
  tag('info').log(`Session restored from ${path.relative(process.cwd(), this.options.session)}`);
@@ -216,7 +219,10 @@ class Explorer {
216
219
  await this.playwrightHelper._startBrowser();
217
220
  }
218
221
  createAction() {
219
- return new Action(this.actor, this.stateManager);
222
+ return new Action(this.actor, this.stateManager, this.playwrightRecorder);
223
+ }
224
+ getPlaywrightRecorder() {
225
+ return this.playwrightRecorder;
220
226
  }
221
227
  async visit(url) {
222
228
  await this.closeOtherTabs();
@@ -411,6 +417,7 @@ class Explorer {
411
417
  if (this.xhrCapture && this.playwrightHelper?.page) {
412
418
  this.xhrCapture.detach(this.playwrightHelper.page);
413
419
  }
420
+ await this.playwrightRecorder.stop();
414
421
  if (this.options?.session && this.playwrightHelper?.browserContext) {
415
422
  const dir = path.dirname(this.options.session);
416
423
  if (!existsSync(dir))
@@ -1,125 +1,76 @@
1
- import { randomBytes } from 'node:crypto';
2
- import { context, trace } from '@opentelemetry/api';
1
+ import { trace } from '@opentelemetry/api';
3
2
  let current = null;
4
- let depth = 0;
5
3
  export const Observability = {
6
4
  async run(name, metadata, fn) {
7
- const started = Observability.startTrace(name, metadata);
8
- try {
9
- if (!started) {
10
- const parentSpan = current?.span;
11
- if (!parentSpan || !current)
12
- return await fn();
13
- const tracer = trace.getTracer('ai');
14
- const childSpan = tracer.startSpan(name, undefined, trace.setSpan(context.active(), parentSpan));
15
- const savedSpan = current.span;
16
- const savedName = current.name;
17
- current.span = childSpan;
18
- current.name = name;
19
- return await context.with(trace.setSpan(context.active(), childSpan), async () => {
20
- try {
21
- return await fn();
22
- }
23
- finally {
24
- childSpan.end();
25
- current.span = savedSpan;
26
- current.name = savedName;
27
- }
28
- });
29
- }
30
- const tracer = trace.getTracer('ai');
31
- const spanContext = {
32
- traceId: current?.traceId || randomBytes(16).toString('hex'),
33
- spanId: randomBytes(8).toString('hex'),
34
- traceFlags: 1,
35
- };
36
- const rootContext = trace.setSpanContext(context.active(), spanContext);
37
- const initSpan = tracer.startSpan(name, undefined, rootContext);
38
- initSpan.setAttribute('langfuse.trace.name', name);
39
- initSpan.setAttribute('langfuse.trace.id', current?.traceId || '');
40
- if (current?.metadata?.sessionId) {
41
- initSpan.setAttribute('langfuse.trace.session_id', String(current.metadata.sessionId));
42
- }
43
- if (current?.metadata?.userId) {
44
- initSpan.setAttribute('langfuse.trace.user_id', String(current.metadata.userId));
45
- }
46
- if (current?.metadata?.tags && Array.isArray(current.metadata.tags)) {
47
- initSpan.setAttribute('langfuse.trace.tags', current.metadata.tags);
48
- }
49
- if (current?.metadata?.input) {
50
- initSpan.setAttribute('langfuse.trace.input', JSON.stringify(current.metadata.input));
51
- }
52
- initSpan.end();
53
- const span = tracer.startSpan(name, undefined, rootContext);
54
- current.span = span;
55
- return await context.with(trace.setSpan(rootContext, span), async () => {
5
+ const tracer = trace.getTracer('ai');
6
+ if (current) {
7
+ return await tracer.startActiveSpan(name, {}, async (span) => {
8
+ const saved = current;
9
+ current = {
10
+ metadata: { ...saved.metadata, ...metadata },
11
+ name,
12
+ span,
13
+ };
56
14
  try {
57
15
  return await fn();
58
16
  }
59
17
  finally {
60
18
  span.end();
61
- current.span = undefined;
19
+ current = saved;
62
20
  }
63
21
  });
64
22
  }
65
- finally {
66
- Observability.endTrace(started);
67
- }
68
- },
69
- startTrace(name, metadata) {
70
- if (current) {
71
- depth += 1;
72
- return false;
73
- }
74
- const langfuseTraceId = metadata.langfuseTraceId || randomBytes(16).toString('hex');
75
- current = {
76
- metadata: {
77
- ...metadata,
78
- langfuseTraceId,
79
- },
80
- traceId: langfuseTraceId,
81
- updateParent: true,
82
- name,
83
- };
84
- depth = 1;
85
- return true;
86
- },
87
- endTrace(started) {
88
- if (!current) {
89
- return;
90
- }
91
- if (!started) {
92
- depth -= 1;
93
- return;
94
- }
95
- depth -= 1;
96
- if (depth <= 0) {
97
- current = null;
98
- depth = 0;
99
- }
23
+ const attributes = buildRootSpanAttributes(name, metadata);
24
+ return await tracer.startActiveSpan(name, { attributes }, async (span) => {
25
+ current = { metadata, name, span };
26
+ try {
27
+ return await fn();
28
+ }
29
+ finally {
30
+ span.end();
31
+ current = null;
32
+ }
33
+ });
100
34
  },
101
35
  getTelemetry() {
102
36
  if (!current) {
103
37
  return undefined;
104
38
  }
105
- const telemetry = {
39
+ const metadata = {};
40
+ if (current.metadata.sessionId)
41
+ metadata.sessionId = current.metadata.sessionId;
42
+ if (current.metadata.userId)
43
+ metadata.userId = current.metadata.userId;
44
+ if (Array.isArray(current.metadata.tags))
45
+ metadata.tags = current.metadata.tags;
46
+ return {
106
47
  isEnabled: true,
107
48
  functionId: current.name,
108
- metadata: {
109
- ...current.metadata,
110
- langfuseTraceId: current.traceId,
111
- langfuseUpdateParent: current.updateParent,
112
- },
49
+ metadata,
113
50
  };
114
- if (current.updateParent) {
115
- current.updateParent = false;
116
- }
117
- return telemetry;
118
51
  },
119
52
  isTracing() {
120
53
  return Boolean(current);
121
54
  },
122
55
  getSpan() {
123
- return current?.span;
56
+ return current?.span ?? trace.getActiveSpan();
124
57
  },
125
58
  };
59
+ function buildRootSpanAttributes(name, metadata) {
60
+ const attributes = {
61
+ 'langfuse.trace.name': name,
62
+ };
63
+ if (metadata.sessionId) {
64
+ attributes['session.id'] = String(metadata.sessionId);
65
+ }
66
+ if (metadata.userId) {
67
+ attributes['user.id'] = String(metadata.userId);
68
+ }
69
+ if (Array.isArray(metadata.tags)) {
70
+ attributes['langfuse.trace.tags'] = metadata.tags;
71
+ }
72
+ if (metadata.input !== undefined) {
73
+ attributes['langfuse.trace.input'] = JSON.stringify(metadata.input);
74
+ }
75
+ return attributes;
76
+ }