@cap-js/agents 0.9.7 → 0.9.8
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 +2 -0
- package/cds-plugin.js +13 -10
- package/lib/agents/middleware/content-filter.js +4 -0
- package/lib/agents/middleware/hitl-decision-note-injector.js +18 -6
- package/lib/agents/middleware/hitl.js +17 -6
- package/lib/agents/middleware/index.js +1 -1
- package/lib/agents/middleware/masking.js +6 -3
- package/lib/agents/middleware/remote-mcp.js +5 -2
- package/lib/agents/middleware/status-update.js +209 -76
- package/lib/agents/middleware/tool-wrap.js +1 -2
- package/lib/agents/summarize-on-timeout.js +5 -2
- package/lib/config/local.js +121 -8
- package/lib/eval/eval-run.js +50 -4
- package/lib/masking/unstructured/hana.js +13 -5
- package/lib/models/aicore.js +16 -3
- package/lib/models/openai.js +9 -0
- package/lib/preview/chat.html +561 -93
- package/lib/telemetry/chat-tracing.js +2 -0
- package/lib/telemetry/mlflow/evaluation.js +83 -4
- package/lib/telemetry/mlflow/exporter/DatabricksExporter.js +39 -5
- package/lib/telemetry/mlflow/exporter/MlflowExporter.js +43 -4
- package/lib/telemetry/mlflow/index.js +1 -7
- package/lib/telemetry/mlflow/prompts.js +15 -11
- package/lib/utils/markdown.js +1 -8
- package/lib/utils/resilience.js +2 -1
- package/lib/utils/toml.js +67 -0
- package/lib/utils/usage.js +34 -0
- package/lib/utils/utils.js +43 -1
- package/package.json +31 -26
- package/srv/handlers/graph-executor/crash-handler.js +47 -0
- package/srv/handlers/graph-executor/hitl.js +27 -0
- package/srv/handlers/graph-executor.js +19 -5
- package/srv/handlers/index.js +6 -1
- package/srv/handlers/mcp-tools.js +11 -2
- package/srv/handlers/subagent-tools.js +13 -4
- package/srv/handlers/system-prompt.js +30 -27
- package/srv/handlers/tools.js +14 -13
|
@@ -156,6 +156,10 @@ export function composeHitlDecisionNote(actionRequests, resume) {
|
|
|
156
156
|
lines.push("- User edited " + action(matched) + " to " + action(decision.editedAction) + ".")
|
|
157
157
|
continue
|
|
158
158
|
}
|
|
159
|
+
if (decision?.type === "reject") {
|
|
160
|
+
lines.push("- User explicitly rejected " + action(original) + ".")
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
159
163
|
}
|
|
160
164
|
if (!lines.length) {
|
|
161
165
|
return undefined
|
|
@@ -163,6 +167,28 @@ export function composeHitlDecisionNote(actionRequests, resume) {
|
|
|
163
167
|
return ["User HITL decisions (not tool failures):", ...lines].join("\n")
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
// The generic combined "call" tool carries its target action in args.action. HITL
|
|
171
|
+
// gates it per-call via a `when` predicate, so the interrupt only fires for a gated
|
|
172
|
+
// action. An edit must therefore not repoint args.action at a *different* action —
|
|
173
|
+
// that would run an un-gated action under the approval granted for the gated one.
|
|
174
|
+
// Editing the parameters is fine; the action itself is fixed.
|
|
175
|
+
const GENERIC_CALL_TOOL = "call"
|
|
176
|
+
|
|
177
|
+
export function guardHitlEdits(resume, actions = []) {
|
|
178
|
+
if (!Array.isArray(resume?.decisions)) return resume
|
|
179
|
+
resume.decisions.forEach((decision, index) => {
|
|
180
|
+
if (decision?.type !== "edit") return
|
|
181
|
+
const original = actions[index]
|
|
182
|
+
if (original?.name !== GENERIC_CALL_TOOL) return
|
|
183
|
+
const from = original.args?.action
|
|
184
|
+
const to = decision.editedAction?.args?.action
|
|
185
|
+
if (to !== undefined && from !== undefined && to !== from) {
|
|
186
|
+
throw new Error(`HITL edit must not change the gated action (expected "${from}", got "${to}").`)
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
return resume
|
|
190
|
+
}
|
|
191
|
+
|
|
166
192
|
async function getPreInterruptToolCalls(graph, config) {
|
|
167
193
|
try {
|
|
168
194
|
if (typeof graph.getState !== "function") return []
|
|
@@ -339,6 +365,7 @@ export async function resumeHitl({ requestContext, graph, config, eventBus, stre
|
|
|
339
365
|
const originalActions = actionRequests.length
|
|
340
366
|
? actionRequests
|
|
341
367
|
: await getPreInterruptToolCalls(graph, config)
|
|
368
|
+
guardHitlEdits(resume, originalActions)
|
|
342
369
|
const decisionNote = composeHitlDecisionNote(originalActions, resume)
|
|
343
370
|
const commandArgs = { resume }
|
|
344
371
|
if (decisionNote) commandArgs.update = { _hitlDecisionNote: decisionNote }
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
resumeHitl,
|
|
20
20
|
resumeTimeoutHitl,
|
|
21
21
|
} from "./graph-executor/hitl.js"
|
|
22
|
+
import { registerShutdownHook } from "./graph-executor/crash-handler.js"
|
|
22
23
|
|
|
23
24
|
const LOG = cds.log("agents")
|
|
24
25
|
|
|
@@ -145,6 +146,9 @@ class GraphExecutor {
|
|
|
145
146
|
this._recursionLimit = options.recursionLimit ?? null
|
|
146
147
|
/** @type {Map<string, AbortController>} per-task abort controllers */
|
|
147
148
|
this._abortControllers = new Map()
|
|
149
|
+
/** @type {Map<string, string | undefined>} task tenant by task ID */
|
|
150
|
+
this._taskTenants = new Map()
|
|
151
|
+
registerShutdownHook(this)
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
/**
|
|
@@ -199,6 +203,7 @@ class GraphExecutor {
|
|
|
199
203
|
// final visual (collapse to the last turn's bubble at task completion).
|
|
200
204
|
let currentMsgId = null
|
|
201
205
|
let thinkingCount = 0
|
|
206
|
+
let thinkingMsgId = null
|
|
202
207
|
// Holds a trailing fragment of the previous chunk that is a prefix of a known
|
|
203
208
|
// pseudonym hash. Prepended to the next chunk so split hashes are resolved correctly.
|
|
204
209
|
let pendingPrefix = ""
|
|
@@ -247,6 +252,9 @@ class GraphExecutor {
|
|
|
247
252
|
pendingPrefix = ""
|
|
248
253
|
if (!raw) continue
|
|
249
254
|
|
|
255
|
+
if (thinkingMsgId !== null && currentMsgId !== thinkingMsgId) thinkingCount++
|
|
256
|
+
thinkingMsgId = currentMsgId
|
|
257
|
+
|
|
250
258
|
// Hashes look like name-8hexchars and never contain spaces.
|
|
251
259
|
// On non-last chunks, slice last token and append to next chunk
|
|
252
260
|
// to avoid unresolved boundaries
|
|
@@ -275,7 +283,6 @@ class GraphExecutor {
|
|
|
275
283
|
parts: [{ kind: "text", text }],
|
|
276
284
|
},
|
|
277
285
|
})
|
|
278
|
-
if (lastChunk) thinkingCount++
|
|
279
286
|
tokenCount++
|
|
280
287
|
} else if (mode === "updates") {
|
|
281
288
|
// The updates stream yields per-node deltas — { <node>: { messages: [oneNewMessage] } },
|
|
@@ -382,8 +389,6 @@ class GraphExecutor {
|
|
|
382
389
|
reason,
|
|
383
390
|
checkpointer: this._graph?.checkpointer,
|
|
384
391
|
getModel: () => this._srv.send("buildModel"),
|
|
385
|
-
// Summary runs after graph abort, so no execution-time grace is needed.
|
|
386
|
-
timeout: 10_000,
|
|
387
392
|
}),
|
|
388
393
|
)
|
|
389
394
|
}
|
|
@@ -397,6 +402,7 @@ class GraphExecutor {
|
|
|
397
402
|
// Cooperative cancellation: per-task AbortController
|
|
398
403
|
const controller = new AbortController()
|
|
399
404
|
this._abortControllers.set(taskId, controller)
|
|
405
|
+
this._taskTenants.set(taskId, cds.context?.tenant)
|
|
400
406
|
|
|
401
407
|
// A2A context for tracing
|
|
402
408
|
if (!cds.context) {
|
|
@@ -406,6 +412,7 @@ class GraphExecutor {
|
|
|
406
412
|
cds.context["agent.context.id"] = contextId
|
|
407
413
|
cds.context["agent.service"] = serviceName
|
|
408
414
|
cds.context["agent.eventBus"] = eventBus
|
|
415
|
+
cds.context["agent.request.metadata"] = requestContext.userMessage?.metadata ?? {}
|
|
409
416
|
|
|
410
417
|
// REVISIT: Resolve graph early for pseudonymizeUserMessage. Mid-term move into beforeAgent together with Audit & Telemetry which rely on it
|
|
411
418
|
const graph = await this._resolveGraph()
|
|
@@ -910,16 +917,19 @@ class GraphExecutor {
|
|
|
910
917
|
eventBus._graphResult = { messages: result.messages || [] }
|
|
911
918
|
}
|
|
912
919
|
|
|
920
|
+
const usageMeta =
|
|
921
|
+
usageData?.total_tokens > 0 ? { "sap.cds.agents.token-usage": usageData } : undefined
|
|
913
922
|
eventBus.publish({
|
|
914
923
|
kind: "status-update",
|
|
915
924
|
taskId,
|
|
916
925
|
contextId,
|
|
917
926
|
status: {
|
|
918
927
|
state: "completed",
|
|
919
|
-
message: agentMessage(output),
|
|
928
|
+
message: agentMessage(output, undefined, usageMeta),
|
|
920
929
|
timestamp: new Date().toISOString(),
|
|
921
930
|
},
|
|
922
931
|
final: true,
|
|
932
|
+
metadata: usageMeta,
|
|
923
933
|
})
|
|
924
934
|
} catch (err) {
|
|
925
935
|
// Aborted (client disconnect or tasks/cancel) — publish canceled, not failed
|
|
@@ -1049,6 +1059,7 @@ class GraphExecutor {
|
|
|
1049
1059
|
setSpanAttrs(rootSpan, linkTraceToPrompt())
|
|
1050
1060
|
|
|
1051
1061
|
this._abortControllers.delete(taskId)
|
|
1062
|
+
this._taskTenants.delete(taskId)
|
|
1052
1063
|
metrics.concurrentExecutions.add(-1, mAttrs)
|
|
1053
1064
|
|
|
1054
1065
|
// Update task record with usage data (non-blocking, best effort)
|
|
@@ -1146,13 +1157,16 @@ function aggregateUsageData(messages) {
|
|
|
1146
1157
|
cache_creation_input_tokens: 0,
|
|
1147
1158
|
cache_read_input_tokens: 0,
|
|
1148
1159
|
reasoning_tokens: 0,
|
|
1160
|
+
context_tokens: 0,
|
|
1149
1161
|
}
|
|
1150
1162
|
for (let i = 0; i < messages.length; i++) {
|
|
1151
1163
|
if (!messages[i].usage_metadata) continue
|
|
1152
1164
|
const innerRes = convertUsageData(messages[i].usage_metadata)
|
|
1153
1165
|
Object.keys(innerRes).forEach((k) => {
|
|
1154
|
-
if (innerRes[k] != null) result[k] += innerRes[k]
|
|
1166
|
+
if (k in result && innerRes[k] != null) result[k] += innerRes[k]
|
|
1155
1167
|
})
|
|
1168
|
+
if (innerRes.input_tokens != null)
|
|
1169
|
+
result.context_tokens = innerRes.input_tokens + (innerRes.output_tokens || 0)
|
|
1156
1170
|
}
|
|
1157
1171
|
return result
|
|
1158
1172
|
}
|
package/srv/handlers/index.js
CHANGED
|
@@ -5,6 +5,7 @@ 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
7
|
import { registerChat } from "./chat.js"
|
|
8
|
+
import { effectiveDefinition } from "../../lib/utils/utils.js"
|
|
8
9
|
|
|
9
10
|
const LOG = cds.log("agents")
|
|
10
11
|
|
|
@@ -91,12 +92,16 @@ export default function registerDefaultAgentHandlers(srv) {
|
|
|
91
92
|
|
|
92
93
|
// Default buildModel: cds.connect.to('llm'), configurable via @agent.llm
|
|
93
94
|
srv.on("buildModel", async (req) => {
|
|
94
|
-
const
|
|
95
|
+
const def = effectiveDefinition(srv)
|
|
96
|
+
const name = def?.["@agent.llm"] || srv?.options?.agent?.llm || "llm"
|
|
95
97
|
const options = cds.requires[name] ?? {}
|
|
96
98
|
let { kind, impl } = options
|
|
97
99
|
if (!impl) impl = cds.requires.kinds[kind]?.impl
|
|
98
100
|
if (!impl) throw new Error("No service implementation found for " + name)
|
|
99
101
|
const { default: LLMProvider } = await import(impl)
|
|
102
|
+
const { credentials, ...o } = options
|
|
103
|
+
if (credentials) o.credentials = '{ *** }'
|
|
104
|
+
LOG.debug (`Creating LLMProvider instance for cds.requires.${name} with options:`, o)
|
|
100
105
|
return new LLMProvider(name, { ...options, ...req.data })
|
|
101
106
|
})
|
|
102
107
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import cds from "@sap/cds"
|
|
2
2
|
import { generateTools } from "./tools.js"
|
|
3
|
-
import { toolName } from "../../lib/utils/utils.js"
|
|
3
|
+
import { toolName, mcpToolKind } from "../../lib/utils/utils.js"
|
|
4
4
|
|
|
5
5
|
const LOG = cds.log("agents:mcp")
|
|
6
6
|
|
|
@@ -45,7 +45,16 @@ export async function buildMcpToolsLocally(serviceName) {
|
|
|
45
45
|
const srv = cds.services[serviceName]
|
|
46
46
|
const tools = generateTools(srv)
|
|
47
47
|
const prefix = toolName(`${serviceName}_`)
|
|
48
|
-
for (const tool of tools)
|
|
48
|
+
for (const tool of tools) {
|
|
49
|
+
// Keep PerActionTool's actionName so status-update can resolve the action's label.
|
|
50
|
+
tool.metadata = {
|
|
51
|
+
...tool.metadata,
|
|
52
|
+
serviceName,
|
|
53
|
+
kind: mcpToolKind(tool.name),
|
|
54
|
+
...(tool.actionName && { actionName: tool.actionName }),
|
|
55
|
+
}
|
|
56
|
+
tool.name = `${prefix}${tool.name}`
|
|
57
|
+
}
|
|
49
58
|
|
|
50
59
|
LOG.debug(
|
|
51
60
|
`Got ${tools.length} MCP tools from ${serviceName}: ${tools.map((t) => t.name).join(", ")}`,
|
|
@@ -78,9 +78,9 @@ function formatToolResult({ text, files }) {
|
|
|
78
78
|
/**
|
|
79
79
|
* Wrap an A2A client as a LangChain tool the agent can call.
|
|
80
80
|
*/
|
|
81
|
-
function createA2ATool(client, agentCard) {
|
|
81
|
+
function createA2ATool(client, agentCard, serviceName) {
|
|
82
82
|
const subagent = agentCard.name
|
|
83
|
-
|
|
83
|
+
const t = tool(
|
|
84
84
|
async ({ message }) => {
|
|
85
85
|
try {
|
|
86
86
|
const messageId = cds.utils.uuid()
|
|
@@ -116,6 +116,8 @@ function createA2ATool(client, agentCard) {
|
|
|
116
116
|
}),
|
|
117
117
|
},
|
|
118
118
|
)
|
|
119
|
+
t.metadata = { ...t.metadata, kind: "agent", agentName: agentCard.name, serviceName }
|
|
120
|
+
return t
|
|
119
121
|
}
|
|
120
122
|
|
|
121
123
|
export async function buildSubAgentToolLocally(serviceName) {
|
|
@@ -130,7 +132,7 @@ export async function buildSubAgentToolLocally(serviceName) {
|
|
|
130
132
|
|
|
131
133
|
const { RequestContext, DefaultExecutionEventBus } = await import("@a2a-js/sdk/server")
|
|
132
134
|
|
|
133
|
-
|
|
135
|
+
const localTool = tool(
|
|
134
136
|
async ({ message }) => {
|
|
135
137
|
const taskId = cds.utils.uuid()
|
|
136
138
|
const contextId = cds.utils.uuid()
|
|
@@ -221,6 +223,13 @@ export async function buildSubAgentToolLocally(serviceName) {
|
|
|
221
223
|
}),
|
|
222
224
|
},
|
|
223
225
|
)
|
|
226
|
+
localTool.metadata = {
|
|
227
|
+
...localTool.metadata,
|
|
228
|
+
kind: "agent",
|
|
229
|
+
agentName: agentCard.name,
|
|
230
|
+
serviceName,
|
|
231
|
+
}
|
|
232
|
+
return localTool
|
|
224
233
|
}
|
|
225
234
|
|
|
226
235
|
export async function buildSubAgentToolFromConnection(serviceName) {
|
|
@@ -334,7 +343,7 @@ export async function buildSubAgentToolFromConnection(serviceName) {
|
|
|
334
343
|
)
|
|
335
344
|
const client = await factory.createFromAgentCard(agentCard)
|
|
336
345
|
|
|
337
|
-
return createA2ATool(client, agentCard)
|
|
346
|
+
return createA2ATool(client, agentCard, serviceName)
|
|
338
347
|
}
|
|
339
348
|
|
|
340
349
|
export function buildSubAgentTool(serviceName) {
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
import cds from "@sap/cds"
|
|
2
2
|
import { getDescription } from "../../lib/utils/utils.js"
|
|
3
|
+
const LOG = cds.log("agents")
|
|
3
4
|
|
|
4
5
|
export function buildSystemPrompt(srv) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
6
|
+
|
|
7
|
+
let text = `
|
|
8
|
+
|
|
9
|
+
You are an AI assistant for the local CAP service \`${srv.name}\`.
|
|
10
|
+
${ getDescription(srv.definition) || '' }
|
|
11
|
+
Use the provided tools to answer questions:
|
|
12
|
+
|
|
13
|
+
- the \`describe\` tool to learn about the service's entities and actions.
|
|
14
|
+
- the \`query\` tool to read data from entities.
|
|
15
|
+
- the \`call\` tool to invoke actions, or individual action tools.
|
|
16
|
+
|
|
17
|
+
IMPORTANT: Don't guess element names when \`query\`ing entities.
|
|
18
|
+
Use the \`describe\` tool instead to find correct element names.
|
|
19
|
+
|
|
20
|
+
${ cds.env.agents?.fileIO?.enabled ? `## File I/O
|
|
21
|
+
When the incomming request message contains '[Uploaded files: ...]',
|
|
22
|
+
use the \`read_file\` tool to read each listed file before answering.
|
|
23
|
+
Use the \`emit_file_part\` tool to return files in your response.` : ''}
|
|
24
|
+
|
|
25
|
+
${ cds.env.agents?.masking ? `## How to handle pseudonymized values
|
|
26
|
+
Never comment on pseudonymization and that you had to work with pseudonymized values
|
|
27
|
+
but use the pseudonymized values in responses like it would be regular values!
|
|
28
|
+
The user sees the actual values and thus should not be made aware of any
|
|
29
|
+
pseudonymization that has taken place internally.` : ''}
|
|
30
|
+
|
|
31
|
+
`.replace(/ {4,}/g,'').trim()
|
|
32
|
+
|
|
33
|
+
if (LOG._debug) LOG.debug (srv.name, '-', "using system prompt:", '\n\n'+ text +'\n')
|
|
34
|
+
return text
|
|
32
35
|
}
|
package/srv/handlers/tools.js
CHANGED
|
@@ -183,31 +183,32 @@ class CallActionTool extends DynamicStructuredTool {
|
|
|
183
183
|
* @param {object} srv - CDS ApplicationService
|
|
184
184
|
*/
|
|
185
185
|
export function generateTools(srv) {
|
|
186
|
-
const entities = getFilteredEntities(srv)
|
|
187
|
-
const actions = getFilteredActions(srv)
|
|
186
|
+
const entities = getFilteredEntities(srv), has_entities = Object.keys(entities).length > 0
|
|
187
|
+
const actions = getFilteredActions(srv), has_actions = Object.keys(actions).length > 0
|
|
188
188
|
|
|
189
189
|
const tools = []
|
|
190
190
|
|
|
191
|
-
// Query tool — one tool for reading all entities
|
|
192
|
-
const entityNames = Object.keys(entities)
|
|
193
|
-
if (entityNames.length > 0) {
|
|
194
|
-
tools.push(new GenericReadTool(srv, entities))
|
|
195
|
-
}
|
|
196
|
-
|
|
197
191
|
// Describe tool — introspect service model
|
|
198
|
-
|
|
199
|
-
|
|
192
|
+
if (has_entities || has_actions) {
|
|
193
|
+
LOG.debug(srv.name, '–', `adding generic 'describe' tool`)
|
|
200
194
|
tools.push(new DescribeTool(srv, entities, actions))
|
|
201
195
|
}
|
|
202
196
|
|
|
197
|
+
// Query tool — one tool for reading all entities
|
|
198
|
+
if (has_entities) {
|
|
199
|
+
LOG.debug(srv.name, '–', `adding generic 'query' entity tool`)
|
|
200
|
+
tools.push(new GenericReadTool(srv, entities))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
203
|
// Action/function tools — per-action (default) or combined call action
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (usePerActionTools) {
|
|
204
|
+
if (has_actions) {
|
|
205
|
+
if (cds.env.mcp?.per_action_tool) {
|
|
207
206
|
for (const [name, action] of Object.entries(actions)) {
|
|
207
|
+
LOG.debug(srv.name, '–', `adding specific tool to call action '${name}'`)
|
|
208
208
|
tools.push(new PerActionTool(srv, name, action))
|
|
209
209
|
}
|
|
210
210
|
} else {
|
|
211
|
+
LOG.debug(srv.name, '–', `adding generic 'call' action tool`)
|
|
211
212
|
tools.push(new CallActionTool(srv, actions))
|
|
212
213
|
}
|
|
213
214
|
}
|