@cap-js/agents 0.9.3 → 0.9.5

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 (35) hide show
  1. package/_i18n/messages.properties +17 -0
  2. package/cds-plugin.js +94 -77
  3. package/lib/agents/markdown/deep-agent.js +1 -1
  4. package/lib/agents/middleware/hitl-decision-note-injector.js +18 -0
  5. package/lib/agents/middleware/hitl.js +2 -2
  6. package/lib/agents/middleware/index.js +1 -1
  7. package/lib/agents/middleware/quota-enforcer.js +17 -8
  8. package/lib/agents/middleware/remote-mcp.js +3 -2
  9. package/lib/agents/middleware/status-update.js +1 -1
  10. package/lib/agents/middleware/tool-wrap.js +5 -6
  11. package/lib/agents/quota-enforcer-at-start.js +21 -18
  12. package/lib/agents/summarize-on-timeout.js +18 -13
  13. package/lib/compile.js +2 -3
  14. package/lib/config/local.js +94 -0
  15. package/lib/eval/eval-run.js +11 -6
  16. package/lib/index.js +9 -12
  17. package/lib/models/aicore.js +91 -127
  18. package/lib/models/anthropic.js +10 -67
  19. package/lib/preview/chat.html +62 -33
  20. package/lib/telemetry/chat-tracing.js +13 -3
  21. package/lib/telemetry/metrics.js +6 -0
  22. package/lib/telemetry/mlflow/exporter/DatabricksExporter.js +1 -1
  23. package/lib/telemetry/mlflow/prompts.js +6 -2
  24. package/lib/utils/caching.js +132 -0
  25. package/lib/utils/message-handling.js +14 -0
  26. package/lib/utils/utils.js +4 -9
  27. package/package.json +7 -5
  28. package/srv/handlers/chat.js +4 -4
  29. package/srv/handlers/graph-executor/hitl.js +381 -0
  30. package/srv/handlers/graph-executor.js +99 -336
  31. package/srv/handlers/index.js +4 -3
  32. package/srv/handlers/mcp-tools.js +3 -2
  33. package/srv/handlers/{sub-agent-tools.js → subagent-tools.js} +43 -22
  34. package/srv/handlers/tools.js +35 -9
  35. package/lib/agents/middleware/hitl-edit-note-injector.js +0 -18
@@ -67,7 +67,7 @@ export default function registerDefaultAgentHandlers(srv) {
67
67
  }
68
68
 
69
69
  const { buildMcpTools } = await import("./mcp-tools.js")
70
- const { buildSubAgentTool } = await import("./sub-agent-tools.js")
70
+ const { buildSubAgentTool } = await import("./subagent-tools.js")
71
71
 
