@cap-js/agents 0.9.2 → 0.9.3

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 (42) hide show
  1. package/README.md +2 -0
  2. package/cds-plugin.js +2 -2
  3. package/lib/agents/middleware/content-filter.js +3 -2
  4. package/lib/agents/middleware/index.js +11 -0
  5. package/lib/agents/middleware/remote-mcp.js +62 -0
  6. package/lib/agents/middleware/tool-wrap.js +52 -0
  7. package/lib/compile.js +6 -2
  8. package/lib/eval/Judge.js +239 -0
  9. package/lib/eval/eval-describe.js +69 -0
  10. package/lib/eval/eval-run.js +147 -0
  11. package/lib/eval/index.js +6 -0
  12. package/lib/eval/metrics.js +53 -0
  13. package/lib/eval/span-collector.js +50 -0
  14. package/lib/index.js +2 -1
  15. package/lib/models/aicore.js +23 -6
  16. package/lib/preview/chat.html +368 -65
  17. package/lib/protocol/agent-card.js +6 -2
  18. package/lib/sidecar.js +1 -1
  19. package/lib/telemetry/chat-tracing.js +39 -8
  20. package/lib/telemetry/mlflow/credentials.js +79 -0
  21. package/lib/telemetry/mlflow/evaluation.js +38 -0
  22. package/lib/telemetry/mlflow/exporter/DatabricksExporter.js +75 -0
  23. package/lib/telemetry/mlflow/exporter/MlflowExporter.js +115 -0
  24. package/lib/telemetry/mlflow/exporter/index.js +23 -0
  25. package/lib/telemetry/mlflow/index.js +16 -0
  26. package/lib/telemetry/mlflow/prompts.js +123 -0
  27. package/lib/telemetry/mlflow/tracing.js +264 -0
  28. package/lib/telemetry/tool-tracing.js +27 -54
  29. package/lib/telemetry/tracing.js +3 -1
  30. package/lib/utils/markdown.js +1 -11
  31. package/lib/utils/resilience.js +133 -0
  32. package/lib/utils/utils.js +17 -0
  33. package/package.json +23 -14
  34. package/srv/handlers/chat.js +278 -0
  35. package/srv/handlers/graph-executor.js +43 -37
  36. package/srv/handlers/index.js +3 -0
  37. package/srv/handlers/mcp-tools.js +7 -45
  38. package/srv/handlers/sub-agent-tools.js +8 -1
  39. package/srv/handlers/tools.js +9 -5
  40. package/index.js +0 -0
  41. package/lib/telemetry/mlflow.js +0 -290
  42. /package/{index.cds → srv/entities.cds} +0 -0
