plugin-agent-orchestrator 1.0.6 → 1.0.13

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.
@@ -13,6 +13,10 @@ import {
13
13
  Tag,
14
14
  Typography,
15
15
  Alert,
16
+ Collapse,
17
+ Empty,
18
+ Input,
19
+ Select,
16
20
  } from 'antd';
17
21
  import { PlusOutlined, EditOutlined, DeleteOutlined, SwapRightOutlined } from '@ant-design/icons';
18
22
  import { useAPIClient, useRequest } from '@nocobase/client';
@@ -34,8 +38,39 @@ export const RulesTab: React.FC = () => {
34
38
  },
35
39
  });
36
40
 
41
+ const { data: llmServicesData, loading: llmLoading } = useRequest({
42
+ url: 'llmServices:list',
43
+ params: {
44
+ filter: { enabled: true },
45
+ },
46
+ });
47
+
48
+ const llmServices = React.useMemo(() => {
49
+ return (llmServicesData as any)?.data || [];
50
+ }, [llmServicesData]);
51
+
37
52
  // P3 FIX: Use shared context instead of duplicate API call
38
53
  const { employeeMap } = useAIEmployees();
54
+ const rules = React.useMemo(() => {
55
+ const rows = (data as any)?.data;
56
+ return Array.isArray(rows) ? rows : [];
57
+ }, [data]);
58
+
59
+ const groupedRules = React.useMemo(() => {
60
+ const groups = new Map<string, any[]>();
61
+ for (const rule of rules) {
62
+ const key = rule.leaderUsername || 'unknown';
63
+ if (!groups.has(key)) {
64
+ groups.set(key, []);
65
+ }
66
+ groups.get(key)!.push(rule);
67
+ }
68
+
69
+ return Array.from(groups.entries()).map(([leaderUsername, items]) => ({
70
+ leaderUsername,
71
+ items,
72
+ }));
73
+ }, [rules]);
39
74
 
