Veto maintains a complete history of every tool call, validation decision, and rule match. Export this data for compliance audits, debugging, or analytics.
How History Tracking Works
Every tool call processed by Veto is automatically recorded:
const veto = await Veto . init ();
const tools = veto . wrap ([ transferFunds , getBalance ]);
// Agent calls tools...
await agent . invoke ({ messages: [ ... ] });
// All decisions are tracked
const stats = veto . getHistoryStats ();
console . log ( stats );
// {
// totalCalls: 5,
// allowedCalls: 4,
// deniedCalls: 1,
// modifiedCalls: 0,
// callsByTool: { transfer_funds: 3, get_balance: 2 }
// }
History is stored in-memory during the Veto instance lifetime. For persistent storage, export to JSON/CSV and save to your database or logging system.
History Record Structure
Each history entry includes:
When the tool call was made.
Name of the tool that was called.
Complete arguments passed to the tool (deep-cloned for immutability).
decision: "allow" | "deny" | "modify"
reason: Human-readable explanation (if available)
ruleId: ID of the matched rule
ruleName: Name of the matched rule
severity: Rule severity level
metadata: Additional rule metadata
Optional: execution time in milliseconds.
Exporting Decisions
JSON Export
import { Veto } from 'veto-sdk' ;
const veto = await Veto . init ();
const tools = veto . wrap ([ ... ]);
// Run your agent...
await agent . invoke ({ messages: [ ... ] });
// Export as JSON
const jsonAudit = veto . exportDecisions ( 'json' );
console . log ( jsonAudit );
Output format:
[
{
"timestamp" : "2026-03-04T14:23:45.123Z" ,
"tool_name" : "transfer_funds" ,
"arguments" : {
"amount" : 15000 ,
"from_account" : "ACC-001" ,
"to_account" : "ACC-999"
},
"policy_version" : "1.0" ,
"rule_id" : "block-large-transfers" ,
"decision" : "deny" ,
"reason" : "Transfer amount exceeds $10,000 threshold"
},
{
"timestamp" : "2026-03-04T14:24:12.456Z" ,
"tool_name" : "get_balance" ,
"arguments" : {
"account_id" : "ACC-001"
},
"policy_version" : "1.0" ,
"rule_id" : null ,
"decision" : "allow" ,
"reason" : null
}
]
CSV Export
const csvAudit = veto . exportDecisions ( 'csv' );
console . log ( csvAudit );
Output format:
timestamp, tool_name, arguments, policy_version, rule_id, decision, reason
2026-03-04T14:23:45.123Z, transfer_funds, "{""amount"":15000,""from_account"":""ACC-001"",""to_account"":""ACC-999""}", 1.0, block-large-transfers, deny, Transfer amount exceeds $10, 000 threshold
2026-03-04T14:24:12.456Z, get_balance, "{""account_id"":""ACC-001""}", 1.0, , allow,
CSV escaping follows RFC 4180. Complex JSON arguments are serialized and quoted.
Saving to File
import { writeFileSync } from 'node:fs' ;
const veto = await Veto . init ();
// ... run agent ...
// Save JSON
writeFileSync ( 'audit.json' , veto . exportDecisions ( 'json' ));
// Save CSV
writeFileSync ( 'audit.csv' , veto . exportDecisions ( 'csv' ));
from veto import Veto
veto = await Veto.init()
# ... run agent ...
# Save JSON
with open ( 'audit.json' , 'w' ) as f:
f.write(veto.export_decisions( 'json' ))
# Save CSV
with open ( 'audit.csv' , 'w' ) as f:
f.write(veto.export_decisions( 'csv' ))
Querying History
Veto provides methods to filter and query history:
Get All Entries
const allEntries = veto . getHistory ();
Get Statistics
const stats = veto . getHistoryStats ();
// {
// totalCalls: 10,
// allowedCalls: 7,
// deniedCalls: 3,
// modifiedCalls: 0,
// callsByTool: { transfer_funds: 5, get_balance: 3, deploy: 2 }
// }
Clear History
Clearing history removes all in-memory records. Export before clearing if you need the data.
Integration with Logging Systems
Stream to Datadog
import { Veto } from 'veto-sdk' ;
import axios from 'axios' ;
const veto = await Veto . init ();
// Wrap tools with logging
const tools = veto . wrap ([ transferFunds ]);
// Run agent
await agent . invoke ({ messages: [ ... ] });
// Export and send to Datadog
const decisions = JSON . parse ( veto . exportDecisions ( 'json' ));
for ( const decision of decisions ) {
await axios . post (
`https://http-intake.logs.datadoghq.com/api/v2/logs` ,
{
ddsource: 'veto' ,
ddtags: `tool: ${ decision . tool_name } ,decision: ${ decision . decision } ` ,
message: decision . reason ?? 'No reason provided' ,
... decision ,
},
{
headers: {
'DD-API-KEY' : process . env . DATADOG_API_KEY ,
},
}
);
}
Stream to Elasticsearch
import { Veto } from 'veto-sdk' ;
import { Client } from '@elastic/elasticsearch' ;
const veto = await Veto . init ();
const elastic = new Client ({ node: 'http://localhost:9200' });
// After agent execution
const decisions = JSON . parse ( veto . exportDecisions ( 'json' ));
const bulkBody = decisions . flatMap (( doc ) => [
{ index: { _index: 'veto-audit' } },
doc ,
]);
await elastic . bulk ({ body: bulkBody });
Stream to PostgreSQL
import { Veto } from 'veto-sdk' ;
import { Pool } from 'pg' ;
const veto = await Veto . init ();
const pool = new Pool ({ connectionString: process . env . DATABASE_URL });
// Create table (once)
await pool . query ( `
CREATE TABLE IF NOT EXISTS veto_audit (
id SERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
tool_name TEXT NOT NULL,
arguments JSONB NOT NULL,
policy_version TEXT,
rule_id TEXT,
decision TEXT NOT NULL,
reason TEXT
)
` );
// Export and insert
const decisions = JSON . parse ( veto . exportDecisions ( 'json' ));
for ( const decision of decisions ) {
await pool . query (
`INSERT INTO veto_audit (timestamp, tool_name, arguments, policy_version, rule_id, decision, reason)
VALUES ($1, $2, $3, $4, $5, $6, $7)` ,
[
decision . timestamp ,
decision . tool_name ,
decision . arguments ,
decision . policy_version ,
decision . rule_id ,
decision . decision ,
decision . reason ,
]
);
}
Real-Time Event Hooks
For real-time monitoring, use Veto’s event system:
import { Veto } from 'veto-sdk' ;
const veto = await Veto . init ();
veto . on ( 'decision' , ( event ) => {
console . log ( 'Decision made:' , {
tool: event . toolName ,
decision: event . decision ,
ruleId: event . ruleId ,
timestamp: event . timestamp ,
});
// Send to your monitoring system
if ( event . decision === 'deny' ) {
sendSlackAlert ( `🚨 Blocked: ${ event . toolName } ` );
}
});
const tools = veto . wrap ([ ... ]);
Event hooks fire before the decision is recorded in history. Use them for real-time alerts.
Compliance Reporting
Generate Daily Reports
import { Veto } from 'veto-sdk' ;
import { writeFileSync } from 'node:fs' ;
const veto = await Veto . init ();
// Run agent throughout the day...
// End-of-day export
const report = JSON . parse ( veto . exportDecisions ( 'json' ));
const timestamp = new Date (). toISOString (). split ( 'T' )[ 0 ];
writeFileSync ( `reports/veto-audit- ${ timestamp } .json` , JSON . stringify ( report , null , 2 ));
console . log ( `Audit report saved: veto-audit- ${ timestamp } .json` );
Filter by Decision Type
const decisions = JSON . parse ( veto . exportDecisions ( 'json' ));
const blocked = decisions . filter (( d ) => d . decision === 'deny' );
const approved = decisions . filter (( d ) => d . decision === 'allow' );
console . log ( `Total calls: ${ decisions . length } ` );
console . log ( `Blocked: ${ blocked . length } ` );
console . log ( `Approved: ${ approved . length } ` );
const decisions = JSON . parse ( veto . exportDecisions ( 'json' ));
const byTool = new Map ();
for ( const decision of decisions ) {
if ( ! byTool . has ( decision . tool_name )) {
byTool . set ( decision . tool_name , { total: 0 , allowed: 0 , denied: 0 });
}
const stats = byTool . get ( decision . tool_name );
stats . total ++ ;
if ( decision . decision === 'allow' ) stats . allowed ++ ;
if ( decision . decision === 'deny' ) stats . denied ++ ;
}
console . table ([ ... byTool . entries ()]. map (([ tool , stats ]) => ({ tool , ... stats })));
History Configuration
Configure history limits in veto.config.yaml:
version : "1.0"
history :
maxSize : 1000 # Maximum number of entries to keep in memory
Default: 1000 entries. Oldest entries are evicted when the limit is reached.
For high-throughput systems, export history periodically and clear it to avoid memory growth.
Example: Complete Audit Pipeline
Initialize Veto with history tracking
import { Veto } from 'veto-sdk' ;
const veto = await Veto . init ();
Wrap tools and run agent
const tools = veto . wrap ([ transferFunds , getBalance ]);
const agent = createAgent ({ model: 'gpt-4o' , tools });
await agent . invoke ({
messages: [{ role: 'user' , content: 'Transfer $50,000 to ACC-999' }],
});
Export decisions after execution
const jsonAudit = veto . exportDecisions ( 'json' );
const csvAudit = veto . exportDecisions ( 'csv' );
Save to disk and database
import { writeFileSync } from 'node:fs' ;
import { pool } from './db' ;
// Save to file
writeFileSync ( 'audit.json' , jsonAudit );
writeFileSync ( 'audit.csv' , csvAudit );
// Save to database
const decisions = JSON . parse ( jsonAudit );
for ( const decision of decisions ) {
await pool . query (
'INSERT INTO veto_audit (timestamp, tool_name, arguments, rule_id, decision, reason) VALUES ($1, $2, $3, $4, $5, $6)' ,
[
decision . timestamp ,
decision . tool_name ,
decision . arguments ,
decision . rule_id ,
decision . decision ,
decision . reason ,
]
);
}
Clear history for next session
Debugging with History
Use history exports to debug policy issues:
# Export decisions after a test run
node agent.js
# Inspect decisions
cat audit.json | jq '.[] | select(.decision == "deny")'
Example output:
{
"timestamp" : "2026-03-04T14:23:45.123Z" ,
"tool_name" : "transfer_funds" ,
"arguments" : {
"amount" : 15000 ,
"from_account" : "ACC-001" ,
"to_account" : "ACC-999"
},
"rule_id" : "block-large-transfers" ,
"decision" : "deny" ,
"reason" : "Transfer amount exceeds $10,000 threshold"
}
Use this data to:
Verify rules matched correctly
Identify unexpected denials
Tune rule conditions
Test rule changes with veto diff --log audit.json
Next Steps
Testing Policies Use audit logs to validate rule behavior
CI/CD Integration Export audit logs in CI for compliance checks
Writing Rules Learn to write rules that generate clean audit logs
Approval Workflows Track approval decisions in audit logs