Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions .docs/audit-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,22 @@ In development, audit events are logged to the console. In production, they are
<details>
<summary>Events</summary>

| Event | Trigger | Key Fields |
| ---------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------- |
| `AgentTaskStarted` | New task submitted | `taskId`, `contextId`, `service`, `userMessage` |
| `AgentTaskResumed` | HITL resume (approve/reject) | `taskId`, `contextId`, `service`, `decision`, `userMessage` |
| `AgentDecision` | LLM invocation returns | `taskId`, `service`, `model`, `iteration`, `toolCalls`, `inputTokens`, `outputTokens`, `duration` |
| `ToolInvocation` | Tool executed | `taskId`, `service`, `tool`, `args`, `outcome`, `result`, `duration` |
| `AgentInputRequired` | Agent requests human approval | `taskId`, `contextId`, `service`, `description`, `userMessage` |
| `AgentTaskCompleted` | Task succeeds | `taskId`, `contextId`, `service`, `duration`, `tokens`, `toolCalls`, `output`, `task` |
| `AgentTaskFailed` | Task fails | `taskId`, `contextId`, `service`, `error`, `errorCode`, `task` |
| `AgentTaskCanceled` | Task canceled | `taskId`, `service` |
| `QuotaExceeded` | Quota breach | `action`, `service`, `user`, `reason`, `forwardedIp` + `ip` (top-level) |
| `ContentFilterBlocked` | Input blocked by content filter | `service`, `user`, `taskId`, `reason`, `source` (`user` or `tool`) |

All events include the original event name in the `data` field for filtering and forensic reconstruction. Common fields (`uuid`, `tenant`, `user`, `time`) are auto-filled by `@cap-js/audit-logging`. Every event also carries a `correlationId` (`cds.context.id`) for cross-referencing with auto-emitted DPP events.
<!-- audit-docs:start -->

| Event | Fields |
| ----- | ------ |
| `AgentDecision` | `service`, `taskId`, `contextId`, `duration`, `iteration`, `model`, `tokenUsage`, `toolCalls` |
| `AgentInputRequired` | `service`, `taskId`, `contextId`, `description`, `interruptData` |
| `AgentTaskCanceled` | `service`, `taskId`, `contextId` |
| `AgentTaskCompleted` | `service`, `taskId`, `contextId`, `duration`, `task`, `tokenUsage`, `toolCalls` |
| `AgentTaskFailed` | `service`, `taskId`, `contextId`, `error`, `errorCode`, `task` |
| `AgentTaskResumed` | `service`, `taskId`, `contextId`, `decision` |
| `AgentTaskStarted` | `service`, `taskId`, `contextId`, `userMessage` |
| `ContentFilterBlocked` | `service`, `taskId`, `reason`, `source`, `user` |
| `IncomingMessageExceedingLength` | `service`, `forwardedIp`, `ip`, `message`, `user` |
| `QuotaExceeded` | `service`, `taskId`, `forwardedIp`, `ip`, `reason`, `user` |
| `ToolInvocation` | `service`, `taskId`, `args`, `duration`, `error?`, `outcome`, `tool` |
<!-- audit-docs:end -->

</details>

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/lint-prettier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ jobs:
- run: npm set min-release-age=3
- run: npm ci
- run: npm run lint
- run: npm run docs:audit:check
- run: npm run prettier:check
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
- Outgoing MCP and A2A connections now consider `credentials.path` together with the destination
- Added additional OpenTelemetry span attributes detailing how many content filters were active

### Changed

- Adjusted agent audit log attributes to follow latest recommendations

### Fixed

