@cap-js/agents 0.9.4 → 0.9.6
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/index.js +1 -1
- package/lib/agents/middleware/quota-enforcer.js +8 -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 +4 -9
- 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 -82
- package/lib/preview/chat.html +47 -22
- package/lib/protocol/agent-card.js +2 -6
- 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/utils.js +2 -39
- package/package.json +5 -6
- package/srv/handlers/graph-executor/hitl.js +121 -3
- package/srv/handlers/graph-executor.js +46 -91
- package/srv/handlers/index.js +2 -2
- package/srv/handlers/mcp-tools.js +3 -2
- package/srv/handlers/{sub-agent-tools.js → subagent-tools.js} +26 -16
- package/srv/handlers/tools.js +7 -10
package/lib/eval/eval-run.js
CHANGED
|
@@ -70,15 +70,20 @@ async function _flushValidations(state) {
|
|
|
70
70
|
tasks.push(
|
|
71
71
|
flushMlflowTraces().then(() =>
|
|
72
72
|
Promise.all([
|
|
73
|
-
postMlflowAssessment(
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
postMlflowAssessment(
|
|
74
|
+
traceId,
|
|
75
|
+
success_rate,
|
|
76
|
+
"",
|
|
77
|
+
"success_rate",
|
|
78
|
+
"cap-js/agents",
|
|
79
|
+
codeOpts,
|
|
80
|
+
).catch(() => {}),
|
|
76
81
|
postMlflowAssessment(
|
|
77
82
|
traceId,
|
|
78
83
|
output_correctness,
|
|
79
84
|
"",
|
|
80
85
|
"output_correctness",
|
|
81
|
-
|
|
86
|
+
"cap-js/agents",
|
|
82
87
|
codeOpts,
|
|
83
88
|
).catch(() => {}),
|
|
84
89
|
]),
|
|
@@ -124,7 +129,7 @@ async function _postAssessmentScore(result, score, comment, config) {
|
|
|
124
129
|
score,
|
|
125
130
|
comment ?? "",
|
|
126
131
|
config.assessmentName,
|
|
127
|
-
config.model ??
|
|
132
|
+
config.model ?? config.assessmentName ?? "judge",
|
|
128
133
|
{ ...opts, metadata: { "mlflow.trace.session": config.sessionId ?? "" } },
|
|
129
134
|
)
|
|
130
135
|
} else {
|
|
@@ -134,7 +139,7 @@ async function _postAssessmentScore(result, score, comment, config) {
|
|
|
134
139
|
score,
|
|
135
140
|
comment ?? "",
|
|
136
141
|
config.assessmentName,
|
|
137
|
-
config.model ??
|
|
142
|
+
config.model ?? config.assessmentName ?? "judge",
|
|
138
143
|
opts,
|
|
139
144
|
)
|
|
140
145
|
}
|
package/lib/index.js
CHANGED
|
@@ -15,6 +15,9 @@ import * as metrics from "./telemetry/metrics.js"
|
|
|
15
15
|
|
|
16
16
|
const LOG = cds.log("agents")
|
|
17
17
|
|
|
18
|
+
const TRUNCATE = cds.env.agents?.truncate || 111
|
|
19
|
+
const truncated = (msg) => (msg?.length > TRUNCATE ? msg.slice(0, TRUNCATE) + "..." : msg)
|
|
20
|
+
|
|
18
21
|
// SSE wire-format helpers, mirroring @a2a-js/sdk's sse_utils so the adapter
|
|
19
22
|
// streams message/stream responses identically to the SDK's reference
|
|
20
23
|
// Express handler. Inlined (not imported) because the SDK does not export
|
|
@@ -215,11 +218,10 @@ export default function A2AProtocolAdapter(srv, options = {}) {
|
|
|
215
218
|
if (method === "message/send" || method === "message/stream") {
|
|
216
219
|
const text = userText
|
|
217
220
|
|
|
218
|
-
const maxLen = cds.env.agents?.
|
|
221
|
+
const maxLen = cds.env.agents?.quotas?.maxIncomingMessageLength
|
|
219
222
|
if (maxLen > 0 && text?.length > maxLen) {
|
|
220
|
-
LOG.warn("message too long", {
|
|
223
|
+
LOG.warn(srv.name, "message too long", {
|
|
221
224
|
conversation: short(contextId),
|
|
222
|
-
service: srv.name,
|
|
223
225
|
})
|
|
224
226
|
metrics.errorsTotal.add(1, { ...requestAttrs, "agent.error.code": 400 })
|
|
225
227
|
|
|
@@ -245,15 +247,13 @@ export default function A2AProtocolAdapter(srv, options = {}) {
|
|
|
245
247
|
return
|
|
246
248
|
}
|
|
247
249
|
|
|
248
|
-
|
|
249
|
-
LOG.info("request", {
|
|
250
|
+
LOG.info(srv.name, "request", {
|
|
250
251
|
conversation: short(contextId),
|
|
251
|
-
service: srv.name,
|
|
252
252
|
method,
|
|
253
|
-
text: truncated,
|
|
253
|
+
text: truncated(text),
|
|
254
254
|
})
|
|
255
255
|
} else {
|
|
256
|
-
LOG.debug("request", { conversation: short(contextId),
|
|
256
|
+
LOG.debug(srv.name, "request", { conversation: short(contextId), method })
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
try {
|
|
@@ -264,9 +264,8 @@ export default function A2AProtocolAdapter(srv, options = {}) {
|
|
|
264
264
|
await import("./agents/quota-enforcer-at-start.js")
|
|
265
265
|
const quotaResult = await quotaEnforcerAtStart()
|
|
266
266
|
if (quotaResult) {
|
|
267
|
-
LOG.warn("quota exceeded", {
|
|
267
|
+
LOG.warn(srv.name, "quota exceeded", {
|
|
268
268
|
conversation: short(contextId),
|
|
269
|
-
service: srv.name,
|
|
270
269
|
reason: quotaResult.message,
|
|
271
270
|
})
|
|
272
271
|
metrics.errorsTotal.add(1, { ...requestAttrs, "agent.error.code": 429 })
|
|
@@ -416,8 +415,6 @@ export default function A2AProtocolAdapter(srv, options = {}) {
|
|
|
416
415
|
})
|
|
417
416
|
})
|
|
418
417
|
|
|
419
|
-
LOG.debug("Adapter initialized", { service: srv.name })
|
|
420
|
-
|
|
421
418
|
router.router = router
|
|
422
419
|
return router
|
|
423
420
|
}
|
package/lib/models/aicore.js
CHANGED
|
@@ -2,24 +2,53 @@ import cds from "@sap/cds"
|
|
|
2
2
|
import { OrchestrationClient } from "@sap-ai-sdk/langchain"
|
|
3
3
|
import { circuitBreaker, timeout } from "../utils/resilience.js"
|
|
4
4
|
|
|
5
|
-
import { SystemMessage, ToolMessage,
|
|
5
|
+
import { SystemMessage, ToolMessage, AIMessage } from "@langchain/core/messages"
|
|
6
6
|
import { ms4 } from "../utils/utils.js"
|
|
7
|
+
import {
|
|
8
|
+
isPromptCachingModel,
|
|
9
|
+
PROMPT_CACHE_MODEL_PARAMS,
|
|
10
|
+
withPromptCachingMessages,
|
|
11
|
+
withPromptCachingOptions,
|
|
12
|
+
withPromptCachingParams,
|
|
13
|
+
} from "../utils/caching.js"
|
|
7
14
|
import { syncSystemPrompt } from "../telemetry/mlflow/prompts.js"
|
|
8
15
|
|
|
9
16
|
const LOG = cds.log("agents")
|
|
10
17
|
const DEFAULT_STREAM_DELIMITERS = [".", "!", "?"]
|
|
11
18
|
|
|
19
|
+
function noTemp0Support(model) {
|
|
20
|
+
switch (model) {
|
|
21
|
+
case "gpt-5.6-sol":
|
|
22
|
+
case "gpt-5.6-luna":
|
|
23
|
+
case "gpt-5.6-terra":
|
|
24
|
+
case "gpt-6-astra":
|
|
25
|
+
return true
|
|
26
|
+
default:
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const llmError = (err) => {
|
|
32
|
+
LOG.error(`AI Core request failed!`, err.rootCause || err)
|
|
33
|
+
cds.error(cds.i18n.messages.at("LLM_UNAVAILABLE"))
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
class _InstrumentedOrchestrationClient extends OrchestrationClient {
|
|
13
37
|
constructor(name, options) {
|
|
14
38
|
const { model, deepAgent, streaming, destinationName, resourceGroup } = options
|
|
15
39
|
const flatten = options.flatten ?? (deepAgent ? true : false)
|
|
16
40
|
|
|
17
|
-
//
|
|
41
|
+
// params resolution: caller > deep-agent default > root
|
|
18
42
|
// cds.env.agents.params. Without this, AI Core silently applies its own
|
|
19
43
|
// defaults (temperature ≈ 1, verbose model → repeated max_tokens truncation
|
|
20
|
-
// and extra ReAct iterations
|
|
21
|
-
const
|
|
44
|
+
// and extra ReAct iterations).
|
|
45
|
+
const rawParams =
|
|
22
46
|
options.params || (deepAgent ? { max_tokens: 4096, temperature: 0 } : cds.env.agents?.params)
|
|
47
|
+
const params = withPromptCachingParams(model, rawParams)
|
|
48
|
+
if (params.temperature === 0 && noTemp0Support(model)) {
|
|
49
|
+
delete params.temperature
|
|
50
|
+
}
|
|
51
|
+
|
|
23
52
|
const auditParams = params && typeof params === "object" ? { ...params } : params
|
|
24
53
|
|
|
25
54
|
LOG.debug("Initializing LLM", { model, deepAgent: !!deepAgent })
|
|
@@ -65,14 +94,24 @@ class _InstrumentedOrchestrationClient extends OrchestrationClient {
|
|
|
65
94
|
destination,
|
|
66
95
|
)
|
|
67
96
|
this.name = name
|
|
68
|
-
this.options = {
|
|
97
|
+
this.options = {
|
|
98
|
+
...options,
|
|
99
|
+
params: auditParams,
|
|
100
|
+
contentFilter,
|
|
101
|
+
flatten,
|
|
102
|
+
promptCaching: isPromptCachingModel(model),
|
|
103
|
+
}
|
|
69
104
|
}
|
|
70
105
|
|
|
71
106
|
async _generate(messages, opts, runManager) {
|
|
72
107
|
const { model, flatten } = this.options
|
|
73
108
|
opts = _withMiddleware(this, opts)
|
|
74
109
|
const prepared = _prepareMessages(messages, { flatten, model, opts })
|
|
75
|
-
|
|
110
|
+
try {
|
|
111
|
+
return super._generate(prepared.inputMessages, prepared.opts, runManager)
|
|
112
|
+
} catch (err) {
|
|
113
|
+
llmError(err)
|
|
114
|
+
}
|
|
76
115
|
}
|
|
77
116
|
|
|
78
117
|
/**
|
|
@@ -80,8 +119,8 @@ class _InstrumentedOrchestrationClient extends OrchestrationClient {
|
|
|
80
119
|
*
|
|
81
120
|
* 1. Message flattening / normalization — same as _generate: content arrays
|
|
82
121
|
* must be reduced to text before reaching AI Core's assistant template.
|
|
83
|
-
* 2.
|
|
84
|
-
*
|
|
122
|
+
* 2. Prompt caching options — same as _generate, or caching is skipped on
|
|
123
|
+
* the streaming path.
|
|
85
124
|
* 3. Content extraction — @sap-ai-sdk/langchain getDeltaContent() only handles
|
|
86
125
|
* string deltas; Anthropic streams content-block arrays (and reasoning in a
|
|
87
126
|
* sibling field), so we re-extract and re-emit them via buildStreamBlocks().
|
|
@@ -94,17 +133,44 @@ class _InstrumentedOrchestrationClient extends OrchestrationClient {
|
|
|
94
133
|
opts = withDefaultStreamDelimiters(prepared.opts)
|
|
95
134
|
|
|
96
135
|
let turnHasToolCall = false
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
136
|
+
try {
|
|
137
|
+
for await (const chunk of super._streamResponseChunks(inputMessages, opts, runManager)) {
|
|
138
|
+
if (chunk.message?.tool_call_chunks?.length > 0) turnHasToolCall = true
|
|
139
|
+
const text = chunk.text || extractTextFromContentBlocks(chunk)
|
|
140
|
+
const reasoning = extractReasoningFromChunk(chunk)
|
|
141
|
+
const blocks = buildStreamBlocks(text, reasoning, turnHasToolCall)
|
|
142
|
+
|
|
143
|
+
if (!blocks.length && !(text && turnHasToolCall)) {
|
|
144
|
+
yield chunk
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
yield patchChunkContent(chunk, blocks)
|
|
106
148
|
}
|
|
107
|
-
|
|
149
|
+
} catch (err) {
|
|
150
|
+
llmError(err)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
mergeOrchestrationConfig(orchestrationConfig, options) {
|
|
155
|
+
const config = super.mergeOrchestrationConfig(orchestrationConfig, options)
|
|
156
|
+
const modelParams = options?.[PROMPT_CACHE_MODEL_PARAMS]
|
|
157
|
+
if (!modelParams) return config
|
|
158
|
+
const currentParams = config.promptTemplating?.model?.params || {}
|
|
159
|
+
const nextParams = { ...modelParams }
|
|
160
|
+
if (currentParams.prompt_cache_key !== undefined) delete nextParams.prompt_cache_key
|
|
161
|
+
if (!Object.keys(nextParams).length) return config
|
|
162
|
+
return {
|
|
163
|
+
...config,
|
|
164
|
+
promptTemplating: {
|
|
165
|
+
...config.promptTemplating,
|
|
166
|
+
model: {
|
|
167
|
+
...config.promptTemplating?.model,
|
|
168
|
+
params: {
|
|
169
|
+
...currentParams,
|
|
170
|
+
...nextParams,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
108
174
|
}
|
|
109
175
|
}
|
|
110
176
|
}
|
|
@@ -113,15 +179,11 @@ export default _InstrumentedOrchestrationClient
|
|
|
113
179
|
|
|
114
180
|
// ─── SDK helpers ─────────────────────────────────────────────────────────────
|
|
115
181
|
|
|
116
|
-
function isClaude(model) {
|
|
117
|
-
return /anthropic|claude/i.test(model || "")
|
|
118
|
-
}
|
|
119
|
-
|
|
120
182
|
/**
|
|
121
183
|
* Inject timeout and circuit breaker middleware into opts.
|
|
122
184
|
*/
|
|
123
185
|
function _withMiddleware(instance, opts) {
|
|
124
|
-
const llmTimeout = ms4(cds.env.agents?.
|
|
186
|
+
const llmTimeout = ms4(cds.env.agents?.quotas?.maxLLMCallTimeout || "120s")
|
|
125
187
|
const middleware = [timeout(llmTimeout), circuitBreaker()]
|
|
126
188
|
return {
|
|
127
189
|
...opts,
|
|
@@ -130,23 +192,16 @@ function _withMiddleware(instance, opts) {
|
|
|
130
192
|
}
|
|
131
193
|
|
|
132
194
|
/**
|
|
133
|
-
* Prepare input messages: flatten, normalize assistant content,
|
|
134
|
-
*
|
|
195
|
+
* Prepare input messages: flatten, normalize assistant content, and set prompt
|
|
196
|
+
* caching call options/model params.
|
|
135
197
|
*/
|
|
136
198
|
function _prepareMessages(messages, { flatten, model, opts }) {
|
|
137
199
|
let inputMessages = flatten ? flattenMessages(messages) : messages
|
|
138
200
|
inputMessages = normalizeAssistantContent(inputMessages)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
tools[tools.length - 1] = {
|
|
144
|
-
...tools[tools.length - 1],
|
|
145
|
-
cache_control: CACHE_CONTROL_EPHEMERAL,
|
|
146
|
-
}
|
|
147
|
-
opts = { ...opts, tools }
|
|
148
|
-
}
|
|
149
|
-
}
|
|
201
|
+
const cached = withPromptCachingMessages(model, inputMessages, opts)
|
|
202
|
+
inputMessages = cached.messages
|
|
203
|
+
opts = cached.opts
|
|
204
|
+
opts = withPromptCachingOptions(model, opts)
|
|
150
205
|
syncSystemPrompt(inputMessages)
|
|
151
206
|
return { inputMessages, opts }
|
|
152
207
|
}
|
|
@@ -164,97 +219,6 @@ export function withDefaultStreamDelimiters(opts = {}) {
|
|
|
164
219
|
}
|
|
165
220
|
}
|
|
166
221
|
|
|
167
|
-
// Claude currently only supports caching of type ephemeral. TTL can differ between 5min or 1h but
|
|
168
|
-
// we use the 5min default
|
|
169
|
-
const CACHE_CONTROL_EPHEMERAL = { type: "ephemeral" }
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Marks: all system messages, the last AI message (with text content), and the last human message.
|
|
173
|
-
* Converts string content to content-block arrays where needed so cache_control
|
|
174
|
-
* can be attached per the Anthropic/SAP AI Core API format.
|
|
175
|
-
*/
|
|
176
|
-
function injectCacheControl(messages) {
|
|
177
|
-
if (!messages || messages.length === 0) return messages
|
|
178
|
-
|
|
179
|
-
const result = messages.map((m) => {
|
|
180
|
-
// Clone to avoid mutating original
|
|
181
|
-
if (m._getType?.() === "system" || m.type === "system") {
|
|
182
|
-
return _withCacheControl(m)
|
|
183
|
-
}
|
|
184
|
-
return m
|
|
185
|
-
})
|
|
186
|
-
// Mark last AI message with non-empty text content (stable breakpoint for multi-turn)
|
|
187
|
-
for (let i = result.length - 1; i >= 0; i--) {
|
|
188
|
-
const type = result[i]._getType?.() || result[i].type
|
|
189
|
-
if (type === "ai" && _hasTextContent(result[i])) {
|
|
190
|
-
result[i] = _withCacheControl(result[i])
|
|
191
|
-
break
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// Mark last human message
|
|
195
|
-
for (let i = result.length - 1; i >= 0; i--) {
|
|
196
|
-
const type = result[i]._getType?.() || result[i].type
|
|
197
|
-
if (type === "human") {
|
|
198
|
-
result[i] = _withCacheControl(result[i])
|
|
199
|
-
break
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return result
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Check if a message has non-empty text content (not just tool_calls).
|
|
207
|
-
*/
|
|
208
|
-
function _hasTextContent(msg) {
|
|
209
|
-
const content = msg.content
|
|
210
|
-
if (typeof content === "string") return content.length > 0
|
|
211
|
-
if (Array.isArray(content)) return content.some((b) => b.type === "text" && b.text?.length > 0)
|
|
212
|
-
return false
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* If content is a string, convert to [{type:"text", text, cache_control}].
|
|
217
|
-
* If content is an array, add cache_control to the last text block.
|
|
218
|
-
*/
|
|
219
|
-
function _withCacheControl(msg) {
|
|
220
|
-
const content = msg.content
|
|
221
|
-
if (typeof content === "string") {
|
|
222
|
-
// Convert to content blocks with cache_control on the block
|
|
223
|
-
const newContent = [{ type: "text", text: content, cache_control: CACHE_CONTROL_EPHEMERAL }]
|
|
224
|
-
return _cloneMessageWithContent(msg, newContent)
|
|
225
|
-
}
|
|
226
|
-
if (Array.isArray(content) && content.length > 0) {
|
|
227
|
-
const newContent = [...content]
|
|
228
|
-
// Find last text block and add cache_control
|
|
229
|
-
for (let i = newContent.length - 1; i >= 0; i--) {
|
|
230
|
-
if (newContent[i].type === "text") {
|
|
231
|
-
newContent[i] = { ...newContent[i], cache_control: CACHE_CONTROL_EPHEMERAL }
|
|
232
|
-
break
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return _cloneMessageWithContent(msg, newContent)
|
|
236
|
-
}
|
|
237
|
-
return msg
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function _cloneMessageWithContent(msg, newContent) {
|
|
241
|
-
const type = msg._getType?.() || msg.type
|
|
242
|
-
if (type === "system") {
|
|
243
|
-
return new SystemMessage({ content: newContent })
|
|
244
|
-
}
|
|
245
|
-
if (type === "ai") {
|
|
246
|
-
return new AIMessage({ content: newContent, tool_calls: msg.tool_calls })
|
|
247
|
-
}
|
|
248
|
-
if (type === "human") {
|
|
249
|
-
return new HumanMessage({ content: newContent })
|
|
250
|
-
}
|
|
251
|
-
if (type === "tool") {
|
|
252
|
-
return new ToolMessage({ content: newContent, tool_call_id: msg.tool_call_id, name: msg.name })
|
|
253
|
-
}
|
|
254
|
-
// Fallback: shallow clone with new content
|
|
255
|
-
return { ...msg, content: newContent }
|
|
256
|
-
}
|
|
257
|
-
|
|
258
222
|
export function flattenMessages(messages) {
|
|
259
223
|
return messages.map((m) => {
|
|
260
224
|
if (!Array.isArray(m.content)) return m
|
package/lib/models/anthropic.js
CHANGED
|
@@ -1,92 +1,20 @@
|
|
|
1
1
|
import { ChatAnthropic } from '@langchain/anthropic'
|
|
2
|
-
import
|
|
3
|
-
import fs from 'node:fs'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import cds from '@sap/cds'
|
|
2
|
+
import { withPromptCachingMessages } from '../utils/caching.js'
|
|
6
3
|
|
|
7
|
-
const HOME = os.homedir() || process.env.HOME || process.env.USERPROFILE
|
|
8
|
-
const local = file => file.replace(HOME,'~')
|
|
9
|
-
const LOG = cds.log('agents')
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* `cds.connect.to` compliant langchain model
|
|
13
|
-
* for connecting to an Anthropic compatible API,
|
|
14
|
-
* with autoconfiguration based on env, options,
|
|
15
|
-
* ~/.claude/settings.json and ~/.config/opencode/opencode.json
|
|
16
|
-
*/
|
|
17
4
|
export default class ChatAnthropicService extends ChatAnthropic {
|
|
18
|
-
constructor (name, options) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
...fromClaude() || fromOpencode(),
|
|
22
|
-
...config
|
|
23
|
-
}
|
|
24
|
-
if (LOG._debug) {
|
|
25
|
-
const { kind, model, anthropicApiUrl, apiKey } = config
|
|
26
|
-
LOG.info (`Using effective config:`, {
|
|
27
|
-
kind,
|
|
28
|
-
model,
|
|
29
|
-
credentials: {
|
|
30
|
-
anthropicApiUrl,
|
|
31
|
-
apiKey: apiKey ? '***' : undefined
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
super (config)
|
|
5
|
+
constructor (name, options = {}) {
|
|
6
|
+
const { credentials = {} } = options
|
|
7
|
+
super ({ ...credentials, ...options })
|
|
36
8
|
this.name = name
|
|
37
|
-
this.options = config
|
|
38
9
|
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function fromEnv (env = process.env, silent) {
|
|
42
|
-
let any, config = {}
|
|
43
|
-
if ((any = env.ANTHROPIC_BASE_URL)) config.anthropicApiUrl = any
|
|
44
|
-
if ((any = env.ANTHROPIC_AUTH_TOKEN)) config.apiKey = any
|
|
45
|
-
if ((any = env.ANTHROPIC_API_KEY)) config.apiKey = any
|
|
46
|
-
if ((any = env.ANTHROPIC_MODEL)) config.model = any
|
|
47
|
-
if (!Object.keys(config).length) return null
|
|
48
|
-
if (!silent) LOG.debug (`Loaded Anthropic settings from env:`, config)
|
|
49
|
-
return config
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function fromClaude() {
|
|
53
|
-
if ('cached' in fromClaude) return fromClaude.cached
|
|
54
|
-
const settings_json = path.join (HOME,'.claude/settings.json')
|
|
55
|
-
try {
|
|
56
|
-
let settings = JSON.parse (fs.readFileSync (settings_json,'utf8'))
|
|
57
|
-
// https://www.schemastore.org/claude-code-settings.json
|
|
58
10
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
conf.model = settings.env[`ANTHROPIC_DEFAULT_${family}_MODEL`] || settings?.model
|
|
63
|
-
}
|
|
64
|
-
LOG.debug(`Loaded Claude settings from`, local(settings_json), ':', conf)
|
|
65
|
-
} catch {
|
|
66
|
-
LOG.debug(`Failed loading Claude settings from`, local(settings_json))
|
|
67
|
-
fromClaude.cached = null
|
|
11
|
+
async _generate (messages, options, runManager) {
|
|
12
|
+
const cached = withPromptCachingMessages(this.model, messages, options)
|
|
13
|
+
return super._generate(cached.messages, cached.opts, runManager)
|
|
68
14
|
}
|
|
69
|
-
return fromClaude.cached
|
|
70
|
-
}
|
|
71
15
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
let conf = JSON.parse (fs.readFileSync (opencode_json,'utf8'))
|
|
77
|
-
LOG.debug(`Loaded OpenCode settings from`, local(opencode_json))
|
|
78
|
-
// https://opencode.ai/config.json
|
|
79
|
-
let o = conf?.provider?.anthropic?.options
|
|
80
|
-
if (!o) return fromOpencode.cached = null
|
|
81
|
-
let any, config = {}
|
|
82
|
-
if ((any = o.anthropicApiUrl ?? o.apiUrl ?? o.baseURL)) config.anthropicApiUrl = any.replace(/\/v1$/,'') // opencode expects the versioned baseUrl, others do not
|
|
83
|
-
if ((any = o.anthropicApiKey ?? o.apiKey)) config.apiKey = any
|
|
84
|
-
if ((any = conf?.model)) config.model = any.replace('anthropic/','')
|
|
85
|
-
fromOpencode.cached = Object.keys(config).length ? config : null
|
|
86
|
-
LOG.debug(`Loaded OpenCode settings from`, local(opencode_json), ':', conf)
|
|
87
|
-
} catch {
|
|
88
|
-
LOG.debug(`Failed loading OpenCode settings from`, local(opencode_json))
|
|
89
|
-
fromOpencode.cached = null
|
|
16
|
+
async *_streamResponseChunks (messages, options, runManager) {
|
|
17
|
+
const cached = withPromptCachingMessages(this.model, messages, options)
|
|
18
|
+
yield* super._streamResponseChunks(cached.messages, cached.opts, runManager)
|
|
90
19
|
}
|
|
91
|
-
return fromOpencode.cached
|
|
92
20
|
}
|
package/lib/preview/chat.html
CHANGED
|
@@ -115,7 +115,6 @@
|
|
|
115
115
|
color: #1d3e5a;
|
|
116
116
|
border-bottom-left-radius: 3px;
|
|
117
117
|
border-left: 3px solid #537492;
|
|
118
|
-
white-space: pre-wrap;
|
|
119
118
|
}
|
|
120
119
|
.msg.approval::before {
|
|
121
120
|
content: "Action Required";
|
|
@@ -127,6 +126,15 @@
|
|
|
127
126
|
letter-spacing: 0.03em;
|
|
128
127
|
text-transform: uppercase;
|
|
129
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
|
+
}
|
|
130
138
|
|
|
131
139
|
.msg.agent p {
|
|
132
140
|
margin: 0 0 6px;
|
|
@@ -372,6 +380,9 @@
|
|
|
372
380
|
.msg.approval::before {
|
|
373
381
|
color: #91adc5;
|
|
374
382
|
}
|
|
383
|
+
.msg.approval pre {
|
|
384
|
+
background: #0f2d45;
|
|
385
|
+
}
|
|
375
386
|
.btn-approve {
|
|
376
387
|
background: #537492;
|
|
377
388
|
}
|
|
@@ -811,31 +822,41 @@
|
|
|
811
822
|
setTimeout(finish, THINKING_SUMMARY_ANIMATION_MS + 80)
|
|
812
823
|
}
|
|
813
824
|
|
|
814
|
-
function addApprovalPrompt(description) {
|
|
825
|
+
function addApprovalPrompt(description, options, actionRequest) {
|
|
815
826
|
const el = document.createElement("div")
|
|
816
827
|
el.className = "msg approval"
|
|
817
|
-
|
|
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
|
+
}
|
|
818
839
|
const actions = document.createElement("div")
|
|
819
840
|
actions.className = "approval-actions"
|
|
820
841
|
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
+
}
|
|
839
860
|
el.appendChild(actions)
|
|
840
861
|
messages.insertBefore(el, typing.parentElement === messages ? typing : statusText)
|
|
841
862
|
messages.scrollTop = messages.scrollHeight
|
|
@@ -927,8 +948,12 @@
|
|
|
927
948
|
pendingTaskId = taskId
|
|
928
949
|
const msgParts = result?.status?.message?.parts ?? []
|
|
929
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]
|
|
930
955
|
flushPendingThinkingUpdate()
|
|
931
|
-
addApprovalPrompt(description)
|
|
956
|
+
addApprovalPrompt(description, options, actionRequest)
|
|
932
957
|
return
|
|
933
958
|
}
|
|
934
959
|
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import cds from "@sap/cds"
|
|
2
2
|
import { createRequire } from "node:module"
|
|
3
3
|
const { path } = cds.utils
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
getFilteredEntities,
|
|
7
|
-
getFilteredActions,
|
|
8
|
-
slugified,
|
|
9
|
-
} from "../utils/utils.js"
|
|
4
|
+
import { getFilteredEntities, getFilteredActions } from "@cap-js/mcp/lib/utils/tools-shared.js"
|
|
5
|
+
import { getDescription, slugified } from "../utils/utils.js"
|
|
10
6
|
import {
|
|
11
7
|
scanSkills,
|
|
12
8
|
parseAgentMetadata,
|
|
@@ -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 =
|
|
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
|
-
|
|
166
|
-
|
|
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
|
package/lib/telemetry/metrics.js
CHANGED
|
@@ -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) {
|