@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
|
@@ -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)}
|
|
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
|
+
}
|
package/lib/utils/utils.js
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
import cds from "@sap/cds"
|
|
2
2
|
import { domainToASCII } from "node:url"
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const match = /{i18n>([^}]+)}/.exec(value)
|
|
7
|
-
if (match) {
|
|
8
|
-
return cds.i18n?.labels?.texts4?.(locale)?.[match[1]] || value
|
|
9
|
-
}
|
|
10
|
-
return value
|
|
11
|
-
}
|
|
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
|
-
}
|
|
4
|
+
import { resolveI18n, getFilteredEntities } from "@cap-js/mcp/lib/utils/tools-shared.js"
|
|
5
|
+
export { resolveI18n, getFilteredEntities }
|
|
18
6
|
|
|
19
7
|
/**
|
|
20
8
|
* Mirrors CAP's internal slug rules used for service path generation.
|
|
@@ -52,31 +40,6 @@ export function getDescription(obj, locale) {
|
|
|
52
40
|
return result || undefined
|
|
53
41
|
}
|
|
54
42
|
|
|
55
|
-
export function getFilteredEntities(srv) {
|
|
56
|
-
return Object.fromEntries(
|
|
57
|
-
Object.entries(srv.entities || {})
|
|
58
|
-
.filter(
|
|
59
|
-
([name, entity]) =>
|
|
60
|
-
!(entity["@cds.autoexposed"] && !entity["@cds.autoexpose"]) &&
|
|
61
|
-
!name.endsWith("DraftAdministrativeData") &&
|
|
62
|
-
!name.endsWith(".texts") &&
|
|
63
|
-
!entity["@cds.api.ignore"],
|
|
64
|
-
)
|
|
65
|
-
.sort(([a], [b]) => a.localeCompare(b)),
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function getFilteredActions(srv) {
|
|
70
|
-
const actions = {}
|
|
71
|
-
for (const op of srv.actions || []) {
|
|
72
|
-
if ((op.kind === "action" || op.kind === "function") && !op["@cds.api.ignore"]) {
|
|
73
|
-
const localName = op.name.split(".").pop()
|
|
74
|
-
actions[localName] = op
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return actions
|
|
78
|
-
}
|
|
79
|
-
|
|
80
43
|
/**
|
|
81
44
|
* Sanitize an agent name into a valid LangChain/LLM tool name.
|
|
82
45
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/agents",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "CDS plugin for building agents",
|
|
5
5
|
"author": "SAP SE (https://www.sap.com)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@a2a-js/sdk": "^0.3.12",
|
|
44
|
-
"@cap-js/attachments": "
|
|
45
|
-
"@cap-js/mcp": "^1.
|
|
44
|
+
"@cap-js/attachments": ">=3",
|
|
45
|
+
"@cap-js/mcp": "^1.5",
|
|
46
46
|
"@langchain/anthropic": "^1.5.1",
|
|
47
47
|
"@langchain/core": "^1",
|
|
48
48
|
"@langchain/langgraph": "^1",
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
"image/jpeg"
|
|
148
148
|
]
|
|
149
149
|
},
|
|
150
|
-
"
|
|
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": "
|
|
192
|
+
"llm": "auto"
|
|
194
193
|
},
|
|
195
194
|
"[with-claude]": {
|
|
196
195
|
"llm": "anthropic"
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import cds from "@sap/cds"
|
|
2
2
|
import { agentMessage, firstDataPart, partsToText } from "../../../lib/utils/message-handling.js"
|
|
3
3
|
import { audit, short } from "../../../lib/utils/utils.js"
|
|
4
|
+
import * as metrics from "../../../lib/telemetry/metrics.js"
|
|
4
5
|
|
|
5
6
|
const LOG = cds.log("agents")
|
|
6
7
|
|
|
7
8
|
export const HITL_METADATA_KEY = "sap.cds.agents.hitl"
|
|
9
|
+
export const TIMEOUT_HITL_METADATA_KEY = "sap.cds.agents.timeout-hitl"
|
|
10
|
+
export const INPUT_REQUIRED_METADATA_KEY = "sap.cds.agents.input-required"
|
|
11
|
+
|
|
12
|
+
function approvalOptions() {
|
|
13
|
+
return [
|
|
14
|
+
{ value: "approve", label: cds.i18n.messages.at("HITL_APPROVE") },
|
|
15
|
+
{ value: "reject", label: cds.i18n.messages.at("HITL_REJECT") },
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function timeoutOptions() {
|
|
20
|
+
return [
|
|
21
|
+
{ value: "continue", label: cds.i18n.messages.at("HITL_CONTINUE") },
|
|
22
|
+
{ value: "reject", label: cds.i18n.messages.at("HITL_STOP") },
|
|
23
|
+
]
|
|
24
|
+
}
|
|
8
25
|
export const requiresHitl = (result) =>
|
|
9
26
|
result?.__interrupt__?.length > 0 || result?.interrupts?.length > 0
|
|
10
27
|
|
|
@@ -22,6 +39,21 @@ export function parseResumeDecision(userText) {
|
|
|
22
39
|
}
|
|
23
40
|
}
|
|
24
41
|
|
|
42
|
+
export function patchRejectMessage(dataPart) {
|
|
43
|
+
if (!Array.isArray(dataPart?.decisions)) return dataPart
|
|
44
|
+
return {
|
|
45
|
+
...dataPart,
|
|
46
|
+
decisions: dataPart.decisions.map((decision) =>
|
|
47
|
+
decision?.type === "reject"
|
|
48
|
+
? {
|
|
49
|
+
...decision,
|
|
50
|
+
message: `The user rejected this particular tool invocation with the reason: ${decision.message ?? ""}`,
|
|
51
|
+
}
|
|
52
|
+
: decision,
|
|
53
|
+
),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
25
57
|
function decisionsForAudit(resume, actionRequests = []) {
|
|
26
58
|
if (!Array.isArray(resume?.decisions)) return [{ action: null, decision: resume }]
|
|
27
59
|
return resume.decisions.map((decision, index) => ({
|
|
@@ -30,6 +62,25 @@ function decisionsForAudit(resume, actionRequests = []) {
|
|
|
30
62
|
}))
|
|
31
63
|
}
|
|
32
64
|
|
|
65
|
+
function hitlMetricAttrs(serviceName, action, decision) {
|
|
66
|
+
return {
|
|
67
|
+
...metrics.attrs(serviceName),
|
|
68
|
+
"agent.hitl.action": action?.name,
|
|
69
|
+
...(decision && { "agent.hitl.decision": decision }),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function recordHitlDecisions(serviceName, actionRequests, resume, actionOffset = 0) {
|
|
74
|
+
if (!Array.isArray(resume?.decisions)) return
|
|
75
|
+
for (const [index, decision] of resume.decisions.entries()) {
|
|
76
|
+
if (!decision?.type) continue
|
|
77
|
+
metrics.hitlDecisions.add(
|
|
78
|
+
1,
|
|
79
|
+
hitlMetricAttrs(serviceName, actionRequests[index + actionOffset], decision.type),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
33
84
|
function extractInterruptDescription(resultOrErr) {
|
|
34
85
|
const interrupt = resultOrErr.__interrupt__?.[0] || resultOrErr.interrupts?.[0]
|
|
35
86
|
const payload = interrupt?.value
|
|
@@ -173,7 +224,10 @@ function publishInputRequired({ requestContext, eventBus, description, interrupt
|
|
|
173
224
|
contextId,
|
|
174
225
|
status: {
|
|
175
226
|
state: "input-required",
|
|
176
|
-
message: agentMessage(description, interruptData, {
|
|
227
|
+
message: agentMessage(description, interruptData, {
|
|
228
|
+
[HITL_METADATA_KEY]: pending,
|
|
229
|
+
[INPUT_REQUIRED_METADATA_KEY]: { options: approvalOptions() },
|
|
230
|
+
}),
|
|
177
231
|
timestamp: new Date().toISOString(),
|
|
178
232
|
},
|
|
179
233
|
final: true,
|
|
@@ -181,6 +235,64 @@ function publishInputRequired({ requestContext, eventBus, description, interrupt
|
|
|
181
235
|
eventBus.finished()
|
|
182
236
|
}
|
|
183
237
|
|
|
238
|
+
export function isTimeoutHitl(task) {
|
|
239
|
+
return task?.status?.message?.metadata?.[TIMEOUT_HITL_METADATA_KEY] === true
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function publishTimeoutHitl({ requestContext, eventBus, description, serviceName }) {
|
|
243
|
+
const { taskId, contextId } = requestContext
|
|
244
|
+
LOG.info(serviceName, "-", "timeout awaiting decision", { conversation: short(contextId) })
|
|
245
|
+
audit("AgentInputRequired", {
|
|
246
|
+
data: { taskId, contextId, service: serviceName, reason: "timeout", description },
|
|
247
|
+
})
|
|
248
|
+
eventBus.publish({
|
|
249
|
+
kind: "status-update",
|
|
250
|
+
taskId,
|
|
251
|
+
contextId,
|
|
252
|
+
status: {
|
|
253
|
+
state: "input-required",
|
|
254
|
+
message: agentMessage(description, undefined, {
|
|
255
|
+
[TIMEOUT_HITL_METADATA_KEY]: true,
|
|
256
|
+
[INPUT_REQUIRED_METADATA_KEY]: { options: timeoutOptions() },
|
|
257
|
+
}),
|
|
258
|
+
timestamp: new Date().toISOString(),
|
|
259
|
+
},
|
|
260
|
+
final: true,
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function resumeTimeoutHitl({ requestContext, eventBus, stream, signal }) {
|
|
265
|
+
const { taskId, contextId } = requestContext
|
|
266
|
+
const decision = partsToText(requestContext.userMessage?.parts).trim()
|
|
267
|
+
if (!decision) throw new Error(cds.i18n.messages.at("RESUME_REQUIRES_TEXT"))
|
|
268
|
+
|
|
269
|
+
if (/^(continue|approve|yes|confirm|ok)$/i.test(decision)) {
|
|
270
|
+
LOG.info("timeout continuation approved", { conversation: short(contextId) })
|
|
271
|
+
audit("AgentTaskResumed", {
|
|
272
|
+
data: { taskId, contextId, service: cds.context?.["agent.service"], reason: "timeout" },
|
|
273
|
+
})
|
|
274
|
+
const resumed = await stream(null, signal)
|
|
275
|
+
return resumed.state
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
LOG.info("timeout continuation declined", { conversation: short(contextId) })
|
|
279
|
+
audit("AgentTaskCanceled", {
|
|
280
|
+
data: { taskId, contextId, service: cds.context?.["agent.service"], reason: "timeout" },
|
|
281
|
+
})
|
|
282
|
+
eventBus.publish({
|
|
283
|
+
kind: "status-update",
|
|
284
|
+
taskId,
|
|
285
|
+
contextId,
|
|
286
|
+
status: {
|
|
287
|
+
state: "canceled",
|
|
288
|
+
message: agentMessage("Task stopped by user after timeout."),
|
|
289
|
+
timestamp: new Date().toISOString(),
|
|
290
|
+
},
|
|
291
|
+
final: true,
|
|
292
|
+
})
|
|
293
|
+
return undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
184
296
|
export async function resumeHitl({ requestContext, graph, config, eventBus, stream, signal }) {
|
|
185
297
|
const { taskId, contextId } = requestContext
|
|
186
298
|
const dataPart = firstDataPart(requestContext.userMessage?.parts)
|
|
@@ -189,14 +301,16 @@ export async function resumeHitl({ requestContext, graph, config, eventBus, stre
|
|
|
189
301
|
throw new Error(cds.i18n.messages.at("RESUME_REQUIRES_TEXT"))
|
|
190
302
|
}
|
|
191
303
|
const { Command } = await import("@langchain/langgraph")
|
|
192
|
-
let resume = dataPart !== undefined ? dataPart : parseResumeDecision(userText)
|
|
304
|
+
let resume = dataPart !== undefined ? patchRejectMessage(dataPart) : parseResumeDecision(userText)
|
|
193
305
|
let actionRequests = []
|
|
194
306
|
|
|
195
307
|
if (Array.isArray(resume?.decisions)) {
|
|
196
308
|
const pending = pendingHitlFromTask(requestContext.task)
|
|
197
309
|
const actionCount = pending?.actionCount ?? (await getPendingHitlActionCount(graph, config))
|
|
198
310
|
actionRequests = pendingActionRequests(requestContext.task, pending)
|
|
311
|
+
const priorDecisionCount = pending?.decisions?.length || 0
|
|
199
312
|
const decisions = [...(pending?.decisions || []), ...resume.decisions]
|
|
313
|
+
recordHitlDecisions(cds.context?.["agent.service"], actionRequests, resume, priorDecisionCount)
|
|
200
314
|
if (decisions.length < actionCount) {
|
|
201
315
|
const interruptData = firstDataPart(requestContext.task?.status?.message?.parts)
|
|
202
316
|
const nextPending = { ...pending, actionCount, decisions }
|
|
@@ -243,7 +357,11 @@ export function handleHitlInterrupt({
|
|
|
243
357
|
const { taskId, contextId } = requestContext
|
|
244
358
|
const description = extractInterruptDescription(result)
|
|
245
359
|
const interruptData = extractInterruptData(result)
|
|
246
|
-
|
|
360
|
+
const actionRequests = interruptData?.actionRequests || interruptActionRequests(result)
|
|
361
|
+
for (const action of actionRequests) {
|
|
362
|
+
metrics.hitlGates.add(1, hitlMetricAttrs(serviceName, action))
|
|
363
|
+
}
|
|
364
|
+
LOG.info(serviceName, "-", "input-required", { conversation: short(contextId), duration })
|
|
247
365
|
onInputRequired?.(description)
|
|
248
366
|
audit("AgentInputRequired", {
|
|
249
367
|
data: { taskId, contextId, service: serviceName, description, interruptData },
|