@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
|
@@ -9,7 +9,14 @@ import { convertUsageData } from "../../lib/telemetry/chat-tracing.js"
|
|
|
9
9
|
import { triggerCleanup } from "../../lib/protocol/persistence/cleanup.js"
|
|
10
10
|
import { COLLECT_RESULT } from "./chat.js"
|
|
11
11
|
import { linkTraceToPrompt } from "../../lib/telemetry/mlflow/tracing.js"
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
handleHitlInterrupt,
|
|
14
|
+
isTimeoutHitl,
|
|
15
|
+
publishTimeoutHitl,
|
|
16
|
+
requiresHitl,
|
|
17
|
+
resumeHitl,
|
|
18
|
+
resumeTimeoutHitl,
|
|
19
|
+
} from "./graph-executor/hitl.js"
|
|
13
20
|
|
|
14
21
|
const LOG = cds.log("agents")
|
|
15
22
|
|
|
@@ -145,7 +152,7 @@ class GraphExecutor {
|
|
|
145
152
|
abort(taskId) {
|
|
146
153
|
const controller = this._abortControllers.get(taskId)
|
|
147
154
|
if (controller && !controller.signal.aborted) {
|
|
148
|
-
LOG.info("aborting", { task: short(taskId)
|
|
155
|
+
LOG.info(this._srv.name, "aborting", { task: short(taskId) })
|
|
149
156
|
controller.abort()
|
|
150
157
|
}
|
|
151
158
|
}
|
|
@@ -163,7 +170,7 @@ class GraphExecutor {
|
|
|
163
170
|
const { CdsCheckpointSaver } =
|
|
164
171
|
await import("../../lib/protocol/persistence/checkpoint-saver.js")
|
|
165
172
|
resolved.checkpointer = new CdsCheckpointSaver()
|
|
166
|
-
LOG.debug("
|
|
173
|
+
LOG.debug(this._srv.name, "- auto-injected CdsCheckpointSaver")
|
|
167
174
|
}
|
|
168
175
|
this._graph = resolved
|
|
169
176
|
return this._graph
|
|
@@ -174,12 +181,10 @@ class GraphExecutor {
|
|
|
174
181
|
* per-token artifact-update SSE events as LLM tokens arrive.
|
|
175
182
|
*/
|
|
176
183
|
async _streamWithPublish(graph, input, config, eventBus, taskId, contextId, signal) {
|
|
177
|
-
const maxExecution = ms4(cds.env.agents?.
|
|
178
|
-
const grace = this._getGrace()
|
|
179
|
-
const softTimeout = Math.max(maxExecution - grace, 1000)
|
|
184
|
+
const maxExecution = ms4(cds.env.agents?.quotas?.maxExecutionTimePerTask || "5min")
|
|
180
185
|
|
|
181
186
|
const controller = new AbortController()
|
|
182
|
-
const timeoutHandle = setTimeout(() => controller.abort(),
|
|
187
|
+
const timeoutHandle = setTimeout(() => controller.abort(), maxExecution)
|
|
183
188
|
const combinedSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal
|
|
184
189
|
|
|
185
190
|
let tokenCount = 0
|
|
@@ -271,8 +276,8 @@ class GraphExecutor {
|
|
|
271
276
|
if (signal?.aborted) {
|
|
272
277
|
throw new AbortError("Task execution aborted")
|
|
273
278
|
}
|
|
274
|
-
throw new TimeoutError(`Graph execution timed out after ${
|
|
275
|
-
timeout:
|
|
279
|
+
throw new TimeoutError(`Graph execution timed out after ${maxExecution / 1000}s`, {
|
|
280
|
+
timeout: maxExecution,
|
|
276
281
|
})
|
|
277
282
|
}
|
|
278
283
|
throw err
|
|
@@ -312,23 +317,13 @@ class GraphExecutor {
|
|
|
312
317
|
|
|
313
318
|
return { state: finalState, tokenCount }
|
|
314
319
|
}
|
|
315
|
-
/**
|
|
316
|
-
* Parse configured timeout grace period.
|
|
317
|
-
*/
|
|
318
|
-
_getGrace() {
|
|
319
|
-
return ms4(cds.env.agents?.pool?.timeoutGrace ?? "15s")
|
|
320
|
-
}
|
|
321
|
-
|
|
322
320
|
async _invokeWithTimeout(graph, input, config, signal) {
|
|
323
|
-
const maxExecution = ms4(cds.env.agents?.
|
|
324
|
-
const grace = this._getGrace()
|
|
325
|
-
// Soft timeout fires early to allow graceful summarization
|
|
326
|
-
const softTimeout = Math.max(maxExecution - grace, 1000)
|
|
321
|
+
const maxExecution = ms4(cds.env.agents?.quotas?.maxExecutionTimePerTask || "5min")
|
|
327
322
|
|
|
328
323
|
// Use explicit AbortController + setTimeout (reffed timer keeps event loop alive)
|
|
329
324
|
// instead of AbortSignal.timeout() which uses an unreffed timer
|
|
330
325
|
const timeoutController = new AbortController()
|
|
331
|
-
const timer = setTimeout(() => timeoutController.abort(),
|
|
326
|
+
const timer = setTimeout(() => timeoutController.abort(), maxExecution)
|
|
332
327
|
|
|
333
328
|
// Combine caller-provided abort signal with timeout
|
|
334
329
|
const combinedSignal = signal
|
|
@@ -344,8 +339,8 @@ class GraphExecutor {
|
|
|
344
339
|
if (signal?.aborted) {
|
|
345
340
|
throw new AbortError("Task execution aborted")
|
|
346
341
|
}
|
|
347
|
-
throw new TimeoutError(`Graph execution timed out after ${
|
|
348
|
-
timeout:
|
|
342
|
+
throw new TimeoutError(`Graph execution timed out after ${maxExecution / 1000}s`, {
|
|
343
|
+
timeout: maxExecution,
|
|
349
344
|
})
|
|
350
345
|
}
|
|
351
346
|
throw err
|
|
@@ -359,7 +354,6 @@ class GraphExecutor {
|
|
|
359
354
|
*/
|
|
360
355
|
async _summarizePartialWork(taskId, contextId, serviceName, reason) {
|
|
361
356
|
const { summarizePartialWork } = await import("../../lib/agents/summarize-on-timeout.js")
|
|
362
|
-
const grace = this._getGrace()
|
|
363
357
|
return summarizePartialWork({
|
|
364
358
|
taskId,
|
|
365
359
|
contextId,
|
|
@@ -367,7 +361,8 @@ class GraphExecutor {
|
|
|
367
361
|
reason,
|
|
368
362
|
checkpointer: this._graph?.checkpointer,
|
|
369
363
|
getModel: () => this._srv.send("buildModel"),
|
|
370
|
-
|
|
364
|
+
// Summary runs after graph abort, so no execution-time grace is needed.
|
|
365
|
+
timeout: 10_000,
|
|
371
366
|
})
|
|
372
367
|
}
|
|
373
368
|
|
|
@@ -447,9 +442,8 @@ class GraphExecutor {
|
|
|
447
442
|
cds.env.agents?.fileIO,
|
|
448
443
|
)
|
|
449
444
|
if (rejection) {
|
|
450
|
-
LOG.warn("input file rejected", {
|
|
445
|
+
LOG.warn(serviceName, "-", "input file rejected", {
|
|
451
446
|
conversation: short(contextId),
|
|
452
|
-
service: serviceName,
|
|
453
447
|
name: safeName,
|
|
454
448
|
mimeType: safeMime,
|
|
455
449
|
reason: rejection,
|
|
@@ -458,9 +452,8 @@ class GraphExecutor {
|
|
|
458
452
|
}
|
|
459
453
|
const buf = Buffer.from(file.bytes, "base64")
|
|
460
454
|
await fileStore.saveInputFile(taskId, safeName, safeMime, buf)
|
|
461
|
-
LOG.info("file uploaded", {
|
|
455
|
+
LOG.info(serviceName, "-", "file uploaded", {
|
|
462
456
|
conversation: short(contextId),
|
|
463
|
-
service: serviceName,
|
|
464
457
|
name: safeName,
|
|
465
458
|
mimeType: safeMime,
|
|
466
459
|
size: buf.length,
|
|
@@ -564,7 +557,8 @@ class GraphExecutor {
|
|
|
564
557
|
const t0 = Date.now()
|
|
565
558
|
|
|
566
559
|
if (isResume) {
|
|
567
|
-
|
|
560
|
+
const resume = isTimeoutHitl(requestContext.task) ? resumeTimeoutHitl : resumeHitl
|
|
561
|
+
result = await resume({
|
|
568
562
|
requestContext,
|
|
569
563
|
graph,
|
|
570
564
|
config,
|
|
@@ -619,7 +613,7 @@ class GraphExecutor {
|
|
|
619
613
|
const outputMapper = this._outputMapper || defaultOutputMapper
|
|
620
614
|
const output = outputMapper(result) || "I could not generate a response."
|
|
621
615
|
|
|
622
|
-
LOG.info("completed", { conversation: short(contextId),
|
|
616
|
+
LOG.info(serviceName, "completed", { conversation: short(contextId), duration })
|
|
623
617
|
|
|
624
618
|
if (wfSpan) {
|
|
625
619
|
wfSpan.setAttribute("agent.outcome", "completed")
|
|
@@ -750,9 +744,8 @@ class GraphExecutor {
|
|
|
750
744
|
? Buffer.byteLength(artifact.file.bytes, "base64")
|
|
751
745
|
: 0
|
|
752
746
|
if (declaredBytes > maxFileBytes) {
|
|
753
|
-
LOG.warn("emit_file_part artifact exceeds cap; skipping", {
|
|
747
|
+
LOG.warn(serviceName, "-", "emit_file_part artifact exceeds cap; skipping", {
|
|
754
748
|
conversation: short(contextId),
|
|
755
|
-
service: serviceName,
|
|
756
749
|
name: artifact.file?.name,
|
|
757
750
|
size: declaredBytes,
|
|
758
751
|
cap: maxFileBytes,
|
|
@@ -777,9 +770,8 @@ class GraphExecutor {
|
|
|
777
770
|
const outputMeta = await fileStore.listOutputFilesMeta(taskId)
|
|
778
771
|
for (const meta of outputMeta) {
|
|
779
772
|
if (meta.size > maxFileBytes) {
|
|
780
|
-
LOG.warn("output file exceeds cap; skipping", {
|
|
773
|
+
LOG.warn(serviceName, "-", "output file exceeds cap; skipping", {
|
|
781
774
|
conversation: short(contextId),
|
|
782
|
-
service: serviceName,
|
|
783
775
|
name: meta.name,
|
|
784
776
|
size: meta.size,
|
|
785
777
|
cap: maxFileBytes,
|
|
@@ -798,7 +790,7 @@ class GraphExecutor {
|
|
|
798
790
|
// Exclude emit_file_part outputs — those are this agent's own artifacts, not
|
|
799
791
|
// downstream files, and re-persisting them would create spurious /uploads/ entries.
|
|
800
792
|
// Enforce the same size + MIME guard as inbound uploads so a malicious
|
|
801
|
-
//
|
|
793
|
+
// subagent cannot bypass the cap by echoing an oversized FilePart.
|
|
802
794
|
await Promise.all(
|
|
803
795
|
fileArtifacts
|
|
804
796
|
.slice(0, source1Count)
|
|
@@ -806,9 +798,8 @@ class GraphExecutor {
|
|
|
806
798
|
if (!fa.file?.bytes || !fa.file?.name || fa._fromEmitFilePart) return false
|
|
807
799
|
const rejection = checkInputFile(fa.file, cds.env.agents?.fileIO)
|
|
808
800
|
if (rejection) {
|
|
809
|
-
LOG.warn("downstream file re-persist rejected", {
|
|
801
|
+
LOG.warn(serviceName, "-", "downstream file re-persist rejected", {
|
|
810
802
|
conversation: short(contextId),
|
|
811
|
-
service: serviceName,
|
|
812
803
|
name: fa.file.name,
|
|
813
804
|
reason: rejection,
|
|
814
805
|
})
|
|
@@ -828,9 +819,8 @@ class GraphExecutor {
|
|
|
828
819
|
// Capture source classification for the log line below.
|
|
829
820
|
for (const filePart of fileArtifacts) {
|
|
830
821
|
if (!filePart.file?.name) {
|
|
831
|
-
LOG.warn("skipping malformed file artifact", {
|
|
822
|
+
LOG.warn(serviceName, "-", "skipping malformed file artifact", {
|
|
832
823
|
conversation: short(contextId),
|
|
833
|
-
service: serviceName,
|
|
834
824
|
})
|
|
835
825
|
continue
|
|
836
826
|
}
|
|
@@ -841,9 +831,8 @@ class GraphExecutor {
|
|
|
841
831
|
typeof filePart.file?.bytes === "string"
|
|
842
832
|
? Buffer.byteLength(filePart.file.bytes, "base64")
|
|
843
833
|
: 0
|
|
844
|
-
LOG.info("file emitted", {
|
|
834
|
+
LOG.info(serviceName, "-", "file emitted", {
|
|
845
835
|
conversation: short(contextId),
|
|
846
|
-
service: serviceName,
|
|
847
836
|
name: safeName,
|
|
848
837
|
mimeType: filePart.file?.mimeType,
|
|
849
838
|
bytes: decodedSize,
|
|
@@ -863,13 +852,12 @@ class GraphExecutor {
|
|
|
863
852
|
|
|
864
853
|
dataArtifacts.forEach((artifact, i) => {
|
|
865
854
|
if (artifact.data == null || typeof artifact.data !== "object") {
|
|
866
|
-
LOG.warn("skipping malformed data artifact", {
|
|
855
|
+
LOG.warn(serviceName, "-", "skipping malformed data artifact", {
|
|
867
856
|
conversation: short(contextId),
|
|
868
|
-
service: serviceName,
|
|
869
857
|
})
|
|
870
858
|
return
|
|
871
859
|
}
|
|
872
|
-
LOG.info("data emitted", { conversation: short(contextId)
|
|
860
|
+
LOG.info(serviceName, "-", "data emitted", { conversation: short(contextId) })
|
|
873
861
|
eventBus.publish({
|
|
874
862
|
kind: "artifact-update",
|
|
875
863
|
taskId,
|
|
@@ -902,7 +890,7 @@ class GraphExecutor {
|
|
|
902
890
|
// Aborted (client disconnect or tasks/cancel) — publish canceled, not failed
|
|
903
891
|
// Use name check (not instanceof) to also catch native DOMException AbortError from LangGraph
|
|
904
892
|
if (err.name === "AbortError") {
|
|
905
|
-
LOG.info("canceled", { conversation: short(contextId)
|
|
893
|
+
LOG.info(serviceName, "-", "canceled", { conversation: short(contextId) })
|
|
906
894
|
if (wfSpan) wfSpan.setAttribute("agent.outcome", "canceled")
|
|
907
895
|
|
|
908
896
|
audit("AgentTaskCanceled", {
|
|
@@ -923,62 +911,34 @@ class GraphExecutor {
|
|
|
923
911
|
return
|
|
924
912
|
}
|
|
925
913
|
|
|
926
|
-
// Timeout —
|
|
914
|
+
// Timeout — pause at checkpoint. User decides whether to resume graph.
|
|
927
915
|
if (err.name === "TimeoutError") {
|
|
928
|
-
LOG.warn("timeout", { conversation: short(contextId)
|
|
916
|
+
LOG.warn(serviceName, "-", "timeout", { conversation: short(contextId) })
|
|
929
917
|
|
|
930
|
-
if (wfSpan) wfSpan.setAttribute("agent.outcome", "
|
|
931
|
-
metrics.errorsTotal.add(1, { ...mAttrs, "agent.error.code": "timeout" })
|
|
918
|
+
if (wfSpan) wfSpan.setAttribute("agent.outcome", "input-required")
|
|
932
919
|
|
|
933
920
|
const summary = await this._summarizePartialWork(
|
|
934
921
|
taskId,
|
|
935
922
|
contextId,
|
|
936
923
|
serviceName,
|
|
937
|
-
"
|
|
924
|
+
"timeOut",
|
|
938
925
|
)
|
|
939
926
|
|
|
940
|
-
|
|
941
|
-
data: {
|
|
942
|
-
taskId,
|
|
943
|
-
contextId,
|
|
944
|
-
service: serviceName,
|
|
945
|
-
error: err.message,
|
|
946
|
-
errorCode: "timeout",
|
|
947
|
-
task: requestContext.task,
|
|
948
|
-
},
|
|
949
|
-
})
|
|
950
|
-
|
|
951
|
-
eventBus.publish({
|
|
952
|
-
kind: "status-update",
|
|
953
|
-
taskId,
|
|
954
|
-
contextId,
|
|
955
|
-
status: {
|
|
956
|
-
state: "canceled",
|
|
957
|
-
message: agentMessage(summary),
|
|
958
|
-
timestamp: new Date().toISOString(),
|
|
959
|
-
},
|
|
960
|
-
final: true,
|
|
961
|
-
})
|
|
927
|
+
publishTimeoutHitl({ requestContext, eventBus, description: summary, serviceName })
|
|
962
928
|
return
|
|
963
929
|
}
|
|
964
930
|
|
|
965
931
|
// Quota exceeded — summarize partial work instead of raw error
|
|
966
932
|
if (err.quotaExceeded) {
|
|
967
|
-
LOG.warn("quota exceeded", {
|
|
933
|
+
LOG.warn(serviceName, "-", "quota exceeded", {
|
|
968
934
|
conversation: short(contextId),
|
|
969
|
-
service: serviceName,
|
|
970
935
|
error: err.message,
|
|
971
936
|
})
|
|
972
937
|
|
|
973
938
|
if (wfSpan) wfSpan.setAttribute("agent.outcome", "quota_exceeded")
|
|
974
939
|
metrics.errorsTotal.add(1, { ...mAttrs, "agent.error.code": "quota_exceeded" })
|
|
975
940
|
|
|
976
|
-
const summary = await this._summarizePartialWork(
|
|
977
|
-
taskId,
|
|
978
|
-
contextId,
|
|
979
|
-
serviceName,
|
|
980
|
-
"quota exceeded",
|
|
981
|
-
)
|
|
941
|
+
const summary = await this._summarizePartialWork(taskId, contextId, serviceName, "quota")
|
|
982
942
|
|
|
983
943
|
audit("AgentTaskFailed", {
|
|
984
944
|
data: {
|
|
@@ -1005,16 +965,11 @@ class GraphExecutor {
|
|
|
1005
965
|
return
|
|
1006
966
|
}
|
|
1007
967
|
|
|
1008
|
-
LOG.error(
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
LOG.debug("failed stack", {
|
|
1014
|
-
conversation: short(contextId),
|
|
1015
|
-
service: serviceName,
|
|
1016
|
-
stack: err.stack,
|
|
1017
|
-
})
|
|
968
|
+
LOG.error(
|
|
969
|
+
Object.assign(err, {
|
|
970
|
+
conversation: short(contextId),
|
|
971
|
+
}),
|
|
972
|
+
)
|
|
1018
973
|
|
|
1019
974
|
if (wfSpan) {
|
|
1020
975
|
wfSpan.setAttribute("agent.outcome", "failed")
|
package/srv/handlers/index.js
CHANGED
|
@@ -67,7 +67,7 @@ export default function registerDefaultAgentHandlers(srv) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const { buildMcpTools } = await import("./mcp-tools.js")
|
|
70
|
-
const { buildSubAgentTool } = await import("./
|
|
70
|
+
const { buildSubAgentTool } = await import("./subagent-tools.js")
|
|
71
71
|
|
|
72
72
|
const results = await Promise.allSettled([
|
|
73
73
|
...mcpEntries.map((e) => buildMcpTools(e.service ?? e)),
|
|
@@ -76,7 +76,7 @@ export default function registerDefaultAgentHandlers(srv) {
|
|
|
76
76
|
|
|
77
77
|
const extraTools = []
|
|
78
78
|
for (const r of results) {
|
|
79
|
-
// MCP connections yield an array of tools;
|
|
79
|
+
// MCP connections yield an array of tools; subagent connections yield a
|
|
80
80
|
// single tool. Normalize both so instrumentTools sees a flat tool list.
|
|
81
81
|
if (r.status === "fulfilled") {
|
|
82
82
|
if (Array.isArray(r.value)) extraTools.push(...r.value.filter(Boolean))
|
|
@@ -101,7 +101,7 @@ export async function buildMcpToolsFromConnection(serviceName) {
|
|
|
101
101
|
|
|
102
102
|
const path = typeof credentials === "object" ? credentials?.path : null
|
|
103
103
|
const mcpUrl = url.replace(/\/$/, "") + (path ? `/${path.replace(/^\//, "")}` : "")
|
|
104
|
-
LOG.info(`Connecting to MCP
|
|
104
|
+
LOG.info(`Connecting to MCP service ${serviceName}`, { at: mcpUrl })
|
|
105
105
|
|
|
106
106
|
const resolveHeaders = async () => {
|
|
107
107
|
if (destinationName) {
|
|
@@ -113,7 +113,8 @@ export async function buildMcpToolsFromConnection(serviceName) {
|
|
|
113
113
|
return token ? { Authorization: `Bearer ${token}` } : {}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
const toolFilter = cds.requires[serviceName]?.mcp?.tools
|
|
117
|
+
return { _mcpDynamic: true, mcpUrl, serviceName, resolveHeaders, toolFilter }
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
export async function buildMcpTools(serviceName) {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import cds from "@sap/cds"
|
|
2
2
|
import { tool } from "@langchain/core/tools"
|
|
3
3
|
import { z } from "zod"
|
|
4
|
+
import { getTracer } from "../../lib/telemetry/metrics.js"
|
|
4
5
|
import { LangGraphExecutor } from "../langgraph-executor-srv.js"
|
|
5
6
|
import { short, toolName } from "../../lib/utils/utils.js"
|
|
6
7
|
|
|
7
|
-
const LOG = cds.log("agents:
|
|
8
|
+
const LOG = cds.log("agents:a2a|agents|subagents|a2a")
|
|
9
|
+
|
|
10
|
+
const TRUNCATE = cds.env.agents?.truncate || 111
|
|
11
|
+
const truncated = (msg) => (msg?.length > TRUNCATE ? msg.slice(0, TRUNCATE) + "..." : msg)
|
|
8
12
|
|
|
9
13
|
import { inspect } from "util"
|
|
10
14
|
/**
|
|
@@ -93,10 +97,14 @@ function createA2ATool(client, agentCard) {
|
|
|
93
97
|
LOG.trace(`Raw results from ${subagent}`, inspect(result, { depth: null, colors: true }))
|
|
94
98
|
let response = formatToolResult(extractResult(result))
|
|
95
99
|
if (response)
|
|
96
|
-
LOG.info(
|
|
100
|
+
LOG.info(
|
|
101
|
+
`Got response from ${subagent}`,
|
|
102
|
+
{ messageId },
|
|
103
|
+
"\n\n" + truncated(response) + "\n",
|
|
104
|
+
)
|
|
97
105
|
return response
|
|
98
106
|
} catch (err) {
|
|
99
|
-
LOG.warn("
|
|
107
|
+
LOG.warn("Subagent tool error", { subagent, error: err.message })
|
|
100
108
|
return `Error communicating with ${subagent}: ${err.message}`
|
|
101
109
|
}
|
|
102
110
|
},
|
|
@@ -112,13 +120,13 @@ function createA2ATool(client, agentCard) {
|
|
|
112
120
|
|
|
113
121
|
export async function buildSubAgentToolLocally(serviceName) {
|
|
114
122
|
const srv = cds.services[serviceName]
|
|
115
|
-
if (!srv) throw new Error(`[
|
|
123
|
+
if (!srv) throw new Error(`[subagents] No local service "${serviceName}" found.`)
|
|
116
124
|
|
|
117
125
|
const executor = LangGraphExecutor.for(srv)
|
|
118
126
|
|
|
119
127
|
const { generateAgentCard } = await import("../../lib/protocol/agent-card.js")
|
|
120
128
|
const agentCard = generateAgentCard(srv)
|
|
121
|
-
LOG.info(`Connecting to
|
|
129
|
+
LOG.info(`Connecting to subagent ${serviceName}`, "(local)")
|
|
122
130
|
|
|
123
131
|
const { RequestContext, DefaultExecutionEventBus } = await import("@a2a-js/sdk/server")
|
|
124
132
|
|
|
@@ -147,7 +155,7 @@ export async function buildSubAgentToolLocally(serviceName) {
|
|
|
147
155
|
.map((p) => p.text)
|
|
148
156
|
.join("\n")
|
|
149
157
|
if (text) statusText = text
|
|
150
|
-
if (state === "failed") failed = text || "
|
|
158
|
+
if (state === "failed") failed = text || "Subagent execution failed."
|
|
151
159
|
} else if (event.kind === "artifact-update") {
|
|
152
160
|
const parts = event.artifact?.parts || []
|
|
153
161
|
if (event.artifact?.artifactId === "response") {
|
|
@@ -176,19 +184,17 @@ export async function buildSubAgentToolLocally(serviceName) {
|
|
|
176
184
|
taskId,
|
|
177
185
|
contextId,
|
|
178
186
|
)
|
|
179
|
-
const truncated = message?.length > 80 ? message.slice(0, 80) + "..." : message
|
|
180
187
|
|
|
181
188
|
try {
|
|
182
|
-
// Run the
|
|
189
|
+
// Run the subagent detached from the calling agent, in its own root
|
|
183
190
|
// context and transaction (cds.spawn creates + commits a fresh tx). A
|
|
184
191
|
// shared transaction breaks the stream.
|
|
185
192
|
await new Promise((resolve, reject) => {
|
|
186
193
|
cds
|
|
187
194
|
.spawn({ user: cds.context?.user, tenant: cds.context?.tenant }, async () => {
|
|
188
|
-
LOG.info("request", {
|
|
195
|
+
LOG.info(srv.name, "-", "request", {
|
|
189
196
|
conversation: short(contextId),
|
|
190
|
-
|
|
191
|
-
text: truncated,
|
|
197
|
+
text: truncated(message),
|
|
192
198
|
})
|
|
193
199
|
|
|
194
200
|
await executor.execute(requestContext, eventBus)
|
|
@@ -198,7 +204,7 @@ export async function buildSubAgentToolLocally(serviceName) {
|
|
|
198
204
|
.on("failed", reject)
|
|
199
205
|
})
|
|
200
206
|
} catch (err) {
|
|
201
|
-
LOG.warn("Local
|
|
207
|
+
LOG.warn("Local subagent tool error", { agent: agentCard.name, error: err.message })
|
|
202
208
|
return `Error running ${agentCard.name}: ${err.message}`
|
|
203
209
|
}
|
|
204
210
|
|
|
@@ -238,7 +244,7 @@ export async function buildSubAgentToolFromConnection(serviceName) {
|
|
|
238
244
|
|
|
239
245
|
if (!localUrl && !destinationName) {
|
|
240
246
|
throw new Error(
|
|
241
|
-
`[
|
|
247
|
+
`[subagents] Could not resolve URL or destination name for service "${serviceName}". Check cds.requires config.`,
|
|
242
248
|
)
|
|
243
249
|
}
|
|
244
250
|
|
|
@@ -286,13 +292,13 @@ export async function buildSubAgentToolFromConnection(serviceName) {
|
|
|
286
292
|
|
|
287
293
|
if (!agentBaseUrl) {
|
|
288
294
|
throw new Error(
|
|
289
|
-
`[
|
|
295
|
+
`[subagents] Could not resolve URL for service "${serviceName}" after destination lookup.`,
|
|
290
296
|
)
|
|
291
297
|
}
|
|
292
298
|
|
|
293
299
|
const path = typeof credentials === "object" ? credentials?.path : null
|
|
294
300
|
const base = agentBaseUrl.replace(/\/$/, "") + (path ? `/${path.replace(/^\//, "")}` : "")
|
|
295
|
-
LOG.info(`Connecting to
|
|
301
|
+
LOG.info(`Connecting to subagent ${serviceName}`, { at: base })
|
|
296
302
|
|
|
297
303
|
// revisit: a2a agents may be tenant specific, card per tenant?
|
|
298
304
|
const initialHeaders = await resolveHeaders()
|
|
@@ -302,7 +308,7 @@ export async function buildSubAgentToolFromConnection(serviceName) {
|
|
|
302
308
|
})
|
|
303
309
|
if (!cardRes.ok) {
|
|
304
310
|
throw new Error(
|
|
305
|
-
`[
|
|
311
|
+
`[subagents] Agent card fetch failed for "${serviceName}" at ${cardUrl}: HTTP ${cardRes.status}`,
|
|
306
312
|
)
|
|
307
313
|
}
|
|
308
314
|
const agentCard = await cardRes.json()
|
|
@@ -312,6 +318,10 @@ export async function buildSubAgentToolFromConnection(serviceName) {
|
|
|
312
318
|
|
|
313
319
|
const customFetch = async (url, init = {}) => {
|
|
314
320
|
const headers = await resolveHeaders()
|
|
321
|
+
if (getTracer()) {
|
|
322
|
+
const { context, propagation } = await import("@opentelemetry/api")
|
|
323
|
+
propagation.inject(context.active(), headers)
|
|
324
|
+
}
|
|
315
325
|
return fetch(url, { ...init, headers: { ...headers, ...(init.headers || {}) } })
|
|
316
326
|
}
|
|
317
327
|
const factory = new ClientFactory(
|
package/srv/handlers/tools.js
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
executeCallActionTool,
|
|
12
12
|
executePerActionTool,
|
|
13
13
|
} from "@cap-js/mcp/lib/tools.js"
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
import { getFilteredEntities, getFilteredActions } from "@cap-js/mcp/lib/utils/tools-shared.js"
|
|
15
16
|
import { isTextMime } from "../../lib/agents/markdown/backends/mime-utils.js"
|
|
16
17
|
import { checkAuthorization } from "@cap-js/mcp/lib/auth.js"
|
|
17
18
|
|
|
@@ -31,7 +32,6 @@ function cachedAuth(srv) {
|
|
|
31
32
|
|
|
32
33
|
class GenericReadTool extends DynamicStructuredTool {
|
|
33
34
|
constructor(srv, entities) {
|
|
34
|
-
const log = getAgentLogger(srv)
|
|
35
35
|
const def = createGenericReadToolDefinition(Object.keys(entities), srv.name, "")
|
|
36
36
|
super({
|
|
37
37
|
name: def.name,
|
|
@@ -39,7 +39,7 @@ class GenericReadTool extends DynamicStructuredTool {
|
|
|
39
39
|
schema: def.inputSchema,
|
|
40
40
|
responseFormat: "content_and_artifact",
|
|
41
41
|
func: async (args) => {
|
|
42
|
-
return unwrap(await executeGenericReadTool(srv, entities, args
|
|
42
|
+
return unwrap(await executeGenericReadTool(srv, entities, args))
|
|
43
43
|
},
|
|
44
44
|
})
|
|
45
45
|
this.srv = srv
|
|
@@ -68,7 +68,6 @@ class GenericReadTool extends DynamicStructuredTool {
|
|
|
68
68
|
|
|
69
69
|
class DescribeTool extends DynamicStructuredTool {
|
|
70
70
|
constructor(srv, entities, actions) {
|
|
71
|
-
const log = getAgentLogger(srv)
|
|
72
71
|
const def = createDescribeToolDefinition(
|
|
73
72
|
Object.keys(entities),
|
|
74
73
|
Object.keys(actions),
|
|
@@ -80,8 +79,8 @@ class DescribeTool extends DynamicStructuredTool {
|
|
|
80
79
|
description: def.description,
|
|
81
80
|
schema: def.inputSchema,
|
|
82
81
|
responseFormat: "content_and_artifact",
|
|
83
|
-
func:
|
|
84
|
-
return unwrap(
|
|
82
|
+
func: (args) => {
|
|
83
|
+
return unwrap(executeDescribe(srv, entities, actions, args))
|
|
85
84
|
},
|
|
86
85
|
})
|
|
87
86
|
this.srv = srv
|
|
@@ -121,7 +120,6 @@ class DescribeTool extends DynamicStructuredTool {
|
|
|
121
120
|
|
|
122
121
|
class PerActionTool extends DynamicStructuredTool {
|
|
123
122
|
constructor(srv, actionName, action) {
|
|
124
|
-
const log = getAgentLogger(srv)
|
|
125
123
|
const def = createPerActionToolDefinition(actionName, action, srv.name, srv.model, "")
|
|
126
124
|
super({
|
|
127
125
|
name: def.name,
|
|
@@ -129,7 +127,7 @@ class PerActionTool extends DynamicStructuredTool {
|
|
|
129
127
|
schema: def.inputSchema,
|
|
130
128
|
responseFormat: "content_and_artifact",
|
|
131
129
|
func: async (args) => {
|
|
132
|
-
return unwrap(await executePerActionTool(srv, actionName, action, args
|
|
130
|
+
return unwrap(await executePerActionTool(srv, actionName, action, args))
|
|
133
131
|
},
|
|
134
132
|
})
|
|
135
133
|
this.srv = srv
|
|
@@ -145,7 +143,6 @@ class PerActionTool extends DynamicStructuredTool {
|
|
|
145
143
|
|
|
146
144
|
class CallActionTool extends DynamicStructuredTool {
|
|
147
145
|
constructor(srv, actions) {
|
|
148
|
-
const log = getAgentLogger(srv)
|
|
149
146
|
const def = createCallActionToolDefinition(Object.keys(actions), srv.name, "")
|
|
150
147
|
super({
|
|
151
148
|
name: def.name,
|
|
@@ -153,7 +150,7 @@ class CallActionTool extends DynamicStructuredTool {
|
|
|
153
150
|
schema: def.inputSchema,
|
|
154
151
|
responseFormat: "content_and_artifact",
|
|
155
152
|
func: async (args) => {
|
|
156
|
-
return unwrap(await executeCallActionTool(srv, actions, args
|
|
153
|
+
return unwrap(await executeCallActionTool(srv, actions, args))
|
|
157
154
|
},
|
|
158
155
|
})
|
|
159
156
|
this.srv = srv
|