@@ -0,0 +1,278 @@
1
+ import cds from "@sap/cds"
2
+ import { startCollection } from "../../lib/eval/span-collector.js"
3
+ import { metricsFromSpans } from "../../lib/eval/metrics.js"
4
+ import { getActiveRunState, logMlflowMetricsForResult } from "../../lib/eval/eval-run.js"
5
+
6
+ export const COLLECT_RESULT = Symbol.for("@cap-js/agents:chat:collect-result")
7
+
8
+ class NoopEventBus {
9
+ constructor() {
10
+ this.events = []
11
+ this[COLLECT_RESULT] = true
12
+ this._graphResult = null
13
+ this._done = false
14
+ this.finished$ = new Promise((resolve, reject) => {
15
+ this._resolve = resolve
16
+ this._reject = reject
17
+ })
18
+ }
19
+
20
+ publish(event) {
21
+ this.events.push(event)
22
+ }
23
+
24
+ finished() {
25
+ this._done = true
26
+ this._resolve?.()
27
+ }
28
+
29
+ error(err) {
30
+ this._reject?.(err)
31
+ }
32
+
33
+ getFinalText() {
34
+ for (let i = this.events.length - 1; i >= 0; i--) {
35
+ const e = this.events[i]
36
+ if (e.kind === "artifact-update" && e.lastChunk === true) {
37
+ const text = e.artifact?.parts
38
+ ?.filter((p) => p.kind === "text")
39
+ .map((p) => p.text)
40
+ .join("")
41
+ if (text) return text
42
+ }
43
+ }
44
+ for (let i = this.events.length - 1; i >= 0; i--) {
45
+ const e = this.events[i]
46
+ if (e.kind === "status-update" && e.status?.message?.parts) {
47
+ const text = e.status.message.parts
48
+ .filter((p) => p.kind === "text")
49
+ .map((p) => p.text)
50
+ .join("")
51
+ if (text) return text
52
+ }
53
+ }
54
+ return ""
55
+ }
56
+
57
+ getStatus() {
58
+ const e = this.events.at(-1)
59
+ if (e && e.kind === "status-update" && e.status?.state) {
60
+ const state = e.status.state
61
+ const msg = e.status?.message?.parts?.find((p) => p.kind === "text")?.text ?? ""
62
+ return { status: state, description: msg }
63
+ }
64
+ return { status: "completed", description: "" }
65
+ }
66
+ }
67
+
68
+ // Derive toolCalls from graph messages by pairing AIMessage.tool_calls with ToolMessage results.
69
+ function toolCallsFromMessages(messages) {
70
+ if (!Array.isArray(messages) || messages.length === 0) return []
71
+
72
+ const resultById = new Map()
73
+ for (const msg of messages) {
74
+ if (msg.tool_call_id && msg.type === "tool") {
75
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
76
+ resultById.set(msg.tool_call_id, content)
77
+ }
78
+ }
79
+
80
+ const entries = []
81
+ for (const msg of messages) {
82
+ if (!Array.isArray(msg.tool_calls) || msg.tool_calls.length === 0) continue
83
+ for (const tc of msg.tool_calls) {
84
+ const raw = resultById.get(tc.id)
85
+ let toolResult
86
+ try {
87
+ toolResult = raw !== undefined ? JSON.parse(raw) : undefined
88
+ } catch {
89
+ toolResult = raw
90
+ }
91
+ const entry = {
92
+ tool: tc.name,
93
+ args: tc.args ?? {},
94
+ outcome: "success",
95
+ ...(toolResult !== undefined && { result: toolResult }),
96
+ }
97
+ attachCqn(entry)
98
+ entries.push(entry)
99
+ }
100
+ }
101
+ return entries
102
+ }
103
+
104
+ /**
105
+ * If entry.args.cql attaches cqn as hidden property.
106
+ * No-op when cql is absent or faulty.
107
+ */
108
+ function attachCqn(entry) {
109
+ const cql = entry.args?.cql
110
+ if (typeof cql !== "string") return
111
+ try {
112
+ const cqn = cds.parse.cql(cql)
113
+ Object.defineProperty(entry, "cqn", {
114
+ value: cqn,
115
+ enumerable: false,
116
+ writable: false,
117
+ configurable: true,
118
+ })
119
+ } catch {
120
+ /* unparseable CQL — skip */
121
+ }
122
+ }
123
+
124
+ function buildRequestContext(query, opts = {}) {
125
+ const taskId = opts.taskId || cds.utils.uuid()
126
+ const contextId = opts.contextId || cds.utils.uuid()
127
+ const parts =
128
+ opts.parts ??
129
+ (typeof query === "string"
130
+ ? [{ kind: "text", text: query }]
131
+ : (query?.parts ?? [{ kind: "text", text: String(query) }]))
132
+ return {
133
+ taskId,
134
+ contextId,
135
+ task: opts.task ?? null,
136
+ userMessage: {
137
+ kind: "message",
138
+ messageId: cds.utils.uuid(),
139
+ role: "user",
140
+ taskId,
141
+ contextId,
142
+ parts,
143
+ },
144
+ }
145
+ }
146
+
147
+ /** Install srv.chat(query, previous?) on an @agent service for eval tests. */
148
+ export function registerChat(srv) {
149
+ srv.chat = async function chat(query, previous) {
150
+ // Resolve the small eval helper API:
151
+ // chat(query)
152
+ // chat(query, prevResult) — extract contextId + taskId for HITL resume
153
+ // chat(query, { _details: true }) — internal/test escape hatch outside test profile
154
+ let opts = {}
155
+ if (typeof previous === "string") {
156
+ throw new TypeError(
157
+ "agent.chat: second argument must be a previous chat result object or options object",
158
+ )
159
+ } else if (previous && typeof previous === "object") {
160
+ if ("text" in previous || "contextId" in previous) {
161
+ // prior chat() result — extract conversation ids and HITL state
162
+ opts = {
163
+ contextId: previous.contextId,
164
+ // mark as resume when prior result was input-required
165
+ ...(previous.status === "input-required" && {
166
+ taskId: previous.taskId,
167
+ task: { id: previous.taskId, status: { state: "input-required" } },
168
+ }),
169
+ }
170
+ } else {
171
+ opts = { _details: previous._details === true }
172
+ }
173
+ }
174
+
175
+ const includeDetails = shouldIncludeChatDetails(opts)
176
+ const runState = includeDetails ? getActiveRunState() : null
177
+
178
+ // Open span collection session before execution so the processor
179
+ // captures all spans from this trace.
180
+ const collection = includeDetails ? await startCollection() : null
181
+
182
+ const { LangGraphExecutor } = await import("../langgraph-executor-srv.js")
183
+ const executor = LangGraphExecutor.for(srv)
184
+ const requestContext = buildRequestContext(query, opts)
185
+ const eventBus = new NoopEventBus()
186
+ const mlflowRunId = runState?.mlflowRunId
187
+ let traceId
188
+
189
+ const runInContext = async () => {
190
+ // Link the trace to the MLflow eval run — graph-executor reads this to set mlflow.sourceRun.
191
+ if (mlflowRunId && cds.context) cds.context["_mlflow.evalRunId"] = mlflowRunId
192
+
193
+ let execError = null
194
+ const execPromise = executor.execute(requestContext, eventBus).catch((err) => {
195
+ execError = err
196
+ if (!eventBus._done) eventBus.finished()
197
+ })
198
+ await Promise.race([eventBus.finished$, execPromise])
199
+ if (!eventBus._done) eventBus.finished()
200
+ if (execError) throw execError
201
+
202
+ if (includeDetails) {
203
+ const rootSpan = cds.context?.["_mlflow.rootSpan"]
204
+ if (rootSpan) traceId = rootSpan.spanContext?.()?.traceId
205
+ }
206
+ }
207
+
208
+ if (cds.context) {
209
+ await runInContext()
210
+ } else {
211
+ await cds._with(
212
+ new cds.EventContext({ tenant: "t0", user: new cds.User.Privileged() }),
213
+ runInContext,
214
+ )
215
+ }
216
+
217
+ const { status, description } = eventBus.getStatus()
218
+ if (status === "failed")
219
+ throw new Error(`agent.chat: task failed — ${description || "no message"}`)
220
+
221
+ const text = eventBus.getFinalText()
222
+ const result = {
223
+ text,
224
+ contextId: requestContext.contextId,
225
+ taskId: requestContext.taskId,
226
+ status,
227
+ }
228
+
229
+ if (includeDetails) {
230
+ const allMessages = eventBus._graphResult?.messages ?? []
231
+
232
+ // Find last HumanMessage whose text matches current query; return from there.
233
+ const queryText = String(query)
234
+ let turnStart = 0
235
+ for (let i = allMessages.length - 1; i >= 0; i--) {
236
+ const m = allMessages[i]
237
+ if (m.getType?.() === "human" || m._getType?.() === "human" || m.type === "human") {
238
+ const content = typeof m.content === "string" ? m.content : (m.content?.[0]?.text ?? "")
239
+ if (content.startsWith(queryText) || queryText.startsWith(content.trim())) {
240
+ turnStart = i
241
+ break
242
+ }
243
+ }
244
+ }
245
+ const messages = allMessages.slice(turnStart)
246
+ const toolCalls = toolCallsFromMessages(messages)
247
+
248
+ const allSpans = collection.collect()
249
+ const spans = traceId
250
+ ? allSpans.filter((s) => s.spanContext?.()?.traceId === traceId)
251
+ : allSpans
252
+
253
+ result.query = String(query)
254
+ result.traceId = traceId
255
+ result.toolCalls = toolCalls
256
+ result.messages = messages.map((m) => {
257
+ // Eval prompts expect text content, not content-block objects.
258
+ if (m.type === "ai") {
259
+ if (Array.isArray(m.content) && m.content[0]?.text) {
260
+ m.content = m.content[0].text
261
+ }
262
+ }
263
+ return m
264
+ })
265
+ result.spans = spans
266
+ result.metrics = metricsFromSpans(spans)
267
+ if (runState) result._evalState = runState
268
+ // Post metrics to MLflow ootb — fire-and-forget.
269
+ logMlflowMetricsForResult(result, runState).catch(() => {})
270
+ }
271
+
272
+ return result
273
+ }
274
+ }
275
+
276
+ export function shouldIncludeChatDetails(opts = {}) {
277
+ return cds.env.profiles?.includes("test") || opts._details === true
278
+ }
@@ -2,11 +2,13 @@ import cds from "@sap/cds"
2
2
  import { short, audit, ms4 } from "../../lib/utils/utils.js"