72
72
  const results = await Promise.allSettled([
73
73
  ...mcpEntries.map((e) => buildMcpTools(e.service ?? e)),
@@ -76,7 +76,7 @@ export default function registerDefaultAgentHandlers(srv) {
76
76
 
77
77
  const extraTools = []
78
78
  for (const r of results) {
79
- // MCP connections yield an array of tools; sub-agent connections yield a
79
+ // MCP connections yield an array of tools; subagent connections yield a
80
80
  // single tool. Normalize both so instrumentTools sees a flat tool list.
81
81
  if (r.status === "fulfilled") {
82
82
  if (Array.isArray(r.value)) extraTools.push(...r.value.filter(Boolean))
@@ -92,7 +92,8 @@ export default function registerDefaultAgentHandlers(srv) {
92
92
  // Default buildModel: cds.connect.to('llm'), configurable via @agent.llm
93
93
  srv.on("buildModel", async (req) => {
94
94
  const name = srv?.options?.agent?.llm || srv?.definition?.["@agent.llm"] || "llm"
95
- let { kind, impl, ...options } = cds.requires[name] ?? {}
95
+ const options = cds.requires[name] ?? {}
96
+ let { kind, impl } = options
96
97
  if (!impl) impl = cds.requires.kinds[kind]?.impl
97
98
  if (!impl) throw new Error("No service implementation found for " + name)
98
99
  const { default: LLMProvider } = await import(impl)
@@ -101,7 +101,7 @@ export async function buildMcpToolsFromConnection(serviceName) {
101
101
 
102
102
  const path = typeof credentials === "object" ? credentials?.path : null
103
103
  const mcpUrl = url.replace(/\/$/, "") + (path ? `/${path.replace(/^\//, "")}` : "")
104
- LOG.info(`Connecting to MCP server at ${mcpUrl}`)
104
+ LOG.info(`Connecting to MCP service ${serviceName}`, { at: mcpUrl })
105
105
 
106
106
  const resolveHeaders = async () => {
107
107
  if (destinationName) {
@@ -113,7 +113,8 @@ export async function buildMcpToolsFromConnection(serviceName) {
113
113
  return token ? { Authorization: `Bearer ${token}` } : {}
114
114
  }
115
115
 
116
- return { _mcpDynamic: true, mcpUrl, serviceName, resolveHeaders }
116
+ const toolFilter = cds.requires[serviceName]?.mcp?.tools
117
+ return { _mcpDynamic: true, mcpUrl, serviceName, resolveHeaders, toolFilter }
117
118
  }
118
119
 
119
120
  export async function buildMcpTools(serviceName) {
@@ -1,11 +1,16 @@
1
1
  import cds from "@sap/cds"
2
2
  import { tool } from "@langchain/core/tools"
3
3
  import { z } from "zod"
4
+ import { getTracer } from "../../lib/telemetry/metrics.js"
4
5
  import { LangGraphExecutor } from "../langgraph-executor-srv.js"
5
6
  import { short, toolName } from "../../lib/utils/utils.js"
6
7
 
7
- const LOG = cds.log("agents:sub-agents")
8
+ const LOG = cds.log("agents:a2a|agents|subagents|a2a")
8
9
 
10
+ const TRUNCATE = cds.env.agents?.truncate || 111
11
+ const truncated = (msg) => (msg?.length > TRUNCATE ? msg.slice(0, TRUNCATE) + "..." : msg)
12
+
13
+ import { inspect } from "util"
9
14
  /**
10
15
  * Extract text and file parts from an A2A response (task or message).
11
16
  */
@@ -15,7 +20,7 @@ function extractResult(result) {
15
20
  const text = []
16
21
  const files = []
17
22
 
18
- const processParts = (parts = []) => {
23
+ const processParts = (parts) => {
19
24
  for (const part of parts) {
20
25
  if (part.kind === "text") text.push(part.text)
21
26
  else if (part.kind === "file") files.push(part)
@@ -23,8 +28,11 @@ function extractResult(result) {
23
28
  }
24
29
 
25
30
  if (result.kind === "task") {
26
- processParts(result.status?.message?.parts)
27
- for (const artifact of result.artifacts || []) processParts(artifact.parts)
31
+ if (result.artifacts)
32
+ for (let artifact of result.artifacts) {
33
+ if (!artifact.artifactId.startsWith("thinking")) processParts(artifact.parts)
34
+ }
35
+ else if (result.status?.message) processParts(result.status.message.parts)
28
36
  if (text.length === 0 && files.length === 0) {
29
37
  return { text: `Task ${result.id}: ${result.status?.state || "unknown"}`, files: [] }
30
38
  }
@@ -71,21 +79,33 @@ function formatToolResult({ text, files }) {
71
79
  * Wrap an A2A client as a LangChain tool the agent can call.
72
80
  */
73
81
  function createA2ATool(client, agentCard) {
82
+ const subagent = agentCard.name
74
83
  return tool(
75
84
  async ({ message }) => {
76
85
  try {
86
+ const messageId = cds.utils.uuid()
87
+ LOG.info(`Sending message to ${subagent}`, { messageId }, "\n\n" + message + "\n")
77
88
  const result = await client.sendMessage({
78
89
  message: {
79
90
  kind: "message",
80
91
  role: "user",
81
- messageId: cds.utils.uuid(),
92
+ messageId,
82
93
  parts: [{ kind: "text", text: message }],
83
94
  },
84
95
  })
85
- return formatToolResult(extractResult(result))
96
+ if (LOG._debug)
97
+ LOG.trace(`Raw results from ${subagent}`, inspect(result, { depth: null, colors: true }))
98
+ let response = formatToolResult(extractResult(result))
99
+ if (response)
100
+ LOG.info(
101
+ `Got response from ${subagent}`,
102
+ { messageId },
103
+ "\n\n" + truncated(response) + "\n",
104
+ )
105
+ return response
86
106
  } catch (err) {
87
- LOG.warn("Sub-agent tool error", { agent: agentCard.name, error: err.message })
88
- return `Error communicating with ${agentCard.name}: ${err.message}`
107
+ LOG.warn("Subagent tool error", { subagent, error: err.message })
108
+ return `Error communicating with ${subagent}: ${err.message}`
89
109
  }
90
110
  },
91
111
  {
@@ -100,13 +120,13 @@ function createA2ATool(client, agentCard) {
100
120
 
101
121
  export async function buildSubAgentToolLocally(serviceName) {
102
122
  const srv = cds.services[serviceName]
103
- if (!srv) throw new Error(`[sub-agents] No local service "${serviceName}" found.`)
123
+ if (!srv) throw new Error(`[subagents] No local service "${serviceName}" found.`)
104
124
 
105
125
  const executor = LangGraphExecutor.for(srv)
106
126
 
107
127
  const { generateAgentCard } = await import("../../lib/protocol/agent-card.js")
108
128
  const agentCard = generateAgentCard(srv)
109
- LOG.info(`Wired local sub-agent "${agentCard.name}" (${serviceName})`)
129
+ LOG.info(`Connecting to subagent ${serviceName}`, "(local)")
110
130
 
111
131
  const { RequestContext, DefaultExecutionEventBus } = await import("@a2a-js/sdk/server")
112
132
 
@@ -135,7 +155,7 @@ export async function buildSubAgentToolLocally(serviceName) {
135
155
  .map((p) => p.text)
136
156
  .join("\n")
137
157
  if (text) statusText = text
138
- if (state === "failed") failed = text || "Sub-agent execution failed."
158
+ if (state === "failed") failed = text || "Subagent execution failed."
139
159
  } else if (event.kind === "artifact-update") {
140
160
  const parts = event.artifact?.parts || []
141
161
  if (event.artifact?.artifactId === "response") {
@@ -164,19 +184,17 @@ export async function buildSubAgentToolLocally(serviceName) {
164
184
  taskId,
165
185
  contextId,
166
186
  )
167
- const truncated = message?.length > 80 ? message.slice(0, 80) + "..." : message
168
187
 
169
188
  try {
170
- // Run the sub-agent detached from the calling agent, in its own root
189
+ // Run the subagent detached from the calling agent, in its own root
171
190
  // context and transaction (cds.spawn creates + commits a fresh tx). A
172
191
  // shared transaction breaks the stream.
173
192
  await new Promise((resolve, reject) => {
174
193
  cds
175
194
  .spawn({ user: cds.context?.user, tenant: cds.context?.tenant }, async () => {
176
- LOG.info("request", {
195
+ LOG.info(srv.name, "-", "request", {
177
196
  conversation: short(contextId),
178
- service: srv.name,
179
- text: truncated,
197
+ text: truncated(message),
180
198
  })
181
199
 
182
200
  await executor.execute(requestContext, eventBus)
@@ -186,7 +204,7 @@ export async function buildSubAgentToolLocally(serviceName) {
186
204
  .on("failed", reject)
187
205
  })
188
206
  } catch (err) {
189
- LOG.warn("Local sub-agent tool error", { agent: agentCard.name, error: err.message })
207
+ LOG.warn("Local subagent tool error", { agent: agentCard.name, error: err.message })
190
208
  return `Error running ${agentCard.name}: ${err.message}`
191
209
  }
192
210
 
@@ -226,7 +244,7 @@ export async function buildSubAgentToolFromConnection(serviceName) {
226
244
 
227
245
  if (!localUrl && !destinationName) {
228
246
  throw new Error(
229
- `[sub-agents] Could not resolve URL or destination name for service "${serviceName}". Check cds.requires config.`,
247
+ `[subagents] Could not resolve URL or destination name for service "${serviceName}". Check cds.requires config.`,
230
248
  )
231
249
  }
232
250
 
@@ -274,13 +292,13 @@ export async function buildSubAgentToolFromConnection(serviceName) {
274
292
 
275
293
  if (!agentBaseUrl) {
276
294
  throw new Error(
277
- `[sub-agents] Could not resolve URL for service "${serviceName}" after destination lookup.`,
295
+ `[subagents] Could not resolve URL for service "${serviceName}" after destination lookup.`,
278
296
  )
279
297
  }
280
298
 
281
299
  const path = typeof credentials === "object" ? credentials?.path : null
282
300
  const base = agentBaseUrl.replace(/\/$/, "") + (path ? `/${path.replace(/^\//, "")}` : "")
283
- LOG.info(`Connecting to sub-agent at ${base}`)
301
+ LOG.info(`Connecting to subagent ${serviceName}`, { at: base })
284
302
 
285
303
  // revisit: a2a agents may be tenant specific, card per tenant?
286
304
  const initialHeaders = await resolveHeaders()
@@ -290,17 +308,20 @@ export async function buildSubAgentToolFromConnection(serviceName) {
290
308
  })
291
309
  if (!cardRes.ok) {
292
310
  throw new Error(
293
- `[sub-agents] Agent card fetch failed for "${serviceName}" at ${cardUrl}: HTTP ${cardRes.status}`,
311
+ `[subagents] Agent card fetch failed for "${serviceName}" at ${cardUrl}: HTTP ${cardRes.status}`,
294
312
  )
295
313
  }
296
314
  const agentCard = await cardRes.json()
297
- LOG.info(`Connected to sub-agent "${agentCard.name}" (${serviceName})`)
298
315
 
299
316
  const { ClientFactory, ClientFactoryOptions, JsonRpcTransportFactory, RestTransportFactory } =
300
317
  await import("@a2a-js/sdk/client")
301
318
 
302
319
  const customFetch = async (url, init = {}) => {
303
320
  const headers = await resolveHeaders()
321
+ if (getTracer()) {
322
+ const { context, propagation } = await import("@opentelemetry/api")
323
+ propagation.inject(context.active(), headers)
324
+ }
304
325
  return fetch(url, { ...init, headers: { ...headers, ...(init.headers || {}) } })
305
326
  }
306
327
  const factory = new ClientFactory(
@@ -11,7 +11,7 @@ import {
11
11
  executeCallActionTool,
12
12
  executePerActionTool,
13
13
  } from "@cap-js/mcp/lib/tools.js"
14
- import { getFilteredEntities, getFilteredActions, getAgentLogger } from "../../lib/utils/utils.js"
14
+ import { getFilteredEntities, getFilteredActions } 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,7 +31,6 @@ function cachedAuth(srv) {
31
31
 
32
32
  class GenericReadTool extends DynamicStructuredTool {
33
33
  constructor(srv, entities) {
34
- const log = getAgentLogger(srv)
35
34
  const def = createGenericReadToolDefinition(Object.keys(entities), srv.name, "")
36
35
  super({
37
36
  name: def.name,
@@ -39,7 +38,7 @@ class GenericReadTool extends DynamicStructuredTool {
39
38
  schema: def.inputSchema,
40
39
  responseFormat: "content_and_artifact",
41
40
  func: async (args) => {
42
- return unwrap(await executeGenericReadTool(srv, entities, args, { log }))
41
+ return unwrap(await executeGenericReadTool(srv, entities, args))
43
42
  },
44
43
  })
45
44
  this.srv = srv
@@ -68,7 +67,6 @@ class GenericReadTool extends DynamicStructuredTool {
68
67
 
69
68
  class DescribeTool extends DynamicStructuredTool {
70
69
  constructor(srv, entities, actions) {
71
- const log = getAgentLogger(srv)
72
70
  const def = createDescribeToolDefinition(
73
71
  Object.keys(entities),
74
72
  Object.keys(actions),
@@ -81,7 +79,7 @@ class DescribeTool extends DynamicStructuredTool {
81
79
  schema: def.inputSchema,
82
80
  responseFormat: "content_and_artifact",
83
81
  func: async (args) => {
84
- return unwrap(await executeDescribe(srv, entities, actions, args, { log }))
82
+ return unwrap(await executeDescribe(srv, entities, actions, args))
85
83
  },
86
84
  })
87
85
  this.srv = srv
@@ -121,7 +119,6 @@ class DescribeTool extends DynamicStructuredTool {
121
119
 
122
120
  class PerActionTool extends DynamicStructuredTool {
123
121
  constructor(srv, actionName, action) {
124
- const log = getAgentLogger(srv)
125
122
  const def = createPerActionToolDefinition(actionName, action, srv.name, srv.model, "")
126
123
  super({
127
124
  name: def.name,
@@ -129,7 +126,7 @@ class PerActionTool extends DynamicStructuredTool {
129
126
  schema: def.inputSchema,
130
127
  responseFormat: "content_and_artifact",
131
128
  func: async (args) => {
132
- return unwrap(await executePerActionTool(srv, actionName, action, args, { log }))
129
+ return unwrap(await executePerActionTool(srv, actionName, action, args))
133
130
  },
134
131
  })
135
132
  this.srv = srv
@@ -145,7 +142,6 @@ class PerActionTool extends DynamicStructuredTool {
145
142
 
146
143
  class CallActionTool extends DynamicStructuredTool {
147
144
  constructor(srv, actions) {
148
- const log = getAgentLogger(srv)
149
145
  const def = createCallActionToolDefinition(Object.keys(actions), srv.name, "")
150
146
  super({
151
147
  name: def.name,
@@ -153,7 +149,7 @@ class CallActionTool extends DynamicStructuredTool {
153
149
  schema: def.inputSchema,
154
150
  responseFormat: "content_and_artifact",
155
151
  func: async (args) => {
156
- return unwrap(await executeCallActionTool(srv, actions, args, { log }))
152
+ return unwrap(await executeCallActionTool(srv, actions, args))
157
153
  },
158
154
  })
159
155
  this.srv = srv
@@ -222,6 +218,10 @@ export function generateTools(srv) {
222
218
  tools.push(createEmitFilePartTool())
223
219
  }
224
220
 
221
+ if (cds.env.agents?.emitDataParts) {
222
+ tools.push(createEmitDataPartTool())
223
+ }
224
+
225
225
  return tools
226
226
  }
227
227
 
@@ -368,3 +368,29 @@ export function createReadFileTool(fileStore, contextId, userId) {
368
368
  },
369
369
  )
370
370
  }
371
+
372
+ /**
373
+ * Create a tool that emits a DataPart in the A2A response.
374
+ * The executor's toolResults collection detects `kind: "data"`
375
+ * and emits the tool result as a data part.
376
+ */
377
+ export function createEmitDataPartTool() {
378
+ return tool(
379
+ async ({ data, mediaType }) => {
380
+ return {
381
+ kind: "data",
382
+ data,
383
+ mediaType: mediaType ?? "application/json",
384
+ }
385
+ },
386
+ {
387
+ name: "emit_data_part",
388
+ description: "Emit a structured A2A DataPart. Only use when instructed.",
389
+ schema: z.object({
390
+ // A2A DataPart is specified to be an object in A2A 0.3
391
+ // https://a2a-protocol.org/v0.3.0/specification/#653-datapart-object
392
+ data: z.looseObject().describe("Structured object"),
393
+ }),
394
+ },
395
+ )
396
+ }
@@ -1,18 +0,0 @@
1
- import { createMiddleware } from "langchain"
2
- import { HumanMessage } from "@langchain/core/messages"
3
- import { z } from "zod"
4
-
5
- // Injects a HumanMessage for a pending _hitlEditNote before the next model turn.
6
- export function hitlEditNoteInjectorMiddleware() {
7
- return createMiddleware({
8
- name: "hitlEditNoteInjectorMiddleware",
9
- stateSchema: z.object({ _hitlEditNote: z.string().optional() }),
10
- beforeModel: async (state) => {
11
- if (!state._hitlEditNote) return
12
- return {
13
- messages: [new HumanMessage(state._hitlEditNote)],
14
- _hitlEditNote: undefined,
15
- }
16
- },
17
- })
18
- }