@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
@@ -126,6 +126,15 @@
126
126
  letter-spacing: 0.03em;
127
127
  text-transform: uppercase;
128
128
  }
129
+ .msg.approval pre {
130
+ margin: 8px 0 0;
131
+ padding: 8px;
132
+ overflow-x: auto;
133
+ border-radius: 6px;
134
+ background: #e7f4ff;
135
+ font-size: 1.1em;
136
+ white-space: pre;
137
+ }
129
138
 
130
139
  .msg.agent p {
131
140
  margin: 0 0 6px;
@@ -371,6 +380,9 @@
371
380
  .msg.approval::before {
372
381
  color: #91adc5;
373
382
  }
383
+ .msg.approval pre {
384
+ background: #0f2d45;
385
+ }
374
386
  .btn-approve {
375
387
  background: #537492;
376
388
  }
@@ -616,7 +628,7 @@
616
628
  function flushPendingThinkingUpdate() {
617
629
  const pending = pendingThinkingUpdate
618
630
  clearPendingThinkingUpdate()
619
- if (pending) renderThinkingStep(pending.artifactId, pending.text, pending.lastChunk)
631
+ if (pending) renderThinkingStep(pending)
620
632
  }
621
633
 
622
634
  function prefersReducedMotion() {
@@ -642,10 +654,17 @@
642
654
  else expandThinkingSummary(el)
643
655
  }
644
656
 
645
- function renderThinkingStep(artifactId, text, lastChunk) {
657
+ function renderThinkingStep(update) {
658
+ const { artifactId, text, lastChunk, append } = update
646
659
  const panel = ensureThinkingSummary()
647
- const isNewStep = artifactId !== currentThinkingArtifactId || currentThinkingClosed
648
- if (isNewStep) {
660
+ const idx = thinkingStepEls.findIndex((step) => step._artifactId === artifactId)
661
+ if (append && idx >= 0) {
662
+ thinkingTexts[idx] += text
663
+ thinkingStepEls[idx].innerHTML = md(thinkingTexts[idx])
664
+ } else if (artifactId === currentThinkingArtifactId && !currentThinkingClosed) {
665
+ thinkingTexts[idx] += "\n" + text
666
+ thinkingStepEls[idx].innerHTML = md(thinkingTexts[idx])
667
+ } else {
649
668
  const step = document.createElement("div")
650
669
  step.className = "thinking-step"
651
670
  step._artifactId = artifactId
@@ -657,17 +676,12 @@
657
676
  updateThinkingSummaryLabel()
658
677
  currentThinkingArtifactId = artifactId
659
678
  currentThinkingClosed = false
660
- } else {
661
- // Append to current open step for this artifactId
662
- const idx = thinkingStepEls.length - 1
663
- thinkingTexts[idx] += text
664
- thinkingStepEls[idx].innerHTML = md(thinkingTexts[idx])
665
679
  }
666
680
  if (lastChunk) currentThinkingClosed = true
667
681
  messages.scrollTop = messages.scrollHeight
668
682
  }
669
683
 
670
- function addThinkingStep(artifactId, text, lastChunk) {
684
+ function addThinkingStep(artifactId, text, lastChunk, append) {
671
685
  if (
672
686
  pendingThinkingUpdate &&
673
687
  pendingThinkingUpdate.artifactId === artifactId &&
@@ -677,7 +691,7 @@
677
691
  pendingThinkingUpdate.lastChunk = lastChunk
678
692
  } else {
679
693
  flushPendingThinkingUpdate()
680
- pendingThinkingUpdate = { artifactId, text, lastChunk }
694
+ pendingThinkingUpdate = { artifactId, text, lastChunk, append }
681
695
  }
682
696
 
683
697
  if (pendingThinkingTimer) clearTimeout(pendingThinkingTimer)
@@ -808,31 +822,41 @@
808
822
  setTimeout(finish, THINKING_SUMMARY_ANIMATION_MS + 80)
809
823
  }
810
824
 
811
- function addApprovalPrompt(description) {
825
+ function addApprovalPrompt(description, options, actionRequest) {
812
826
  const el = document.createElement("div")
813
827
  el.className = "msg approval"
814
- el.innerHTML = md(description || "The agent requires your approval to continue.")
828
+ const content = document.createElement("div")
829
+ content.textContent = actionRequest?.name
830
+ ? `Tool execution requires approval\n\nTool: ${actionRequest.name}`
831
+ : description || "The agent requires your approval to continue."
832
+ el.appendChild(content)
833
+ content.innerHTML = md(content.textContent)
834
+ if (actionRequest?.args !== undefined) {
835
+ const json = document.createElement("pre")
836
+ json.textContent = JSON.stringify(actionRequest.args, null, 2)
837
+ el.appendChild(json)
838
+ }
815
839
  const actions = document.createElement("div")
816
840
  actions.className = "approval-actions"
817
841
 
818
- const approveBtn = document.createElement("button")
819
- approveBtn.className = "btn-approve"
820
- approveBtn.textContent = "Approve"
821
- approveBtn.addEventListener("click", () => {
822
- removeApprovalPrompt(el)
823
- resume("approve")
824
- })
825
-
826
- const rejectBtn = document.createElement("button")
827
- rejectBtn.className = "btn-reject"
828
- rejectBtn.textContent = "Reject"
829
- rejectBtn.addEventListener("click", () => {
830
- removeApprovalPrompt(el)
831
- resume("reject")
832
- })
833
-
834
- actions.appendChild(approveBtn)
835
- actions.appendChild(rejectBtn)
842
+ const choices =
843
+ Array.isArray(options) && options.length
844
+ ? options
845
+ : [
846
+ { value: "approve", label: "Approve" },
847
+ { value: "reject", label: "Reject" },
848
+ ]
849
+ for (const option of choices) {
850
+ if (typeof option?.value !== "string" || typeof option?.label !== "string") continue
851
+ const button = document.createElement("button")
852
+ button.className = option.value === "reject" ? "btn-reject" : "btn-approve"
853
+ button.textContent = option.label
854
+ button.addEventListener("click", () => {
855
+ removeApprovalPrompt(el)
856
+ resume(option.value)
857
+ })
858
+ actions.appendChild(button)
859
+ }
836
860
  el.appendChild(actions)
837
861
  messages.insertBefore(el, typing.parentElement === messages ? typing : statusText)
838
862
  messages.scrollTop = messages.scrollHeight
@@ -924,8 +948,12 @@
924
948
  pendingTaskId = taskId
925
949
  const msgParts = result?.status?.message?.parts ?? []
926
950
  const description = partsToText(msgParts)
951
+ const options =
952
+ result?.status?.message?.metadata?.["sap.cds.agents.input-required"]?.options
953
+ const actionRequest = msgParts.find((part) => part.kind === "data")?.data
954
+ ?.actionRequests?.[0]
927
955
  flushPendingThinkingUpdate()
928
- addApprovalPrompt(description)
956
+ addApprovalPrompt(description, options, actionRequest)
929
957
  return
930
958
  }
931
959
 
@@ -1100,7 +1128,8 @@
1100
1128
 
1101
1129
  if (event.artifact?.artifactId?.startsWith("thinking")) {
1102
1130
  const lastChunk = event.lastChunk ?? false
1103
- if (text) addThinkingStep(event.artifact.artifactId, text, lastChunk)
1131
+ if (text)
1132
+ addThinkingStep(event.artifact.artifactId, text, lastChunk, event.append ?? false)
1104
1133
  return null
1105
1134
  }
1106
1135
 
@@ -62,7 +62,7 @@ export function _patchChatModelProto(proto) {
62
62
  const provider = _resolveProvider()
63
63
  const node = opts?.runName || "agent"
64
64
  const messages = Array.isArray(input) ? input : undefined
65
- const cacheControl = _detectCacheControl(messages)
65
+ const cacheControl = _detectCacheRequest({ messages, opts, modelOptions: this.options })
66
66
  const streaming = this.streaming || this.orchestrationConfig?.streaming || false
67
67
  const mAttrs = _metricAttrs(model, node)
68
68
 
@@ -162,8 +162,18 @@ function _metricAttrs(model, node) {
162
162
  }
163
163
  }
164
164
 
165
- /** Detect cache_control presence in messages (set by injectCacheControl in aicore). */
166
- function _detectCacheControl(messages) {
165
+ function _detectCacheRequest({ messages, opts, modelOptions }) {
166
+ const params = modelOptions?.params
167
+ return Boolean(
168
+ _messagesHaveCacheControl(messages) ||
169
+ opts?.cache_control ||
170
+ modelOptions?.promptCaching ||
171
+ params?.prompt_cache_retention ||
172
+ params?.prompt_cache_options,
173
+ )
174
+ }
175
+
176
+ function _messagesHaveCacheControl(messages) {
167
177
  if (!Array.isArray(messages)) return false
168
178
  return messages.some((m) => {
169
179
  if (!Array.isArray(m.content)) return false
@@ -53,6 +53,12 @@ export const llmInvocations = meter.createCounter("agent.llm.invocations", {
53
53
  export const toolInvocations = meter.createCounter("agent.tool.invocations", {
54
54
  description: "Tool invocation count",
55
55
  })
56
+ export const hitlGates = meter.createCounter("agent.hitl.gates", {
57
+ description: "HITL action gates requested",
58
+ })
59
+ export const hitlDecisions = meter.createCounter("agent.hitl.decisions", {
60
+ description: "HITL decisions by action",
61
+ })
56
62
 
57
63
  /** Common attributes for all agent metrics */
58
64
  export function attrs(srv) {
@@ -11,7 +11,7 @@ export class DatabricksExporter extends MlflowExporter {
11
11
  ) {
12
12
  const { uc, warehouseId } = this._creds
13
13
  const ucPrefix = `${uc.catalog}.${uc.schema}.${uc.tablePrefix}`
14
- let url = `/api/4.0/mlflow/traces/${encodeURIComponent(ucPrefix)}/tr-${traceId}/assessments`
14
+ let url = `/api/4.0/mlflow/traces/${encodeURIComponent(ucPrefix)}/${traceId}/assessments`
15
15
  if (warehouseId) url += `?sql_warehouse_id=${encodeURIComponent(warehouseId)}`
16
16
  await this._fetch(url, {
17
17
  trace_id: traceId,
@@ -71,9 +71,9 @@ export function syncSystemPrompt(messages) {
71
71
  export function resolvePromptName(srv) {
72
72
  if (!srv?.name) return ""
73
73
  const agentDir = resolveAgentDir(srv)
74
- if (!agentDir) return srv.name
74
+ if (!agentDir) return _mlflowName(srv.name)
75
75
  const root = cds.root ?? process.cwd()
76
- return relative(root, join(agentDir, "AGENTS.md")).replace(/\\/g, "/")
76
+ return _mlflowName(relative(root, join(agentDir, "AGENTS.md")).replace(/\\/g, "/"))
77
77
  }
78
78
 
79
79
  export function hashPrompt(text) {
@@ -88,6 +88,10 @@ function _description(name) {
88
88
  return `System prompt for CAP agent service "${name.split("/")[0]}"`
89
89
  }
90
90
 
91
+ function _mlflowName(name) {
92
+ return String(name).replace(/[\\/.:]/g, "_")
93
+ }
94
+
91
95
  function _registrationTags() {
92
96
  return [{ key: "mlflow.prompt.is_prompt", value: "true" }]
93
97
  }
@@ -0,0 +1,132 @@
1
+ import cds from "@sap/cds"
2
+
3
+ export const PROMPT_CACHE_MODEL_PARAMS = Symbol("promptCacheModelParams")
4
+
5
+ const CACHE_CONTROL_EPHEMERAL = { type: "ephemeral" }
6
+
7
+ export function isPromptCachingModel(model) {
8
+ return promptCachingMode(model) !== undefined
9
+ }
10
+
11
+ export function withPromptCachingParams(model, params) {
12
+ if (params !== undefined && (typeof params !== "object" || Array.isArray(params))) return params
13
+ const mode = promptCachingMode(model)
14
+ const base = params || {}
15
+ if (mode === "gpt-implicit") {
16
+ return {
17
+ ...base,
18
+ ...(base.prompt_cache_options === undefined
19
+ ? { prompt_cache_options: { mode: "implicit", ttl: "30m" } }
20
+ : {}),
21
+ }
22
+ }
23
+ if (mode === "gpt-retention") {
24
+ return {
25
+ ...base,
26
+ ...(base.prompt_cache_retention === undefined ? { prompt_cache_retention: "24h" } : {}),
27
+ }
28
+ }
29
+ return params
30
+ }
31
+
32
+ export function withPromptCachingMessages(model, messages, opts) {
33
+ if (promptCachingMode(model) !== "claude") return { messages, opts }
34
+
35
+ const cachedMessages = [...messages]
36
+ addCacheControlToLast(cachedMessages, "system")
37
+ addCacheControlToLast(cachedMessages, "ai")
38
+ addCacheControlToLast(cachedMessages, "human")
39
+
40
+ if (!opts?.tools?.length) return { messages: cachedMessages, opts }
41
+ const tools = [...opts.tools]
42
+ tools[tools.length - 1] = { ...tools[tools.length - 1], cache_control: CACHE_CONTROL_EPHEMERAL }
43
+ return { messages: cachedMessages, opts: { ...opts, tools } }
44
+ }
45
+
46
+ export function withPromptCachingOptions(model, opts) {
47
+ const mode = promptCachingMode(model)
48
+ if (mode === "nova" && !opts?.cache_control) {
49
+ return { ...opts, cache_control: CACHE_CONTROL_EPHEMERAL }
50
+ }
51
+ if (!isGptCachingMode(mode)) return opts
52
+
53
+ const params = opts?.[PROMPT_CACHE_MODEL_PARAMS] || {}
54
+ if (params.prompt_cache_key !== undefined) return opts
55
+ return {
56
+ ...opts,
57
+ [PROMPT_CACHE_MODEL_PARAMS]: {
58
+ ...params,
59
+ prompt_cache_key: opts?.prompt_cache_key ?? buildPromptCacheKey(),
60
+ },
61
+ }
62
+ }
63
+
64
+ function buildPromptCacheKey() {
65
+ return cds.context?.tenant || ""
66
+ }
67
+
68
+ function promptCachingMode(model) {
69
+ const name = String(model || "")
70
+ if (/anthropic|claude/i.test(name)) return "claude"
71
+ if (/(?:^|[-_])nova(?:[-_]|$)/i.test(name) || /amazon.*nova/i.test(name)) return "nova"
72
+
73
+ const version = gptVersion(name)
74
+ if (!version) return undefined
75
+ if (version.major > 5 || (version.major === 5 && version.minor >= 6)) return "gpt-implicit"
76
+ if ((version.major === 5 && version.minor < 6) || (version.major === 4 && version.minor === 1)) {
77
+ return "gpt-retention"
78
+ }
79
+ }
80
+
81
+ function isGptCachingMode(mode) {
82
+ return mode === "gpt-implicit" || mode === "gpt-retention"
83
+ }
84
+
85
+ function gptVersion(model) {
86
+ const match = model.match(/(?:^|[^a-z0-9])gpt[-_]?([0-9]+)(?:[._-]([0-9]+))?/i)
87
+ return match && { major: Number(match[1]), minor: Number(match[2] || 0) }
88
+ }
89
+
90
+ function addCacheControlToLast(messages, type) {
91
+ for (let index = messages.length - 1; index >= 0; index--) {
92
+ if (messageType(messages[index]) === type && hasTextContent(messages[index])) {
93
+ messages[index] = withCacheControl(messages[index])
94
+ return
95
+ }
96
+ }
97
+ }
98
+
99
+ function messageType(message) {
100
+ return message._getType?.() || message.type
101
+ }
102
+
103
+ function hasTextContent(message) {
104
+ return (
105
+ (typeof message.content === "string" && message.content.length > 0) ||
106
+ (Array.isArray(message.content) &&
107
+ message.content.some((block) => block?.type === "text" && block.text?.length > 0))
108
+ )
109
+ }
110
+
111
+ function withCacheControl(message) {
112
+ const content = message.content
113
+ if (typeof content === "string") {
114
+ return cloneMessage(message, [
115
+ { type: "text", text: content, cache_control: CACHE_CONTROL_EPHEMERAL },
116
+ ])
117
+ }
118
+ if (!Array.isArray(content)) return message
119
+
120
+ for (let index = content.length - 1; index >= 0; index--) {
121
+ if (content[index]?.type === "text") {
122
+ const nextContent = [...content]
123
+ nextContent[index] = { ...nextContent[index], cache_control: CACHE_CONTROL_EPHEMERAL }
124
+ return cloneMessage(message, nextContent)
125
+ }
126
+ }
127
+ return message
128
+ }
129
+
130
+ function cloneMessage(message, content) {
131
+ return Object.assign(Object.create(Object.getPrototypeOf(message)), message, { content })
132
+ }
@@ -1,3 +1,17 @@
1
+ import { randomUUID } from "node:crypto"
2
+
3
+ export function agentMessage(text, data, metadata) {
4
+ const parts = [{ kind: "text", text }]
5
+ if (data && typeof data === "object") parts.push({ kind: "data", data })
6
+ return {
7
+ kind: "message",
8
+ messageId: randomUUID(),
9
+ role: "agent",
10
+ parts,
11
+ ...(metadata && { metadata }),
12
+ }
13
+ }
14
+
1
15
  /**
2
16
  * A2A message part utilities — handle both wire formats:
3
17
  *
@@ -10,12 +10,6 @@ function resolveI18n(value, locale) {
10
10
  return value
11
11
  }
12
12
 
13
- export function getAgentLogger(srv) {
14
- const slug = slugified(srv.name)
15
- const agentSlug = slug.endsWith("-agent") ? slug : `${slug}-agent`
16
- return cds.log("agents:" + srv.name, { label: agentSlug })
17
- }
18
-
19
13
  /**
20
14
  * Mirrors CAP's internal slug rules used for service path generation.
21
15
  */
@@ -57,10 +51,11 @@ export function getFilteredEntities(srv) {
57
51
  Object.entries(srv.entities || {})
58
52
  .filter(
59
53
  ([name, entity]) =>
60
- !(entity["@cds.autoexposed"] && !entity["@cds.autoexpose"]) &&
54
+ !entity["@cds.autoexposed"] &&
55
+ !entity["@cds.api.ignore"] &&
61
56
  !name.endsWith("DraftAdministrativeData") &&
62
- !name.endsWith(".texts") &&
63
- !entity["@cds.api.ignore"],
57
+ !name.endsWith(".drafts") &&
58
+ !name.endsWith(".texts"),
64
59
  )
65
60
  .sort(([a], [b]) => a.localeCompare(b)),
66
61
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/agents",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "CDS plugin for building agents",
5
5
  "author": "SAP SE (https://www.sap.com)",
6
6
  "license": "Apache-2.0",
@@ -41,7 +41,7 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@a2a-js/sdk": "^0.3.12",
44
- "@cap-js/attachments": "^4",
44
+ "@cap-js/attachments": ">=3",
45
45
  "@cap-js/mcp": "^1.4",
46
46
  "@langchain/anthropic": "^1.5.1",
47
47
  "@langchain/core": "^1",
@@ -147,7 +147,7 @@
147
147
  "image/jpeg"
148
148
  ]
149
149
  },
150
- "pool": {
150
+ "quotas": {
151
151
  "maxConcurrentTasks": 10,
152
152
  "maxConcurrentTasksPerUser": 4,
153
153
  "maxTasksPerHour": 100,
@@ -159,7 +159,6 @@
159
159
  "maxLLMTokensPerTask": 200000,
160
160
  "maxLLMCallTimeout": "120s",
161
161
  "maxExecutionTimePerTask": "5min",
162
- "timeoutGrace": "15s",
163
162
  "maxIncomingMessageLength": 5000
164
163
  }
165
164
  },
@@ -190,7 +189,7 @@
190
189
  }
191
190
  },
192
191
  "[development]": {
193
- "llm": "mock"
192
+ "llm": "auto"
194
193
  },
195
194
  "[with-claude]": {
196
195
  "llm": "anthropic"
@@ -217,6 +216,9 @@
217
216
  "_javaHcqlCompat": true
218
217
  }
219
218
  }
219
+ },
220
+ "folders": {
221
+ "srvs": "srv/*"
220
222
  }
221
223
  },
222
224
  "workspaces": [
@@ -73,7 +73,7 @@ function toolCallsFromMessages(messages) {
73
73
  for (const msg of messages) {
74
74
  if (msg.tool_call_id && msg.type === "tool") {
75
75
  const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)
76
- resultById.set(msg.tool_call_id, content)
76
+ resultById.set(msg.tool_call_id, { content, ...msg })
77
77
  }
78
78
  }
79
79
 
@@ -84,14 +84,14 @@ function toolCallsFromMessages(messages) {
84
84
  const raw = resultById.get(tc.id)
85
85
  let toolResult
86
86
  try {
87
- toolResult = raw !== undefined ? JSON.parse(raw) : undefined
87
+ toolResult = raw?.content !== undefined ? JSON.parse(raw.content) : undefined
88
88
  } catch {
89
- toolResult = raw
89
+ toolResult = raw?.content
90
90
  }
91
91
  const entry = {
92
92
  tool: tc.name,
93
93
  args: tc.args ?? {},
94
- outcome: "success",
94
+ outcome: raw.status,
95
95
  ...(toolResult !== undefined && { result: toolResult }),
96
96
  }
97
97
  attachCqn(entry)