3
3
  import { partsToText, buildChatMessages, firstDataPart } from "../../lib/utils/message-handling.js"
4
4
  import * as metrics from "../../lib/telemetry/metrics.js"
5
- import { mlflowAttrs, mlflowTraceAttrs, setSpanAttrs } from "../../lib/telemetry/mlflow.js"
5
+ import { mlflowAttrs, mlflowTraceAttrs, setSpanAttrs } from "../../lib/telemetry/mlflow/index.js"
6
6
  import { CdsFileStore } from "../../lib/protocol/persistence/file-store.js"
7
7
  import { formatFileSize, sanitizeFilename } from "./tools.js"
8
8
  import { convertUsageData } from "../../lib/telemetry/chat-tracing.js"
9
9
  import { triggerCleanup } from "../../lib/protocol/persistence/cleanup.js"
10
+ import { COLLECT_RESULT } from "./chat.js"
11
+ import { linkTraceToPrompt } from "../../lib/telemetry/mlflow/tracing.js"
10
12
 
11
13
  const LOG = cds.log("agents")
12
14
 
@@ -334,16 +336,14 @@ class GraphExecutor {
334
336
 
335
337
  let tokenCount = 0
336
338
  let finalState = null
337
- // Track the current turn (langchain message id) and whether it has emitted a
338
- // tool call. Anthropic-style turns can stream a text preamble BEFORE their
339
- // tool_use block ("Let me first look up …"); we can't tell in advance that
340
- // such a turn is planning rather than the final answer, so we stream those
341
- // tokens optimistically. Once we see tool_call_chunks for the same turn, we
342
- // know retrospectively that the preamble was planning — emit an authoritative
343
- // event-level replace with empty text to wipe the leaked preamble, then skip
344
- // all further text from this turn.
339
+ // Track the current turn (langchain message id). Each new turn opens a fresh
340
+ // bubble on the client via `append:false`; subsequent tokens of the same turn
341
+ // are `append:true` (accumulate). Anthropic-style turns can stream a text
342
+ // preamble BEFORE their tool_use block ("Let me first look up …") — we let
343
+ // that reasoning text stream normally; the client is responsible for the
344
+ // final visual (collapse to the last turn's bubble at task completion).
345
345
  let currentMsgId = null
346
- let turnHasToolCall = false
346
+ let thinkingCount = 0
347
347
 
348
348
  try {
349
349
  if (typeof graph.stream !== "function" || cds.env.agents?.streaming === false) {
@@ -379,30 +379,11 @@ class GraphExecutor {
379
379
 
380
380
  if (msgChunk.id && msgChunk.id !== currentMsgId) {
381
381
  currentMsgId = msgChunk.id
382
- turnHasToolCall = false
383
382
  tokenCount = 0
384
383
  }
385
384
 
386
- // Retroactively invalidate a leaked planning preamble. In a ReAct loop
387
- // the model can emit "Let me look this up …" before its tool_use block
388
- if (msgChunk.tool_call_chunks?.length && !turnHasToolCall) {
389
- turnHasToolCall = true
390
- if (tokenCount > 0) {
391
- eventBus.publish({
392
- kind: "artifact-update",
393
- taskId,
394
- contextId,
395
- append: false,
396
- lastChunk: false,
397
- artifact: {
398
- artifactId: "response",
399
- parts: [{ kind: "text", text: "" }],
400
- },
401
- })
402
- tokenCount = 0
403
- }
404
- }
405
- if (turnHasToolCall) continue
385
+ const lastChunk =
386
+ !!msgChunk.additional_kwargs?.intermediate_results?.llm?.choices[0].finish_reason
406
387
 
407
388
  const text = messageText(msgChunk?.content)
408
389
  if (!text) continue
@@ -415,12 +396,13 @@ class GraphExecutor {
415
396
  taskId,
416
397
  contextId,
417
398
  append: tokenCount > 0,
418
- lastChunk: false,
399
+ lastChunk: lastChunk,
419
400
  artifact: {
420
- artifactId: "response",
401
+ artifactId: `thinking-${thinkingCount}`,
421
402
  parts: [{ kind: "text", text }],
422
403
  },
423
404
  })
405
+ if (lastChunk) thinkingCount++
424
406
  tokenCount++
425
407
  } else if (mode === "updates") {
426
408
  // The updates stream yields per-node deltas — { <node>: { messages: [oneNewMessage] } },
@@ -699,6 +681,12 @@ class GraphExecutor {
699
681
  }),
700
682
  )
701
683
  setSpanAttrs(rootSpan, mlflowTraceAttrs())
684
+ // Required for MLFLow run linking
685
+ const evalRunId = cds.context?.["_mlflow.evalRunId"]
686
+ if (evalRunId) {
687
+ rootSpan.setAttribute("mlflow.sourceRun", evalRunId)
688
+ wfSpan.setAttribute("mlflow.sourceRun", evalRunId)
689
+ }
702
690
  }