- Fixed skill loading for markdown-based agents
Expand Down
2 changes: 1 addition & 1 deletion lib/telemetry/chat-tracing.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ function _handleSuccess(
service: opts?.configurable?._service || cds.context?.["agent.service"],
model,
iteration: opts?.configurable?._iteration ?? cds.context?.["agent.iteration"],
toolCalls: response?.toolCalls,
toolCalls: response?.toolCalls.map((t) => t.name),
tokenUsage,
duration,
},
Expand Down
74 changes: 26 additions & 48 deletions lib/telemetry/tool-tracing.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function _patchToolsProto(proto) {
const toolName = this.name || this.constructor.name

const invoke = async (span) => {
const t0 = Date.now()
if (span) {
span.setAttribute("gen_ai.operation.name", "execute_tool")
span.setAttribute("gen_ai.provider.name", "langchain")
Expand All @@ -53,13 +54,16 @@ export function _patchToolsProto(proto) {
setSpanAttrs(span, mlflowAttrs("TOOL", { inputs, functionName: toolName }))
if (LOG._debug) span.setAttribute("gen_ai.tool.call.arguments", JSON.stringify(inputs))
}
const t0 = Date.now()
const taskId = config?.configurable?._taskId || cds.context?.["agent.task.id"]
let outcome
let errorMessage
let duration
try {
const result = await original.call(this, args, config)
const duration = Date.now() - t0
duration = Date.now() - t0

const semanticError = result?.artifact?.isError === true
const outcome = semanticError ? "error" : "success"
outcome = semanticError ? "error" : "success"
const outputs = result?.content ?? result

if (span) {
Expand All @@ -81,36 +85,16 @@ export function _patchToolsProto(proto) {
span.setAttribute("gen_ai.tool.call.result", outputs)
}
}

metrics.toolInvocations.add(1, {
"sap.tenantId": cds.context?.tenant || "anonymous",
"agent.service": config?.configurable?._service || cds.context?.["agent.service"],
tool: toolName,
outcome,
})

// Audit: record tool invocation
const taskId = config?.configurable?._taskId || cds.context?.["agent.task.id"]
if (taskId) {
if (semanticError) {
const resultStr = typeof outputs === "string" ? outputs : JSON.stringify(outputs)
audit("ToolInvocation", {
data: {
taskId,
service: config?.configurable?._service || cds.context?.["agent.service"],
tool: toolName,
args,
outcome,
...(semanticError
? { error: resultStr?.slice(0, 2000) }
: { result: resultStr?.slice(0, 2000) }),
duration,
},
})
errorMessage = resultStr
}

return result
} catch (err) {
const duration = Date.now() - t0
duration = Date.now() - t0
outcome = "error"
errorMessage = err.message
if (span) {
span.setAttribute("gen_ai.tool.call.outcome", "error")
span.setAttribute("error.type", err.constructor?.name || "Error")
Expand All @@ -119,31 +103,25 @@ export function _patchToolsProto(proto) {
setSpanAttrs(span, mlflowAttrs("TOOL", { outputs: { error: err.message } }))
}

throw err
} finally {
metrics.toolInvocations.add(1, {
"sap.tenantId": cds.context?.tenant || "anonymous",
"agent.service": config?.configurable?._service || cds.context?.["agent.service"],
tool: toolName,
outcome: "error",
outcome,
})
audit("ToolInvocation", {
data: {
taskId,
service: config?.configurable?._service || cds.context?.["agent.service"],
tool: toolName,
args,
outcome,
...(outcome === "error" ? { error: errorMessage } : {}),
duration,
},
})

// Audit: record failed tool invocation
const taskId = config?.configurable?._taskId || cds.context?.["agent.task.id"]
if (taskId) {
audit("ToolInvocation", {
data: {
taskId,
service: config?.configurable?._service || cds.context?.["agent.service"],
tool: toolName,
args,
outcome: "error",
error: err.message,
duration,
},
})
}

throw err
} finally {
if (span) span.end()
}
}
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"watch:hybrid": "DEBUG=agents cds bind --exec -- cds w tests/projects/bookshop",
"watch:with-claude": "cds w tests/projects/bookshop --profile with-claude",
"watch:deep-agent": "cds w tests/projects/deep-agent --profile hybrid",
"docs:audit": "node scripts/generate-audit-docs.js",
"docs:audit:check": "node scripts/generate-audit-docs.js --check",
"prettier": "npx -y prettier@3 --write .",
"prettier:check": "npx -y prettier@3 --check ."
},
Expand Down Expand Up @@ -63,6 +65,7 @@
"@cap-js/sqlite": "^3",
"@sap/cds-mtxs": "^4",
"@toon-format/toon": ">=2.3",
"acorn": "^8.18.0",
"deepagents": "^1.10.2"
},
"peerDependencies": {
Expand Down
Loading
Loading