@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.
- package/_i18n/messages.properties +17 -0
- package/cds-plugin.js +94 -77
- package/lib/agents/markdown/deep-agent.js +1 -1
- package/lib/agents/middleware/hitl-decision-note-injector.js +18 -0
- package/lib/agents/middleware/hitl.js +2 -2
- package/lib/agents/middleware/index.js +1 -1
- package/lib/agents/middleware/quota-enforcer.js +17 -8
- package/lib/agents/middleware/remote-mcp.js +3 -2
- package/lib/agents/middleware/status-update.js +1 -1
- package/lib/agents/middleware/tool-wrap.js +5 -6
- package/lib/agents/quota-enforcer-at-start.js +21 -18
- package/lib/agents/summarize-on-timeout.js +18 -13
- package/lib/compile.js +2 -3
- package/lib/config/local.js +94 -0
- package/lib/eval/eval-run.js +11 -6
- package/lib/index.js +9 -12
- package/lib/models/aicore.js +91 -127
- package/lib/models/anthropic.js +10 -67
- package/lib/preview/chat.html +62 -33
- package/lib/telemetry/chat-tracing.js +13 -3
- package/lib/telemetry/metrics.js +6 -0
- package/lib/telemetry/mlflow/exporter/DatabricksExporter.js +1 -1
- package/lib/telemetry/mlflow/prompts.js +6 -2
- package/lib/utils/caching.js +132 -0
- package/lib/utils/message-handling.js +14 -0
- package/lib/utils/utils.js +4 -9
- package/package.json +7 -5
- package/srv/handlers/chat.js +4 -4
- package/srv/handlers/graph-executor/hitl.js +381 -0
- package/srv/handlers/graph-executor.js +99 -336
- package/srv/handlers/index.js +4 -3
- package/srv/handlers/mcp-tools.js +3 -2
- package/srv/handlers/{sub-agent-tools.js → subagent-tools.js} +43 -22
- package/srv/handlers/tools.js +35 -9
- package/lib/agents/middleware/hitl-edit-note-injector.js +0 -18
package/srv/handlers/index.js
CHANGED
|
@@ -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("./
|
|
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;
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
92
|
+
messageId,
|
|
82
93
|
parts: [{ kind: "text", text: message }],
|
|
83
94
|
},
|
|
84
95
|
})
|
|
85
|
-
|
|
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("
|
|
88
|
-
return `Error communicating with ${
|
|
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(`[
|
|
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(`
|
|
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 || "
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
`[
|
|
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
|
-
`[
|
|
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
|
|
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
|
-
`[
|
|
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(
|
package/srv/handlers/tools.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
executeCallActionTool,
|
|
12
12
|
executePerActionTool,
|
|
13
13
|
} from "@cap-js/mcp/lib/tools.js"
|
|
14
|
-
import { getFilteredEntities, getFilteredActions
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
}
|