40
75
  const handleOpen = (record?: any) => {
41
76
  setEditingRecord(record);
@@ -136,6 +171,24 @@ export const RulesTab: React.FC = () => {
136
171
  width: 100,
137
172
  render: (v: number) => `${((v ?? 120000) / 1000).toFixed(0)}s`,
138
173
  },
174
+ {
175
+ title: 'LLM Override',
176
+ key: 'llmOverride',
177
+ width: 140,
178
+ render: (_: any, record: any) => {
179
+ if (record.llmService && record.model) {
180
+ const svc = llmServices.find((s: any) => s.name === record.llmService);
181
+ const svcName = svc ? svc.title : record.llmService;
182
+ return (
183
+ <Space direction="vertical" size={0}>
184
+ <Text style={{ fontSize: 12 }}>{svcName}</Text>
185
+ <Text type="secondary" style={{ fontSize: 12 }}>{record.model}</Text>
186
+ </Space>
187
+ );
188
+ }
189
+ return <Text type="secondary" style={{ fontSize: 12 }}>Inherited</Text>;
190
+ },
191
+ },
139
192
  {
140
193
  title: 'Enabled',
141
194
  dataIndex: 'enabled',
@@ -199,14 +252,41 @@ export const RulesTab: React.FC = () => {
199
252
  New Rule
200
253
  </Button>
201
254
  </div>
202
- <Table
203
- rowKey="id"
204
- loading={loading}
205
- dataSource={(data as any)?.data || []}
206
- columns={columns}
207
- pagination={{ hideOnSinglePage: true, pageSize: 20 }}
208
- size="middle"
209
- />
255
+ {groupedRules.length ? (
256
+ <Collapse
257
+ bordered={false}
258
+ defaultActiveKey={groupedRules.map((group) => group.leaderUsername)}
259
+ items={groupedRules.map((group) => ({
260
+ key: group.leaderUsername,
261
+ label: (
262
+ <Space>
263
+ <Tag color="blue">{employeeMap.get(group.leaderUsername) || group.leaderUsername}</Tag>
264
+ <Text type="secondary">{group.items.length} sub-agent{group.items.length > 1 ? 's' : ''}</Text>
265
+ </Space>
266
+ ),
267
+ children: (
268
+ <Table
269
+ rowKey="id"
270
+ loading={loading}
271
+ dataSource={group.items}
272
+ columns={columns}
273
+ pagination={false}
274
+ size="middle"
275
+ />
276
+ ),
277
+ }))}
278
+ />
279
+ ) : (
280
+ <Table
281
+ rowKey="id"
282
+ loading={loading}
283
+ dataSource={[]}
284
+ columns={columns}
285
+ pagination={false}
286
+ size="middle"
287
+ locale={{ emptyText: <Empty description="No orchestration rules yet" /> }}
288
+ />
289
+ )}
210
290
  </Card>
211
291
 
212
292
  <Drawer
@@ -262,6 +342,52 @@ export const RulesTab: React.FC = () => {
262
342
  <InputNumber min={10000} max={600000} step={10000} style={{ width: '100%' }} />
263
343
  </Form.Item>
264
344
 
345
+ <Form.Item
346
+ name="llmService"
347
+ label="Override LLM Service"
348
+ tooltip="Optional: Provider name. Leave empty to inherit from Leader."
349
+ >
350
+ <Select
351
+ allowClear
352
+ placeholder="Inherit from Leader"
353
+ options={llmServices.map((svc: any) => ({
354
+ label: svc.title || svc.name,
355
+ value: svc.name,
356
+ }))}
357
+ onChange={() => form.setFieldValue('model', undefined)}
358
+ />
359
+ </Form.Item>
360
+
361
+ <Form.Item
362
+ noStyle
363
+ shouldUpdate={(prevValues, currentValues) => prevValues.llmService !== currentValues.llmService}
364
+ >
365
+ {() => {
366
+ const selectedServiceId = form.getFieldValue('llmService');
367
+ const selectedService = llmServices.find((s: any) => s.name === selectedServiceId);
368
+ const availableModels = selectedService?.enabledModels?.models || [];
369
+
370
+ return (
371
+ <Form.Item
372
+ name="model"
373
+ label="Override Model"
374
+ tooltip="Optional: Model name. Leave empty to inherit from Leader."
375
+ rules={[{ required: !!selectedServiceId, message: 'Please select a model' }]}
376
+ >
377
+ <Select
378
+ allowClear
379
+ placeholder={selectedServiceId ? 'Select a model' : 'Inherit from Leader'}
380
+ disabled={!selectedServiceId}
381
+ options={availableModels.map((m: any) => ({
382
+ label: m.label,
383
+ value: m.value,
384
+ }))}
385
+ />
386
+ </Form.Item>
387
+ );
388
+ }}
389
+ </Form.Item>
390
+
265
391
  <Form.Item name="enabled" label="Enabled" valuePropName="checked">
266
392
  <Switch />
267
393
  </Form.Item>
@@ -9,19 +9,25 @@ import {
9
9
  Alert,
10
10
  Button,
11
11
  Empty,
12
+ Space,
13
+ Timeline,
14
+ Collapse,
15
+ Spin,
12
16
  } from 'antd';
13
17
  import {
14
18
  EyeOutlined,
15
19
  CheckCircleOutlined,
16
20
  CloseCircleOutlined,
17
21
  } from '@ant-design/icons';
18
- import { useRequest } from '@nocobase/client';
22
+ import { useAPIClient, useRequest } from '@nocobase/client';
19
23
  import { useAIEmployees } from './AIEmployeesContext';
20
24
 
21
25
  const { Text, Paragraph } = Typography;
22
26
 
23
27
  export const TracingTab: React.FC = () => {
28
+ const api = useAPIClient();
24
29
  const [selectedLog, setSelectedLog] = useState<any>(null);
30
+ const [detailLoading, setDetailLoading] = useState(false);
25
31
 
26
32
  const { data, loading, refresh } = useRequest({
27
33
  url: 'orchestratorTracing:list',
@@ -32,6 +38,48 @@ export const TracingTab: React.FC = () => {
32
38
  });
33
39
 
34
40
  const { employeeMap } = useAIEmployees();
41
+ const logs = React.useMemo(() => {
42
+ let rows = (data as any)?.data;
43
+ // Handle double-wrapped NocoBase response data
44
+ if (rows && !Array.isArray(rows) && Array.isArray(rows.data)) {
45
+ rows = rows.data;
46
+ }
47
+ return Array.isArray(rows) ? rows : Array.isArray(data) ? data : [];
48
+ }, [data]);
49
+
50
+ const groupedLogs = React.useMemo(() => {
51
+ const groups = new Map<string, any[]>();
52
+ for (const log of logs) {
53
+ const key = log.leaderUsername || 'unknown';
54
+ if (!groups.has(key)) {
55
+ groups.set(key, []);
56
+ }
57
+ groups.get(key)!.push(log);
58
+ }
59
+ return Array.from(groups.entries()).map(([leaderUsername, items]) => ({
60
+ leaderUsername,
61
+ items,
62
+ }));
63
+ }, [logs]);
64
+
65
+ const formatDuration = (ms: number) => {
66
+ if (!ms) return '-';
67
+ return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
68
+ };
69
+
70
+ const handleOpenLog = async (record: any) => {
71
+ setSelectedLog(record);
72
+ setDetailLoading(true);
73
+ try {
74
+ const res = await api.request({
75
+ url: 'orchestratorTracing:get',
76
+ params: { filterByTk: record.id },
77
+ });
78
+ setSelectedLog((res as any)?.data?.data || (res as any)?.data || record);
79
+ } finally {
80
+ setDetailLoading(false);
81
+ }
82
+ };
35
83
 
36
84
  const columns = [
37
85
  {
@@ -41,6 +89,14 @@ export const TracingTab: React.FC = () => {
41
89
  width: 170,
42
90
  render: (v: string) => v ? new Date(v).toLocaleString() : '-',
43
91
  },
92
+ {
93
+ title: 'Leader',
94
+ dataIndex: 'leaderUsername',
95
+ key: 'leaderUsername',
96
+ render: (username: string) => (
97
+ <Tag color="blue">{employeeMap.get(username) || username}</Tag>
98
+ ),
99
+ },
44
100
  {
45
101
  title: 'Sub-Agent',
46
102
  dataIndex: 'subAgentUsername',
@@ -78,10 +134,7 @@ export const TracingTab: React.FC = () => {
78
134
  dataIndex: 'durationMs',
79
135
  key: 'durationMs',
80
136
  width: 90,
81
- render: (ms: number) => {
82
- if (!ms) return '-';
83
- return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
84
- },
137
+ render: formatDuration,
85
138
  },
86
139
  {
87
140
  title: 'Depth',
@@ -99,7 +152,7 @@ export const TracingTab: React.FC = () => {
99
152
  type="link"
100
153
  size="small"
101
154
  icon={<EyeOutlined />}
102
- onClick={() => setSelectedLog(record)}
155
+ onClick={() => handleOpenLog(record)}
103
156
  >
104
157
  Detail
105
158
  </Button>
@@ -126,24 +179,51 @@ export const TracingTab: React.FC = () => {
126
179
  <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
127
180
  <Button onClick={refresh}>Refresh</Button>
128
181
  </div>
129
- <Table
130
- rowKey="id"
131
- loading={loading}
132
- dataSource={Array.isArray((data as any)?.data) ? (data as any).data : (Array.isArray(data) ? data : [])}
133
- columns={columns}
134
- pagination={{ hideOnSinglePage: true, pageSize: 20 }}
135
- size="middle"
136
- locale={{ emptyText: <Empty description="No delegation executions yet" /> }}
137
- />
182
+ {groupedLogs.length ? (
183
+ <Collapse
184
+ bordered={false}
185
+ defaultActiveKey={groupedLogs.map((group) => group.leaderUsername)}
186
+ items={groupedLogs.map((group) => ({
187
+ key: group.leaderUsername,
188
+ label: (
189
+ <Space>
190
+ <Tag color="blue">{employeeMap.get(group.leaderUsername) || group.leaderUsername}</Tag>
191
+ <Text type="secondary">{group.items.length} execution{group.items.length > 1 ? 's' : ''}</Text>
192
+ </Space>
193
+ ),
194
+ children: (
195
+ <Table
196
+ rowKey="id"
197
+ loading={loading}
198
+ dataSource={group.items}
199
+ columns={columns}
200
+ pagination={{ hideOnSinglePage: true, pageSize: 20 }}
201
+ size="middle"
202
+ />
203
+ ),
204
+ }))}
205
+ />
206
+ ) : (
207
+ <Table
208
+ rowKey="id"
209
+ loading={loading}
210
+ dataSource={[]}
211
+ columns={columns}
212
+ pagination={false}
213
+ size="middle"
214
+ locale={{ emptyText: <Empty description="No delegation executions yet" /> }}
215
+ />
216
+ )}
138
217
  </Card>
139
218
 
140
219
  <Drawer
141
220
  title="Delegation Detail"
142
- width={640}
221
+ width={760}
143
222
  onClose={() => setSelectedLog(null)}
144
223
  open={!!selectedLog}
145
224
  >
146
225
  {selectedLog && (
226
+ <Spin spinning={detailLoading}>
147
227
  <>
148
228
  <Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
149
229
  <Descriptions.Item label="Status">
@@ -154,6 +234,11 @@ export const TracingTab: React.FC = () => {
154
234
  {selectedLog.status}
155
235
  </Tag>
156
236
  </Descriptions.Item>
237
+ <Descriptions.Item label="Leader">
238
+ <Tag color="blue">
239
+ {employeeMap.get(selectedLog.leaderUsername) || selectedLog.leaderUsername}
240
+ </Tag>
241
+ </Descriptions.Item>
157
242
  <Descriptions.Item label="Sub-Agent">
158
243
  <Tag color="green">
159
244
  {employeeMap.get(selectedLog.subAgentUsername) || selectedLog.subAgentUsername}
@@ -166,11 +251,7 @@ export const TracingTab: React.FC = () => {
166
251
  {selectedLog.depth ?? 0}
167
252
  </Descriptions.Item>
168
253
  <Descriptions.Item label="Duration">
169
- {selectedLog.durationMs
170
- ? selectedLog.durationMs >= 1000
171
- ? `${(selectedLog.durationMs / 1000).toFixed(1)}s`
172
- : `${selectedLog.durationMs}ms`
173
- : '-'}
254
+ {formatDuration(selectedLog.durationMs)}
174
255
  </Descriptions.Item>
175
256
  <Descriptions.Item label="Time">
176
257
  {selectedLog.createdAt ? new Date(selectedLog.createdAt).toLocaleString() : '-'}
@@ -183,6 +264,74 @@ export const TracingTab: React.FC = () => {
183
264
  </Paragraph>
184
265
  </Card>
185
266
 
267
+ {selectedLog.context && (
268
+ <Card title="Context" size="small" style={{ marginBottom: 16 }}>
269
+ <Paragraph style={{ whiteSpace: 'pre-wrap', margin: 0, fontSize: 13 }}>
270
+ {selectedLog.context}
271
+ </Paragraph>
272
+ </Card>
273
+ )}
274
+
275
+ <Card title="Sub-Agent Flow" size="small" style={{ marginBottom: 16 }}>
276
+ {Array.isArray(selectedLog.trace) && selectedLog.trace.length ? (
277
+ <Timeline
278
+ items={selectedLog.trace.map((item: any, index: number) => ({
279
+ key: index,
280
+ color: item.status === 'error' ? 'red' : item.type === 'tool_call' ? 'blue' : 'green',
281
+ children: (
282
+ <div>
283
+ <Space direction="vertical" size={2} style={{ width: '100%' }}>
284
+ <Text strong>{item.title || item.type}</Text>
285
+ <Text type="secondary">{item.at ? new Date(item.at).toLocaleString() : ''}</Text>
286
+ {item.toolName && <Text code>{item.toolName}</Text>}
287
+ {item.content && (
288
+ <Paragraph style={{ whiteSpace: 'pre-wrap', margin: 0, fontSize: 13 }}>
289
+ {item.content}
290
+ </Paragraph>
291
+ )}
292
+ {item.args && (
293
+ <Paragraph style={{ whiteSpace: 'pre-wrap', margin: 0, fontSize: 12 }}>
294
+ {JSON.stringify(item.args, null, 2)}
295
+ </Paragraph>
296
+ )}
297
+ </Space>
298
+ </div>
299
+ ),
300
+ }))}
301
+ />
302
+ ) : (
303
+ <Empty description="No flow trace captured" />
304
+ )}
305
+ </Card>
306
+
307
+ {Array.isArray(selectedLog.messages) && selectedLog.messages.length > 0 && (
308
+ <Collapse
309
+ style={{ marginBottom: 16 }}
310
+ items={[
311
+ {
312
+ key: 'messages',
313
+ label: `Raw messages (${selectedLog.messages.length})`,
314
+ children: (
315
+ <Space direction="vertical" style={{ width: '100%' }}>
316
+ {selectedLog.messages.map((message: any) => (
317
+ <Card key={message.index} size="small" title={`${message.index + 1}. ${message.type}`}>
318
+ <Paragraph style={{ whiteSpace: 'pre-wrap', margin: 0, fontSize: 12 }}>
319
+ {message.content || JSON.stringify(message.toolCalls || message, null, 2)}
320
+ </Paragraph>
321
+ {message.toolCalls?.length > 0 && (
322
+ <Paragraph style={{ whiteSpace: 'pre-wrap', margin: '8px 0 0', fontSize: 12 }}>
323
+ {JSON.stringify(message.toolCalls, null, 2)}
324
+ </Paragraph>
325
+ )}
326
+ </Card>
327
+ ))}
328
+ </Space>
329
+ ),
330
+ },
331
+ ]}
332
+ />
333
+ )}
334
+
186
335
  <Card
187
336
  title="Result"
188
337
  size="small"
@@ -210,6 +359,7 @@ export const TracingTab: React.FC = () => {
210
359
  </Card>
211
360
  )}
212
361
  </>
362
+ </Spin>
213
363
  )}
214
364
  </Drawer>
215
365
  </div>
@@ -39,6 +39,16 @@ export default defineCollection({
39
39
  defaultValue: 120000,
40
40
  comment: 'Timeout in ms for sub-agent execution',
41
41
  },
42
+ {
43
+ name: 'llmService',
44
+ type: 'string',
45
+ comment: 'Optional overridden LLM provider for the sub-agent',
46
+ },
47
+ {
48
+ name: 'model',
49
+ type: 'string',
50
+ comment: 'Optional overridden model for the sub-agent',
51
+ },
42
52
  ],
43
53
  indexes: [
44
54
  {
@@ -31,7 +31,12 @@ export default defineCollection({
31
31
  {
32
32
  name: 'toolName',
33
33
  type: 'string',
34
- comment: 'The tool name used for delegation (e.g., delegate_to_sql_expert)',
34
+ comment: 'The tool name used for delegation (e.g., delegate_pm_to_sql_expert)',
35
+ },
36
+ {
37
+ name: 'context',
38
+ type: 'text',
39
+ comment: 'Optional context sent with the delegated task',
35
40
  },
36
41
  {
37
42
  name: 'task',
@@ -46,7 +51,7 @@ export default defineCollection({
46
51
  {
47
52
  name: 'status',
48
53
  type: 'string',
49
- comment: 'success or error',
54
+ comment: 'running, success, or error',
50
55
  },
51
56
  {
52
57
  name: 'depth',
@@ -64,6 +69,18 @@ export default defineCollection({
64
69
  type: 'text',
65
70
  comment: 'Error message if status is error',
66
71
  },
72
+ {
73
+ name: 'trace',
74
+ type: 'json',
75
+ defaultValue: [],
76
+ comment: 'Structured timeline of sub-agent execution and tool calls',
77
+ },
78
+ {
79
+ name: 'messages',
80
+ type: 'json',
81
+ defaultValue: [],
82
+ comment: 'Serialized LangChain messages from the sub-agent run',
83
+ },
67
84
  {
68
85
  name: 'userId',
69
86
  type: 'bigInt',
@@ -0,0 +1,41 @@
1
+ import { Migration } from '@nocobase/server';
2
+
3
+ export default class AddTracingDetailFieldsMigration extends Migration {
4
+ on = 'afterLoad';
5
+ appVersion = '<=2.x';
6
+
7
+ async up() {
8
+ const queryInterface = this.db.sequelize.getQueryInterface();
9
+ const DataTypes = this.db.sequelize.constructor['DataTypes'];
10
+ const tableName = `${this.db.options.tablePrefix || ''}orchestratorLogs`;
11
+
12
+ const tableExists = await queryInterface
13
+ .describeTable(tableName)
14
+ .then(() => true)
15
+ .catch(() => false);
16
+
17
+ if (!tableExists) {
18
+ return;
19
+ }
20
+
21
+ const columns = await queryInterface.describeTable(tableName);
22
+ const addIfMissing = async (name: string, definition: any) => {
23
+ if (!columns[name]) {
24
+ await queryInterface.addColumn(tableName, name, definition);
25
+ }
26
+ };
27
+
28
+ await addIfMissing('context', { type: DataTypes.TEXT, allowNull: true });
29
+ await addIfMissing('trace', { type: DataTypes.JSON, allowNull: true, defaultValue: [] });
30
+ await addIfMissing('messages', { type: DataTypes.JSON, allowNull: true, defaultValue: [] });
31
+ }
32
+
33
+ async down() {
34
+ const queryInterface = this.db.sequelize.getQueryInterface();
35
+ const tableName = `${this.db.options.tablePrefix || ''}orchestratorLogs`;
36
+
37
+ for (const column of ['context', 'trace', 'messages']) {
38
+ await queryInterface.removeColumn(tableName, column).catch(() => {});
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,37 @@
1
+ import { Migration } from '@nocobase/server';
2
+
3
+ export default class AddLlmFieldsToOrchestratorConfig extends Migration {
4
+ on = 'afterLoad';
5
+ appVersion = '>=0.1.0';
6
+
7
+ async up() {
8
+ const queryInterface = this.db.sequelize.getQueryInterface();
9
+ const tablePrefix = this.db.options.tablePrefix || '';
10
+ const tableName = `${tablePrefix}orchestratorConfig`;
11
+
12
+ const tableExists = await queryInterface.tableExists(tableName);
13
+ if (!tableExists) return;
14
+
15
+ const tableDesc = await queryInterface.describeTable(tableName);
16
+
17
+ if (!tableDesc['llmService']) {
18
+ await queryInterface.addColumn(tableName, 'llmService', {
19
+ type: 'VARCHAR(255)',
20
+ allowNull: true,
21
+ });
22
+ console.log(`[AgentOrchestrator] Added llmService column to ${tableName}`);
23
+ }
24
+
25
+ if (!tableDesc['model']) {
26
+ await queryInterface.addColumn(tableName, 'model', {
27
+ type: 'VARCHAR(255)',
28
+ allowNull: true,
29
+ });
30
+ console.log(`[AgentOrchestrator] Added model column to ${tableName}`);
31
+ }
32
+ }
33
+
34
+ async down() {
35
+ // No rollback
36
+ }
37
+ }
@@ -16,10 +16,11 @@ export function registerTracingResource(plugin: Plugin) {
16
16
  */
17
17
  async list(ctx, next) {
18
18
  const repo = ctx.db.getRepository('orchestratorLogs');
19
- const { page = 1, pageSize = 50, sort = ['-createdAt'] } = ctx.action.params;
19
+ const { page = 1, pageSize = 50, sort = ['-createdAt'], filter = {} } = ctx.action.params;
20
20
 
21
21
  try {
22
22
  const [rows, count] = await repo.findAndCount({
23
+ filter,
23
24
  sort,
24
25
  offset: (Number(page) - 1) * Number(pageSize),
25
26
  limit: Number(pageSize),
@@ -32,6 +33,7 @@ export function registerTracingResource(plugin: Plugin) {
32
33
  subAgentUsername: row.subAgentUsername,
33
34
  toolName: row.toolName,
34
35
  task: row.task,
36
+ context: row.context,
35
37
  result: row.result,
36
38
  status: row.status,
37
39
  depth: row.depth,
@@ -39,6 +41,8 @@ export function registerTracingResource(plugin: Plugin) {
39
41
  error: row.error,
40
42
  userId: row.userId,
41
43
  createdAt: row.createdAt,
44
+ traceCount: Array.isArray(row.trace) ? row.trace.length : 0,
45
+ messageCount: Array.isArray(row.messages) ? row.messages.length : 0,
42
46
  })),
43
47
  meta: {
44
48
  count,
@@ -72,7 +76,7 @@ export function registerTracingResource(plugin: Plugin) {
72
76
  filter: { id: filterByTk },
73
77
  });
74
78
 
75
- ctx.body = { data: log };
79
+ ctx.body = { data: log?.toJSON?.() || log || null };
76
80
  } catch (e) {
77
81
  ctx.log.error('[AgentOrchestrator] Tracing get error', e);
78
82
  ctx.body = { data: null };