@cap-js/agents 0.9.4 → 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/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 +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 -82
- package/lib/preview/chat.html +47 -22
- 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 +4 -9
- package/package.json +4 -5
- 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 +5 -9
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
# Agent status messages (shown during task execution)
|
|
2
|
+
#XMSG Status info message send to the user client when a tool is called. {0} = tool name
|
|
2
3
|
agent_status_calling_tools = Calling {0}
|
|
4
|
+
#XMSG Status info message send to the user client when an entity is being queried. {0} = entity name
|
|
3
5
|
agent_status_querying = Querying {0}
|
|
6
|
+
#XMSG Status info message send to the user client after a tool result returned and the agent is processing it
|
|
4
7
|
agent_status_processing_response = Processing tool response
|
|
8
|
+
#XMSG Status info message send to the user client after multiple tool results returned and the agent is processing it
|
|
5
9
|
agent_status_processing_responses = Processing tool responses
|
|
10
|
+
agent_status_summarizing_progress = Agent interrupted. Summarising progress.
|
|
11
|
+
AGENT_SUMMARY_QUOTA_FALLBACK = The task was stopped as it reached its resource consumption limit. Some work may have been done — please check the results or retry with a simpler request.
|
|
12
|
+
AGENT_SUMMARY_TIMEOUT_FALLBACK = The task was stopped as it reached its time limit. Progress may be incomplete. Continue running or stop?
|
|
6
13
|
|
|
7
14
|
# Authentication
|
|
15
|
+
#XMSG Error message for HTTP 401 status
|
|
8
16
|
UNAUTHORIZED = Unauthorized
|
|
17
|
+
#XMSG Error message for HTTP 403 status
|
|
9
18
|
FORBIDDEN = Forbidden
|
|
10
19
|
|
|
11
20
|
# Input validation
|
|
21
|
+
#XMSG Error message send to the users client when their message exceeds the limit. {0} = number of allowed characters
|
|
12
22
|
MESSAGE_TOO_LONG = The message must not exceed {0} characters.
|
|
13
23
|
|
|
14
24
|
# Quota enforcement
|
|
@@ -28,4 +38,11 @@ CONTENT_FILTER_TOOL_BLOCKED = The last tool called yielded a malicious result. P
|
|
|
28
38
|
PUSH_URL_NOT_ALLOWED = Push notification callback URL ''{0}'' not in allowed domains: {1}
|
|
29
39
|
|
|
30
40
|
# HITL
|
|
41
|
+
HITL_APPROVE = Approve
|
|
42
|
+
HITL_REJECT = Reject
|
|
43
|
+
HITL_CONTINUE = Continue
|
|
44
|
+
HITL_STOP = Stop
|
|
31
45
|
RESUME_REQUIRES_TEXT = Resume message must contain text (e.g. ''approve'' or ''reject'') or a data part.
|
|
46
|
+
|
|
47
|
+
# LLM
|
|
48
|
+
LLM_UNAVAILABLE = The agent is temporarily unavailable. Please try again later.
|
package/cds-plugin.js
CHANGED
|
@@ -1,37 +1,21 @@
|
|
|
1
1
|
import cds from "@sap/cds"
|
|
2
2
|
|
|
3
3
|
const LOG = cds.log("agents")
|
|
4
|
-
import { patchLangChain } from "./lib/telemetry/tracing.js"
|
|
5
|
-
import cds_compile_to_a2a from "./lib/compile.js"
|
|
6
4
|
import registerDefaultAgentHandlers from "./srv/handlers/index.js"
|
|
5
|
+
import { patchLangChain } from "./lib/telemetry/tracing.js"
|
|
7
6
|
import { slugified } from "./lib/utils/utils.js"
|
|
8
|
-
|
|
7
|
+
import cds_compile_to_a2a from "./lib/compile.js"
|
|
9
8
|
cds.compile.to.a2a = cds_compile_to_a2a
|
|
10
9
|
|
|
11
|
-
// Detect optional peer plugins (@cap-js/telemetry, @cap-js/audit-logging)
|
|
12
|
-
const hasTelemetry = !!cds.env.requires?.telemetry
|
|
13
|
-
const hasAuditLog = !!cds.env.requires?.["audit-log"]
|
|
14
|
-
const hasMetrics = !!cds.env.requires?.telemetry?.["metrics"]
|
|
15
|
-
const hasTracing = !!cds.env.requires?.telemetry?.["tracing"]
|
|
16
|
-
if (!hasTelemetry && cds.env.profiles?.includes("production"))
|
|
17
|
-
LOG.warn("@cap-js/telemetry not configured - metrics and tracing disabled")
|
|
18
|
-
if (!hasAuditLog && cds.env.profiles?.includes("production"))
|
|
19
|
-
LOG.warn("@cap-js/audit-logging not configured - audit events disabled")
|
|
20
|
-
if (hasTelemetry && !hasMetrics && cds.env.profiles?.includes("production"))
|
|
21
|
-
LOG.warn("@cap-js/telemetry has no metrics configured - metrics disabled")
|
|
22
|
-
if (hasTelemetry && !hasTracing && cds.env.profiles?.includes("production"))
|
|
23
|
-
LOG.warn("@cap-js/telemetry has no tracing configured - tracing disabled")
|
|
24
|
-
|
|
25
10
|
// Enable doc comments in CSN for agent card generation
|
|
26
11
|
cds.env.cdsc = { ...cds.env.cdsc, docComment: true }
|
|
27
12
|
|
|
28
|
-
// Ensure A2A correlation fields are indexed by SAP Cloud Logging
|
|
29
|
-
cds.env.log ??= {}
|
|
30
|
-
const cls_fields = (cds.env.log.cls_custom_fields ??= [])
|
|
31
|
-
if (!cls_fields.includes("agent.task.id")) cls_fields.push("agent.task.id")
|
|
32
|
-
if (!cls_fields.includes("agent.context.id")) cls_fields.push("agent.context.id")
|
|
33
|
-
|
|
34
13
|
cds.on("bootstrap", (app) => {
|
|
14
|
+
// Ensure A2A correlation fields are indexed by SAP Cloud Logging
|
|
15
|
+
const cls_fields = ((cds.env.log ??= {}).cls_custom_fields ??= [])
|
|
16
|
+
if (!cls_fields.includes("agent.task.id")) cls_fields.push("agent.task.id")
|
|
17
|
+
if (!cls_fields.includes("agent.context.id")) cls_fields.push("agent.context.id")
|
|
18
|
+
|
|
35
19
|
const providers = {
|
|
36
20
|
["llm-mock"]: {},
|
|
37
21
|
anthropic: {
|
|
@@ -77,64 +61,97 @@ cds.on("bootstrap", (app) => {
|
|
|
77
61
|
})
|
|
78
62
|
})
|
|
79
63
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
})
|
|
64
|
+
!(function cds_agents_config_compat() {
|
|
65
|
+
// Also support legacy `cds.agents.pool` configuration by merging it into `cds.agents.quotas`
|
|
66
|
+
if (cds.env.agents?.pool)
|
|
67
|
+
cds.env.agents.quotas = { ...cds.env.agents.quotas, ...cds.env.agents.pool }
|
|
68
|
+
})()
|
|
85
69
|
|
|
86
|
-
|
|
87
|
-
cds.on("served", async () => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
await
|
|
70
|
+
!(function cds_requires_llm() {
|
|
71
|
+
cds.on("served", async () => {
|
|
72
|
+
if (
|
|
73
|
+
cds.requires.llm === "anthropic" ||
|
|
74
|
+
cds.requires.llm?.kind === "anthropic" ||
|
|
75
|
+
cds.requires.llm === "auto" ||
|
|
76
|
+
cds.requires.llm?.kind === "auto"
|
|
77
|
+
) {
|
|
78
|
+
const { resolve_config } = await import("./lib/config/local.js")
|
|
79
|
+
let options = cds.requires.llm
|
|
80
|
+
if (options === "auto") options = { kind: "auto" }
|
|
81
|
+
cds.env.requires.llm = resolve_config(options)
|
|
95
82
|
}
|
|
96
|
-
}
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
84
|
+
const config = cds.requires.llm,
|
|
85
|
+
credentials = {}
|
|
86
|
+
const { url, destination, anthropicApiUrl, apiKey } = config?.credentials || {}
|
|
87
|
+
if (url) credentials.url = url
|
|
88
|
+
if (destination) credentials.destination = destination
|
|
89
|
+
if (anthropicApiUrl) credentials.anthropicApiUrl = anthropicApiUrl
|
|
90
|
+
if (apiKey) credentials.apiKey = "***"
|
|
91
|
+
LOG.info(`cds.connect.to 'llm' with:`, { ...config, credentials })
|
|
92
|
+
})
|
|
93
|
+
})()
|
|
94
|
+
|
|
95
|
+
!(function add_agent_handlers() {
|
|
96
|
+
cds.on("serving", (srv) => {
|
|
97
|
+
if (srv.definition?.protocols?.agent) registerDefaultAgentHandlers(srv)
|
|
98
|
+
})
|
|
99
|
+
})()
|
|
100
|
+
|
|
101
|
+
if (cds.env.requires?.telemetry)
|
|
102
|
+
!(function telemetry() {
|
|
103
|
+
// Schedule active_users metric computation + MLflow exporter
|
|
104
|
+
cds.on("served", async () => {
|
|
105
|
+
const { setupActiveUsersMetric } = await import("./lib/telemetry/active-users.js")
|
|
106
|
+
setupActiveUsersMetric()
|
|
107
|
+
// Defer LangChain patching so the CDS model is fully loaded before patches land.
|
|
108
|
+
// opt-out via cds.env.agents.trace_langchain = false
|
|
109
|
+
if (cds.env.agents?.trace_langchain !== false) {
|
|
110
|
+
await patchLangChain()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (cds.env.agents?.mlflow) {
|
|
114
|
+
const { setupMlflowExporter } = await import("./lib/telemetry/mlflow/index.js")
|
|
115
|
+
setupMlflowExporter()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
})()
|
|
103
119
|
|
|
104
120
|
// Bootstrap sidecar mode when the agent-sidecar profile is active
|
|
105
|
-
if (cds.env.profiles?.includes("agent-sidecar"))
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
121
|
+
if (cds.env.profiles?.includes("agent-sidecar"))
|
|
122
|
+
!(function sidecar() {
|
|
123
|
+
// Auto-mark @agent services as external so CDS does not serve them locally.
|
|
124
|
+
// Auto-mark them as hcql services served externally, they are served from the main app.
|
|
125
|
+
// This runs after model load but before cds.serve() filters definitions,
|
|
126
|
+
// so users don't need to add these things manually.
|
|
127
|
+
cds.on("loaded", (csn) => {
|
|
128
|
+
const hcql = cds.requires.kinds["hcql"]
|
|
129
|
+
const agentSidecar = cds.requires.agent || {}
|
|
130
|
+
const hcqlBase = agentSidecar.url // For local development the base URL is given in the package.json
|
|
131
|
+
const agentCredentials = agentSidecar.credentials || {}
|
|
132
|
+
for (const [name, def] of Object.entries(csn.definitions || {})) {
|
|
133
|
+
if (def.kind !== "service") continue
|
|
134
|
+
if (!def["@agent"]) continue
|
|
135
|
+
// Mark as external so CDS does not serve it locally — it will be served via HCQL from the main app.
|
|
136
|
+
def["@cds.external"] = true
|
|
137
|
+
// Java main apps use the CDS service name in the HCQL path (/hcql/CatalogService),
|
|
138
|
+
// Node.js main apps use the slugified path (/hcql/catalog).
|
|
139
|
+
const isJava = !cds.env.profiles?.includes("node")
|
|
140
|
+
let n = isJava ? name : slugified(name)
|
|
141
|
+
if (cds.requires[name]) continue // skip if user provided service-specific config for this service
|
|
142
|
+
if (cds.requires[n]) continue // skip if user provided service-specific config for possibly the slugified version
|
|
143
|
+
const newRequiresEntry = { ...hcql, kind: "hcql" }
|
|
144
|
+
newRequiresEntry.credentials = {
|
|
145
|
+
...agentCredentials,
|
|
146
|
+
...(hcqlBase && { url: `${hcqlBase}/${n.split(".").pop()}` }),
|
|
147
|
+
...(agentCredentials.destination && { path: `/${n.split(".").pop()}` }),
|
|
148
|
+
}
|
|
149
|
+
cds.requires[n] = newRequiresEntry
|
|
131
150
|
}
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
})
|
|
151
|
+
})
|
|
135
152
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
153
|
+
cds.on("served", async () => {
|
|
154
|
+
const { bootstrapSidecar } = await import("./lib/sidecar.js")
|
|
155
|
+
await bootstrapSidecar()
|
|
156
|
+
})
|
|
157
|
+
})()
|
|
@@ -27,7 +27,7 @@ export async function createAutoDeepAgent(srv, agentDir) {
|
|
|
27
27
|
createSkillsMiddleware,
|
|
28
28
|
} = await import("deepagents")
|
|
29
29
|
|
|
30
|
-
LOG.debug("Auto-building deep agent", {
|
|
30
|
+
LOG.debug("Auto-building deep agent for:", {
|
|
31
31
|
service: srv?.name,
|
|
32
32
|
agentDir,
|
|
33
33
|
tools: tools.length,
|
|
@@ -6,7 +6,7 @@ import { z } from "zod"
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Quota enforcement middleware for deep agents.
|
|
9
|
-
* Reads limits from cds.env.agents.
|
|
9
|
+
* Reads limits from cds.env.agents.quotas at check time (not creation time).
|
|
10
10
|
* Returns array to spread into middleware config.
|
|
11
11
|
*/
|
|
12
12
|
export async function quotaEnforcerMiddleware() {
|
|
@@ -21,12 +21,12 @@ export async function quotaEnforcerMiddleware() {
|
|
|
21
21
|
afterModel: {
|
|
22
22
|
canJumpTo: ["end"],
|
|
23
23
|
hook: (state) => {
|
|
24
|
-
const
|
|
24
|
+
const quotas = cds.env.agents?.quotas || {}
|
|
25
25
|
const newCallCount = state.runModelCallCount + 1
|
|
26
26
|
|
|
27
27
|
// Check LLM invocation limit
|
|
28
|
-
if (
|
|
29
|
-
const reason = `LLM call limit exceeded: ${newCallCount} calls (max ${
|
|
28
|
+
if (quotas.maxLLMInvocationsPerTask && newCallCount > quotas.maxLLMInvocationsPerTask) {
|
|
29
|
+
const reason = `LLM call limit exceeded: ${newCallCount} calls (max ${quotas.maxLLMInvocationsPerTask} per task)`
|
|
30
30
|
audit("QuotaExceeded", {
|
|
31
31
|
data: {
|
|
32
32
|
service: cds.context?.["agent.service"],
|
|
@@ -47,8 +47,8 @@ export async function quotaEnforcerMiddleware() {
|
|
|
47
47
|
const newTokenCount = state.runTokenCount + consumed
|
|
48
48
|
|
|
49
49
|
// Check token limit
|
|
50
|
-
if (
|
|
51
|
-
const reason = `Token limit exceeded: ${newTokenCount} tokens (max ${
|
|
50
|
+
if (quotas.maxLLMTokensPerTask && newTokenCount > quotas.maxLLMTokensPerTask) {
|
|
51
|
+
const reason = `Token limit exceeded: ${newTokenCount} tokens (max ${quotas.maxLLMTokensPerTask} per task)`
|
|
52
52
|
audit("QuotaExceeded", {
|
|
53
53
|
data: {
|
|
54
54
|
service: cds.context?.["agent.service"],
|
|
@@ -67,8 +67,8 @@ export async function quotaEnforcerMiddleware() {
|
|
|
67
67
|
const newToolCallCount = state.runToolCallCount + toolCalls
|
|
68
68
|
|
|
69
69
|
// Check tool call limit
|
|
70
|
-
if (
|
|
71
|
-
const reason = `Tool call limit exceeded: ${newToolCallCount} calls (max ${
|
|
70
|
+
if (quotas.maxToolCallsPerTask && newToolCallCount > quotas.maxToolCallsPerTask) {
|
|
71
|
+
const reason = `Tool call limit exceeded: ${newToolCallCount} calls (max ${quotas.maxToolCallsPerTask} per task)`
|
|
72
72
|
audit("QuotaExceeded", {
|
|
73
73
|
data: {
|
|
74
74
|
service: cds.context?.["agent.service"],
|
|
@@ -19,13 +19,14 @@ export function remoteMcpMiddleware() {
|
|
|
19
19
|
const cache = (cds.context.__mcpDynamicTools ??= {})
|
|
20
20
|
const placeholders = (request.tools ?? []).filter((t) => t._mcpDynamic)
|
|
21
21
|
await Promise.all(
|
|
22
|
-
placeholders.map(async ({ mcpUrl, serviceName, resolveHeaders }) => {
|
|
22
|
+
placeholders.map(async ({ mcpUrl, serviceName, resolveHeaders, toolFilter }) => {
|
|
23
23
|
if (cache[mcpUrl]) return
|
|
24
24
|
const headers = await resolveHeaders()
|
|
25
25
|
const client = new MultiServerMCPClient({
|
|
26
26
|
mcpServers: { default: { transport: "http", url: mcpUrl, headers } },
|
|
27
27
|
})
|
|
28
|
-
|
|
28
|
+
let raw = await client.getTools()
|
|
29
|
+
if (toolFilter?.length) raw = raw.filter((t) => toolFilter.includes(t.name))
|
|
29
30
|
for (const t of raw) t.name = toolName(`${serviceName}_${t.name}`)
|
|
30
31
|
cache[mcpUrl] = raw
|
|
31
32
|
LOG.debug(
|
|
@@ -63,7 +63,7 @@ function resolveToolLabel(tc) {
|
|
|
63
63
|
/**
|
|
64
64
|
* Publishes a non-final "working" status-update to the eventBus.
|
|
65
65
|
*/
|
|
66
|
-
function publishStatus(text) {
|
|
66
|
+
export function publishStatus(text) {
|
|
67
67
|
const eventBus = cds.context?.["agent.eventBus"]
|
|
68
68
|
if (!eventBus || !text) return
|
|
69
69
|
|
|
@@ -10,13 +10,13 @@ const LOG = cds.log("agents")
|
|
|
10
10
|
* Handles two paths: thrown errors (err.details appended when present)
|
|
11
11
|
* and tools returning artifact.isError=true (@cap-js/mcp action pattern).
|
|
12
12
|
*/
|
|
13
|
-
export function toolWrapMiddleware() {
|
|
13
|
+
export function toolWrapMiddleware(srv) {
|
|
14
14
|
return createMiddleware({
|
|
15
15
|
name: "ToolWrapMiddleware",
|
|
16
|
-
wrapToolCall: async (request, handler)
|
|
16
|
+
wrapToolCall: async function (request, handler) {
|
|
17
17
|
const { name, id, args } = request.toolCall
|
|
18
18
|
try {
|
|
19
|
-
LOG.debug("
|
|
19
|
+
LOG.debug(srv.name, "calling tool", name, args)
|
|
20
20
|
const result = await handler(request)
|
|
21
21
|
if (ToolMessage.isInstance(result) && result.artifact?.isError === true) {
|
|
22
22
|
result.status = "error"
|
|
@@ -29,12 +29,11 @@ export function toolWrapMiddleware() {
|
|
|
29
29
|
) {
|
|
30
30
|
result.status = "error"
|
|
31
31
|
}
|
|
32
|
-
if (result?.status === "error") LOG.debug("
|
|
33
|
-
else LOG.debug("[tool] completed", name)
|
|
32
|
+
if (result?.status === "error") LOG.debug(srv.name, "tool error", name, result.content)
|
|
34
33
|
return result
|
|
35
34
|
} catch (err) {
|
|
36
35
|
if (isGraphInterrupt(err)) throw err
|
|
37
|
-
LOG.debug("
|
|
36
|
+
LOG.debug(srv.name, "tool error", name, err)
|
|
38
37
|
let content = `Error: ${err.message}`
|
|
39
38
|
if (Array.isArray(err.details) && err.details.length > 0) {
|
|
40
39
|
const lines = err.details.map((d) => `- ${d.message}`).join("\n")
|
|
@@ -24,11 +24,9 @@ function secondsUntilMidnightUTC() {
|
|
|
24
24
|
* Returns null if within limits, or { message, retryAfter } if a limit is breached.
|
|
25
25
|
*/
|
|
26
26
|
export default async function quotaEnforcerAtStart() {
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
LOG.debug(
|
|
30
|
-
"No quota pool configuration found at cds.env.agents.pool — quota enforcement disabled",
|
|
31
|
-
)
|
|
27
|
+
const quotas = cds.env.agents?.quotas
|
|
28
|
+
if (!quotas) {
|
|
29
|
+
LOG.debug("No quota configuration found at cds.env.agents.quotas — quota enforcement disabled")
|
|
32
30
|
return null
|
|
33
31
|
}
|
|
34
32
|
|
|
@@ -65,44 +63,49 @@ export default async function quotaEnforcerAtStart() {
|
|
|
65
63
|
.columns("coalesce(sum(usageLlmTokens),0) as llmTokensThisDay")
|
|
66
64
|
.where({ createdAt: { ">=": today.toISOString() } }),
|
|
67
65
|
])
|
|
68
|
-
if (
|
|
66
|
+
if (quotas.maxConcurrentTasks != null && concurrentTasks >= quotas.maxConcurrentTasks) {
|
|
69
67
|
return {
|
|
70
|
-
message: cds.i18n.messages.at("QUOTA_CONCURRENT_TASKS", [
|
|
68
|
+
message: cds.i18n.messages.at("QUOTA_CONCURRENT_TASKS", [quotas.maxConcurrentTasks]),
|
|
71
69
|
retryAfter: 30,
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
72
|
if (
|
|
75
|
-
|
|
76
|
-
concurrentTasksThisUser >=
|
|
73
|
+
quotas.maxConcurrentTasksPerUser != null &&
|
|
74
|
+
concurrentTasksThisUser >= quotas.maxConcurrentTasksPerUser
|
|
77
75
|
) {
|
|
78
76
|
return {
|
|
79
77
|
message: cds.i18n.messages.at("QUOTA_CONCURRENT_TASKS_PER_USER", [
|
|
80
|
-
|
|
78
|
+
quotas.maxConcurrentTasksPerUser,
|
|
81
79
|
]),
|
|
82
80
|
retryAfter: 30,
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
|
-
if (
|
|
83
|
+
if (quotas.maxTasksPerHour != null && lastHourTasks >= quotas.maxTasksPerHour) {
|
|
86
84
|
return {
|
|
87
|
-
message: cds.i18n.messages.at("QUOTA_TASKS_PER_HOUR", [
|
|
85
|
+
message: cds.i18n.messages.at("QUOTA_TASKS_PER_HOUR", [quotas.maxTasksPerHour]),
|
|
88
86
|
retryAfter: secondsUntilNextHour(),
|
|
89
87
|
}
|
|
90
88
|
}
|
|
91
|
-
if (
|
|
89
|
+
if (
|
|
90
|
+
quotas.maxTasksPerHourPerUser != null &&
|
|
91
|
+
lastHourTasksThisUser >= quotas.maxTasksPerHourPerUser
|
|
92
|
+
) {
|
|
92
93
|
return {
|
|
93
|
-
message: cds.i18n.messages.at("QUOTA_TASKS_PER_HOUR_PER_USER", [
|
|
94
|
+
message: cds.i18n.messages.at("QUOTA_TASKS_PER_HOUR_PER_USER", [
|
|
95
|
+
quotas.maxTasksPerHourPerUser,
|
|
96
|
+
]),
|
|
94
97
|
retryAfter: secondsUntilNextHour(),
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
|
-
if (
|
|
100
|
+
if (quotas.maxToolCallsPerHour != null && lastHourToolCalls >= quotas.maxToolCallsPerHour) {
|
|
98
101
|
return {
|
|
99
|
-
message: cds.i18n.messages.at("QUOTA_TOOL_CALLS_PER_HOUR", [
|
|
102
|
+
message: cds.i18n.messages.at("QUOTA_TOOL_CALLS_PER_HOUR", [quotas.maxToolCallsPerHour]),
|
|
100
103
|
retryAfter: secondsUntilNextHour(),
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
|
-
if (
|
|
106
|
+
if (quotas.maxLLMTokensPerDay != null && llmTokensThisDay >= quotas.maxLLMTokensPerDay) {
|
|
104
107
|
return {
|
|
105
|
-
message: cds.i18n.messages.at("QUOTA_LLM_TOKENS_PER_DAY", [
|
|
108
|
+
message: cds.i18n.messages.at("QUOTA_LLM_TOKENS_PER_DAY", [quotas.maxLLMTokensPerDay]),
|
|
106
109
|
retryAfter: secondsUntilMidnightUTC(),
|
|
107
110
|
}
|
|
108
111
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import cds from "@sap/cds"
|
|
2
2
|
import { HumanMessage } from "@langchain/core/messages"
|
|
3
3
|
import { short } from "../utils/utils.js"
|
|
4
|
+
import { publishStatus } from "./middleware/status-update.js"
|
|
4
5
|
|
|
5
6
|
const LOG = cds.log("agents")
|
|
6
7
|
|
|
7
|
-
const DEFAULT_TIMEOUT =
|
|
8
|
+
const DEFAULT_TIMEOUT = 20_000
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Summarize partial work from a graph execution that was interrupted
|
|
@@ -17,10 +18,10 @@ const DEFAULT_TIMEOUT = 10_000
|
|
|
17
18
|
* @param {object} options
|
|
18
19
|
* @param {string} options.contextId
|
|
19
20
|
* @param {string} options.serviceName
|
|
20
|
-
* @param {string} options.reason -
|
|
21
|
+
* @param {string} options.reason - "timeOut" or "quota"
|
|
21
22
|
* @param {object} [options.checkpointer] - LangGraph checkpointer instance
|
|
22
23
|
* @param {Function} options.getModel - Async function returning a LangChain chat model
|
|
23
|
-
* @param {number} [options.timeout] - Max ms to spend on summarization (default
|
|
24
|
+
* @param {number} [options.timeout] - Max ms to spend on summarization (default 20s)
|
|
24
25
|
* @returns {Promise<string>} Summary message or fallback
|
|
25
26
|
*/
|
|
26
27
|
export async function summarizePartialWork({
|
|
@@ -31,7 +32,10 @@ export async function summarizePartialWork({
|
|
|
31
32
|
getModel,
|
|
32
33
|
timeout = DEFAULT_TIMEOUT,
|
|
33
34
|
}) {
|
|
34
|
-
|
|
35
|
+
publishStatus(cds.i18n.messages.at(`agent_status_summarizing_progress`))
|
|
36
|
+
const fallback = cds.i18n.messages.at(
|
|
37
|
+
reason === "timeOut" ? "AGENT_SUMMARY_TIMEOUT_FALLBACK" : "AGENT_SUMMARY_QUOTA_FALLBACK",
|
|
38
|
+
)
|
|
35
39
|
|
|
36
40
|
try {
|
|
37
41
|
if (!getModel) return fallback
|
|
@@ -52,10 +56,11 @@ export async function summarizePartialWork({
|
|
|
52
56
|
return `[${role}]: ${content?.slice(0, 500)}`
|
|
53
57
|
})
|
|
54
58
|
.join("\n")
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
${
|
|
59
|
+
let summaryPrompt =
|
|
60
|
+
reason === "timeOut"
|
|
61
|
+
? `Agent task was not completed within its time limit. Write short, precise progress summary so user can decide whether to continue. State completed work and immediate next work. Do not claim work not shown. Start with: Agent did not finish within time! End with: Continue running or stop? No other questions to the user allowed!`
|
|
62
|
+
: `Agent task was interrupted. Reason: ${reason}. Based on conversation history, provide brief summary of completed work and remaining work. Be concise.`
|
|
63
|
+
summaryPrompt += `\n\n Conversation Snippet: \n\n ${conversationSnippet.trim()}`
|
|
59
64
|
|
|
60
65
|
let summaryTimer
|
|
61
66
|
try {
|
|
@@ -70,19 +75,19 @@ ${conversationSnippet}`
|
|
|
70
75
|
])
|
|
71
76
|
|
|
72
77
|
const summary =
|
|
73
|
-
typeof response.content === "string" ? response.content :
|
|
78
|
+
typeof response.content === "string" ? response.content : response.content?.[0]?.text
|
|
79
|
+
if (!summary) throw new Error("Summary LLM returned no text")
|
|
74
80
|
|
|
75
|
-
LOG.info("partial work summary generated", {
|
|
81
|
+
LOG.info(serviceName, "partial work summary generated", {
|
|
76
82
|
conversation: short(contextId),
|
|
77
|
-
service: serviceName,
|
|
78
83
|
reason,
|
|
79
84
|
})
|
|
80
|
-
return
|
|
85
|
+
return summary
|
|
81
86
|
} finally {
|
|
82
87
|
clearTimeout(summaryTimer)
|
|
83
88
|
}
|
|
84
89
|
} catch (err) {
|
|
85
|
-
LOG.debug("partial work summary failed, using fallback", {
|
|
90
|
+
LOG.debug(serviceName, "partial work summary failed, using fallback", {
|
|
86
91
|
conversation: short(contextId),
|
|
87
92
|
error: err.message,
|
|
88
93
|
})
|
package/lib/compile.js
CHANGED
|
@@ -51,9 +51,8 @@ function cds_compile_to_a2a(csn, options = {}) {
|
|
|
51
51
|
streaming: true,
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
return JSON.stringify(agentCard, null, 2)
|
|
54
|
+
if (process.stdout.isTTY || options.as === "json") return agentCard
|
|
55
|
+
else return JSON.stringify(agentCard, null, 2)
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
export default cds_compile_to_a2a
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import os from "node:os"
|
|
4
|
+
import cds from "@sap/cds"
|
|
5
|
+
|
|
6
|
+
const HOME = os.homedir() || process.env.HOME || process.env.USERPROFILE
|
|
7
|
+
const local = (file) => file.replace(HOME, "~")
|
|
8
|
+
const LOG = cds.log("agents")
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `cds.connect.to` compliant langchain model
|
|
12
|
+
* for connecting to an Anthropic compatible API,
|
|
13
|
+
* with autoconfiguration based on env, options,
|
|
14
|
+
* ~/.claude/settings.json and ~/.config/opencode/opencode.json
|
|
15
|
+
*/
|
|
16
|
+
export function resolve_config(options) {
|
|
17
|
+
let config = fromEnv()
|
|
18
|
+
if (!config?.anthropicApiUrl)
|
|
19
|
+
config = {
|
|
20
|
+
...config,
|
|
21
|
+
...(fromClaude() || fromOpencode()),
|
|
22
|
+
}
|
|
23
|
+
let { model, ...credentials } = config
|
|
24
|
+
if (!model && !options?.model) return { kind: "mock" }
|
|
25
|
+
return {
|
|
26
|
+
kind: "anthropic",
|
|
27
|
+
model: options?.model || model,
|
|
28
|
+
credentials: {
|
|
29
|
+
...credentials,
|
|
30
|
+
...options?.credentials,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fromEnv(env = process.env, silent) {
|
|
36
|
+
let any,
|
|
37
|
+
config = {}
|
|
38
|
+
if ((any = env.ANTHROPIC_BASE_URL)) config.anthropicApiUrl = any
|
|
39
|
+
if ((any = env.ANTHROPIC_AUTH_TOKEN)) config.apiKey = any
|
|
40
|
+
if ((any = env.ANTHROPIC_API_KEY)) config.apiKey = any
|
|
41
|
+
if ((any = env.ANTHROPIC_MODEL)) config.model = any
|
|
42
|
+
if (!Object.keys(config).length) return null
|
|
43
|
+
if (!silent) LOG.debug(`Loaded Anthropic settings from env:`, config)
|
|
44
|
+
return config
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fromClaude() {
|
|
48
|
+
if ("cached" in fromClaude) return fromClaude.cached
|
|
49
|
+
const settings_json = path.join(HOME, ".claude/settings.json")
|
|
50
|
+
try {
|
|
51
|
+
let settings = JSON.parse(fs.readFileSync(settings_json, "utf8"))
|
|
52
|
+
// https://www.schemastore.org/claude-code-settings.json
|
|
53
|
+
|
|
54
|
+
let conf = (fromClaude.cached = fromEnv(settings?.env, "silent"))
|
|
55
|
+
if (!conf.model && settings.env) {
|
|
56
|
+
let family = (settings.model || "sonnet").toUpperCase()
|
|
57
|
+
conf.model = settings.env[`ANTHROPIC_DEFAULT_${family}_MODEL`] || settings?.model
|
|
58
|
+
}
|
|
59
|
+
LOG.debug(`Loaded config from`, local(settings_json), ":", sanitized(conf))
|
|
60
|
+
} catch {
|
|
61
|
+
LOG.debug(`Failed loading Claude settings from`, local(settings_json))
|
|
62
|
+
fromClaude.cached = null
|
|
63
|
+
}
|
|
64
|
+
return fromClaude.cached
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fromOpencode() {
|
|
68
|
+
if ("cached" in fromOpencode) return fromOpencode.cached
|
|
69
|
+
const opencode_json = path.join(HOME, ".config/opencode/opencode.json")
|
|
70
|
+
try {
|
|
71
|
+
let conf = JSON.parse(fs.readFileSync(opencode_json, "utf8"))
|
|
72
|
+
LOG.debug(`Loaded OpenCode settings from`, local(opencode_json))
|
|
73
|
+
// https://opencode.ai/config.json
|
|
74
|
+
let o = conf?.provider?.anthropic?.options
|
|
75
|
+
if (!o) return (fromOpencode.cached = null)
|
|
76
|
+
let any,
|
|
77
|
+
config = {}
|
|
78
|
+
if ((any = o.anthropicApiUrl ?? o.apiUrl ?? o.baseURL))
|
|
79
|
+
config.anthropicApiUrl = any.replace(/\/v1$/, "") // opencode expects the versioned baseUrl, others do not
|
|
80
|
+
if ((any = o.anthropicApiKey ?? o.apiKey)) config.apiKey = any
|
|
81
|
+
if ((any = conf?.model)) config.model = any.replace("anthropic/", "")
|
|
82
|
+
fromOpencode.cached = Object.keys(config).length ? config : null
|
|
83
|
+
LOG.debug(`Loaded config from`, local(opencode_json), ":", sanitized(config))
|
|
84
|
+
} catch {
|
|
85
|
+
LOG.debug(`Failed loading OpenCode settings from`, local(opencode_json))
|
|
86
|
+
fromOpencode.cached = null
|
|
87
|
+
}
|
|
88
|
+
return fromOpencode.cached
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const sanitized = ({ apiKey, ...rest }) => ({
|
|
92
|
+
...rest,
|
|
93
|
+
apiKey: apiKey ? "***" : undefined,
|
|
94
|
+
})
|