703
691
 
704
692
  let usageData
@@ -753,7 +741,6 @@ class GraphExecutor {
753
741
  contextId,
754
742
  service: serviceName,
755
743
  decision,
756
- userMessage: requestContext.userMessage,
757
744
  },
758
745
  })
759
746
  // On edit, stash a diff note in state; the injector middleware prepends it next turn.
@@ -823,7 +810,7 @@ class GraphExecutor {
823
810
  contextId,
824
811
  service: serviceName,
825
812
  description,
826
- userMessage: requestContext.userMessage,
813
+ interruptData,
827
814
  },
828
815
  })
829
816
 
@@ -878,8 +865,7 @@ class GraphExecutor {
878
865
  service: serviceName,
879
866
  duration,
880
867
  tokenUsage: usageData,
881
- toolCalls: totalToolCalls(result.messages),
882
- output: output?.slice(0, 2000),
868
+ toolCalls: toolCallsShortened(result.messages),
883
869
  task: requestContext.task,
884
870
  },
885
871
  })
@@ -1078,6 +1064,12 @@ class GraphExecutor {
1078
1064
  })
1079
1065
  }
1080
1066
 
1067
+ // Programmatic .chat() path: stash graph result on eventBus so chat.js
1068
+ // can read messages without an extra checkpoint roundtrip.
1069
+ if (eventBus[COLLECT_RESULT]) {
1070
+ eventBus._graphResult = { messages: result.messages || [] }
1071
+ }
1072
+
1081
1073
  eventBus.publish({
1082
1074
  kind: "status-update",
1083
1075
  taskId,
@@ -1245,6 +1237,10 @@ class GraphExecutor {
1245
1237
  final: true,
1246
1238
  })
1247
1239
  } finally {
1240
+ // setSpanAttrs must happen at the end for the linking as else prompt might not yet have been created
1241
+ const rootSpan = cds.context["_mlflow.rootSpan"]
1242
+ setSpanAttrs(rootSpan, linkTraceToPrompt())
1243
+
1248
1244
  this._abortControllers.delete(taskId)
1249
1245
  metrics.concurrentExecutions.add(-1, mAttrs)
1250
1246
 
@@ -1372,3 +1368,13 @@ function totalToolCalls(messages) {
1372
1368
  return acc
1373
1369
  }, 0)
1374
1370
  }
1371
+
1372
+ /**
1373
+ * @param {[import('@langchain/core/messages').Message]} messages
1374
+ */
1375
+ function toolCallsShortened(messages) {
1376
+ return messages.reduce((acc, val) => {
1377
+ if (val.type === "tool") acc.push(val.name)
1378
+ return acc
1379
+ }, [])
1380
+ }
@@ -4,6 +4,7 @@ import { buildSystemPrompt } from "./system-prompt.js"
4
4
  import buildMiddleware from "../../lib/agents/middleware/index.js"
5
5
  import { partsToText } from "../../lib/utils/message-handling.js"
6
6
  import { cleanupExpiredTasks } from "../../lib/protocol/persistence/cleanup.js"
7
+ import { registerChat } from "./chat.js"
7
8
 
8
9
  const LOG = cds.log("agents")
9
10
 
@@ -175,4 +176,6 @@ export default function registerDefaultAgentHandlers(srv) {
175
176
  srv.on("cleanupTasks", async () => {
176
177
  await cleanupExpiredTasks(srv.name)
177
178
  })
179
+
180
+ registerChat(srv)
178
181
  }
@@ -1,5 +1,4 @@
1
1
  import cds from "@sap/cds"
2
- import { MultiServerMCPClient } from "@langchain/mcp-adapters"
3
2
  import { generateTools } from "./tools.js"
4
3
  import { toolName } from "../../lib/utils/utils.js"
5
4
 
@@ -42,48 +41,26 @@ async function resolveDestination(destinationName, dest, localUrl) {
42
41
  }
43
42
  }
44
43
 
45
- /**
46
- * Wrap MCP tool invocations to convert errors into plain string results.
47
- * deepagents' wrapToolCall middleware marks errors thrown inside it as
48
- * "middleware errors", which LangChain's ToolNode re-throws rather than
49
- * converting to a ToolMessage (see ToolNode.js: isMiddlewareError check).
50
- * MCP tool schema validation errors therefore crash the graph instead of
51
- * being fed back to the LLM as recoverable feedback.
52
- */
53
- function wrapToolsWithErrorHandling(tools, serviceName) {
54
- return tools.map((tool) => {
55
- const original = tool.invoke.bind(tool)
56
- tool.invoke = async (args, config) => {
57
- try {
58
- return await original(args, config)
59
- } catch (err) {
60
- LOG.warn(`MCP tool "${tool.name}" error: ${err.message}`, { service: serviceName })
61
- return `Error: ${err.message}`
62
- }
63
- }
64
- return tool
65
- })
66
- }
67
-
68
44
  export async function buildMcpToolsLocally(serviceName) {
69
45
  const srv = cds.services[serviceName]
70
46
  const tools = generateTools(srv)
71
47
  const prefix = toolName(`${serviceName}_`)
72
48
  for (const tool of tools) tool.name = `${prefix}${tool.name}`
73
49
 
74
- LOG.info(
50
+ LOG.debug(
75
51
  `Got ${tools.length} MCP tools from ${serviceName}: ${tools.map((t) => t.name).join(", ")}`,
76
52
  )
77
53
  return tools
78
54
  }
79
55
 
80
56
  /**
81
- * Build LangChain tools from a CAP MCP connection.
82
- * Resolves the destination URL and auth headers, connects to the MCP server,
83
- * and returns the tools wrapped with error handling for use in agent graphs.
57
+ * Build a dynamic MCP placeholder from a CAP MCP connection.
58
+ * Resolves the destination URL and auth-header factory; the actual tools/list
59
+ * call is deferred to remoteMcpMiddleware which runs per-request with the
60
+ * current user's credentials.
84
61
  *
85
62
  * @param {string} serviceName - cds.requires service key
86
- * @returns {Promise<import("@langchain/core/tools").StructuredTool[]>}
63
+ * @returns {Promise<{ _mcpDynamic: true, mcpUrl: string, resolveHeaders: () => Promise<object> }>}
87
64
  */
88
65
  export async function buildMcpToolsFromConnection(serviceName) {
89
66
  let endpoints = cds.service.endpoints4({
@@ -136,22 +113,7 @@ export async function buildMcpToolsFromConnection(serviceName) {
136
113
  return token ? { Authorization: `Bearer ${token}` } : {}
137
114
  }
138
115
 
139
- const client = new MultiServerMCPClient({
140
- mcpServers: {
141
- [serviceName]: { url: mcpUrl },
142
- },
143
- beforeToolCall: async () => ({ headers: await resolveHeaders() }),
144
- })
145
-
146
- const tools = await client.getTools()
147
- const prefix = toolName(`${serviceName}_`)
148
- for (const tool of tools) tool.name = `${prefix}${tool.name}`
149
-
150
- LOG.info(
151
- `Got ${tools.length} MCP tools from ${serviceName}: ${tools.map((t) => t.name).join(", ")}`,
152
- )
153
-
154
- return wrapToolsWithErrorHandling(tools, serviceName)
116
+ return { _mcpDynamic: true, mcpUrl, serviceName, resolveHeaders }
155
117
  }
156
118
 
157
119
  export async function buildMcpTools(serviceName) {
@@ -2,7 +2,7 @@ import cds from "@sap/cds"
2
2
  import { tool } from "@langchain/core/tools"
3
3
  import { z } from "zod"
4
4
  import { LangGraphExecutor } from "../langgraph-executor-srv.js"
5
- import { toolName } from "../../lib/utils/utils.js"
5
+ import { short, toolName } from "../../lib/utils/utils.js"
6
6
 
7
7
  const LOG = cds.log("agents:sub-agents")
8
8
 
@@ -164,6 +164,7 @@ export async function buildSubAgentToolLocally(serviceName) {
164
164
  taskId,
165
165
  contextId,
166
166
  )
167
+ const truncated = message?.length > 80 ? message.slice(0, 80) + "..." : message
167
168
 
168
169
  try {
169
170
  // Run the sub-agent detached from the calling agent, in its own root
@@ -172,6 +173,12 @@ export async function buildSubAgentToolLocally(serviceName) {
172
173
  await new Promise((resolve, reject) => {
173
174
  cds
174
175
  .spawn({ user: cds.context?.user, tenant: cds.context?.tenant }, async () => {
176
+ LOG.info("request", {
177
+ conversation: short(contextId),
178
+ service: srv.name,
179
+ text: truncated,
180
+ })
181
+
175
182
  await executor.execute(requestContext, eventBus)
176
183
  await done
177
184
  })
@@ -11,7 +11,7 @@ import {
11
11
  executeCallActionTool,
12
12
  executePerActionTool,
13
13
  } from "@cap-js/mcp/lib/tools.js"
14
- import { getFilteredEntities, getFilteredActions } from "../../lib/utils/utils.js"
14
+ import { getFilteredEntities, getFilteredActions, getAgentLogger } from "../../lib/utils/utils.js"
15
15
  import { isTextMime } from "../../lib/agents/markdown/backends/mime-utils.js"
16
16
  import { checkAuthorization } from "@cap-js/mcp/lib/auth.js"
17
17
 
@@ -31,6 +31,7 @@ function cachedAuth(srv) {
31
31
 
32
32
  class GenericReadTool extends DynamicStructuredTool {
33
33
  constructor(srv, entities) {
34
+ const log = getAgentLogger(srv)
34
35
  const def = createGenericReadToolDefinition(Object.keys(entities), srv.name, "")
35
36
  super({
36
37
  name: def.name,
@@ -38,7 +39,7 @@ class GenericReadTool extends DynamicStructuredTool {
38
39
  schema: def.inputSchema,
39
40
  responseFormat: "content_and_artifact",
40
41
  func: async (args) => {
41
- return unwrap(await executeGenericReadTool(srv, entities, args, { log: LOG }))
42
+ return unwrap(await executeGenericReadTool(srv, entities, args, { log }))
42
43
  },
43
44
  })
44
45
  this.srv = srv
@@ -67,6 +68,7 @@ class GenericReadTool extends DynamicStructuredTool {
67
68
 
68
69
  class DescribeTool extends DynamicStructuredTool {
69
70
  constructor(srv, entities, actions) {
71
+ const log = getAgentLogger(srv)
70
72
  const def = createDescribeToolDefinition(
71
73
  Object.keys(entities),
72
74
  Object.keys(actions),
@@ -79,7 +81,7 @@ class DescribeTool extends DynamicStructuredTool {
79
81
  schema: def.inputSchema,
80
82
  responseFormat: "content_and_artifact",
81
83
  func: async (args) => {
82
- return unwrap(await executeDescribe(srv, entities, actions, args, { log: LOG }))
84
+ return unwrap(await executeDescribe(srv, entities, actions, args, { log }))
83
85
  },
84
86
  })
85
87
  this.srv = srv
@@ -119,6 +121,7 @@ class DescribeTool extends DynamicStructuredTool {
119
121
 
120
122
  class PerActionTool extends DynamicStructuredTool {
121
123
  constructor(srv, actionName, action) {
124
+ const log = getAgentLogger(srv)
122
125
  const def = createPerActionToolDefinition(actionName, action, srv.name, srv.model, "")
123
126
  super({
124
127
  name: def.name,
@@ -126,7 +129,7 @@ class PerActionTool extends DynamicStructuredTool {
126
129
  schema: def.inputSchema,
127
130
  responseFormat: "content_and_artifact",
128
131
  func: async (args) => {
129
- return unwrap(await executePerActionTool(srv, actionName, action, args, { log: LOG }))
132
+ return unwrap(await executePerActionTool(srv, actionName, action, args, { log }))
130
133
  },
131
134
  })
132
135
  this.srv = srv
@@ -142,6 +145,7 @@ class PerActionTool extends DynamicStructuredTool {
142
145
 
143
146
  class CallActionTool extends DynamicStructuredTool {
144
147
  constructor(srv, actions) {
148
+ const log = getAgentLogger(srv)
145
149
  const def = createCallActionToolDefinition(Object.keys(actions), srv.name, "")
146
150
  super({
147
151
  name: def.name,
@@ -149,7 +153,7 @@ class CallActionTool extends DynamicStructuredTool {
149
153
  schema: def.inputSchema,
150
154
  responseFormat: "content_and_artifact",
151
155
  func: async (args) => {
152
- return unwrap(await executeCallActionTool(srv, actions, args, { log: LOG }))
156
+ return unwrap(await executeCallActionTool(srv, actions, args, { log }))
153
157
  },
154
158
  })
155
159
  this.srv = srv
package/index.js DELETED
File without changes