agentledger-runtime 1.0.2
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/README.md +119 -0
- package/examples/README.md +22 -0
- package/examples/quickstart/quickstart.js +7 -0
- package/examples/travel_assistant/travel_assistant.js +396 -0
- package/package.json +21 -0
- package/src/cli.js +733 -0
- package/src/index.d.ts +235 -0
- package/src/index.js +1683 -0
- package/test/runtime.test.js +272 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# AgentLedger Node / TypeScript Runtime
|
|
2
|
+
|
|
3
|
+
This directory contains the dependency-free Node/TypeScript-compatible runtime-core baseline for AgentLedger 1.0.2.
|
|
4
|
+
|
|
5
|
+
It runs a native local runtime loop, participates in the shared Python/Go/TypeScript/Rust conformance gate, and should be treated as runtime-core aligned; concrete production adapters are shipped separately as they mature.
|
|
6
|
+
|
|
7
|
+
## Current Status
|
|
8
|
+
|
|
9
|
+
Implemented:
|
|
10
|
+
|
|
11
|
+
- local JSON store with atomic file writes and in-memory store for tests
|
|
12
|
+
- run/session/step state model
|
|
13
|
+
- leased step claim, heartbeat, lease recovery, and cancellation fencing
|
|
14
|
+
- `AgentContext` with state patch writes and runtime-managed tool calls
|
|
15
|
+
- `ToolGateway` with Tool Ledger idempotency for side-effect tools
|
|
16
|
+
- evidence export, replay, trace/diff/debug consumers, time-travel timeline, repro helpers
|
|
17
|
+
- policy denial, approval pause/resume, sandbox fail-closed behavior
|
|
18
|
+
- cost records, budget enforcement, and failure attribution
|
|
19
|
+
- media artifact refs and stream checkpoint refs in evidence/replay
|
|
20
|
+
- scheduler facade, worker service, failure injection, adversarial review, evidence regression
|
|
21
|
+
- MCP-style and dependency-free framework adapters
|
|
22
|
+
- `.d.ts` declarations
|
|
23
|
+
- official optional adapter APIs for Postgres, S3/MinIO, OTLP transport, and Docker sandbox manifests
|
|
24
|
+
- CLI for `conformance`, `contract validate`, and `contract export`
|
|
25
|
+
|
|
26
|
+
Not claimed yet:
|
|
27
|
+
|
|
28
|
+
- live Postgres/S3/Docker/OTLP service-backed hardening
|
|
29
|
+
- full framework-native packages
|
|
30
|
+
- full media processing and stream transport adapters
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
Use the published npm package in a Node.js project:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install agentledger-runtime
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Import the runtime API:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import { Runtime, simpleRun } from 'agentledger-runtime';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Install or run the CLI through npm package binaries after installation:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx agentledger-ts --help
|
|
51
|
+
npx agentledger-ts quickstart
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
From this repository, use `node src/cli.js ...` while developing.
|
|
55
|
+
|
|
56
|
+
## Quickstart
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
import { Runtime, exportEvidence } from 'agentledger-runtime';
|
|
60
|
+
|
|
61
|
+
const rt = await Runtime.local('.agentledger-ts/state.json');
|
|
62
|
+
rt.registerTool({
|
|
63
|
+
name: 'docs.echo',
|
|
64
|
+
func: async (args) => ({ echo: args.text }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { runId } = await rt.createRun({ input: 'hello' });
|
|
68
|
+
await rt.runOnce({
|
|
69
|
+
runId,
|
|
70
|
+
workerId: 'worker-ts',
|
|
71
|
+
agentRole: 'Agent',
|
|
72
|
+
agent: async (ctx, state) => {
|
|
73
|
+
const result = await ctx.callTool('docs.echo', { text: state.input });
|
|
74
|
+
await ctx.writeState('result', result);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(exportEvidence(rt.store, runId).run.run_id);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Adapter Quickstart
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
import { PostgresAdapter, S3BlobStoreAdapter, DockerSandboxAdapter } from 'agentledger-runtime';
|
|
85
|
+
|
|
86
|
+
await new PostgresAdapter(injectedSqlClient).applyMigrations();
|
|
87
|
+
|
|
88
|
+
const s3 = new S3BlobStoreAdapter(injectedObjectClient, { bucket: 'agentledger-test' });
|
|
89
|
+
const { ref } = await s3.putJSON({ answer: 'ok' });
|
|
90
|
+
console.log(ref);
|
|
91
|
+
|
|
92
|
+
const manifest = new DockerSandboxAdapter().manifest({ network: 'deny' }, ['echo', 'ok']);
|
|
93
|
+
console.log(manifest);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Verify
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cd typescript
|
|
100
|
+
npm test
|
|
101
|
+
npm run check
|
|
102
|
+
npm run conformance
|
|
103
|
+
npm run contract:validate
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
From the repository root:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python3.11 scripts/check_language_parity.py --json-report /tmp/agentledger-language-parity.json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Compatibility Target
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
../contracts/agentledger.runtime.v1.json
|
|
116
|
+
../contracts/conformance/runtime_semantics.v1.json
|
|
117
|
+
../docs/LANGUAGE_QUICKSTART.md
|
|
118
|
+
../docs/LANGUAGE_PARITY_MATRIX.md
|
|
119
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# AgentLedger TypeScript Examples
|
|
2
|
+
|
|
3
|
+
Run from `typescript/` unless noted.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd typescript
|
|
9
|
+
node examples/quickstart/quickstart.js
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Source: `quickstart/quickstart.js`
|
|
13
|
+
|
|
14
|
+
## CLI Quickstart
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd typescript
|
|
18
|
+
node src/cli.js quickstart
|
|
19
|
+
node src/cli.js conformance
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Package surface: `agentledger-runtime`. See `../README.md` for package metadata and API examples.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { simpleRun } from '../../src/index.js';
|
|
2
|
+
|
|
3
|
+
const result = await simpleRun(async (_ctx, state) => ({ message: 'hello from typescript', input: state.input }), {
|
|
4
|
+
initialState: { input: 'world' },
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
console.log(JSON.stringify({ run_id: result.run_id, output: result.output, state: result.state }, null, 2));
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Travel Assistant Demo — AgentLedger TypeScript Interactive Demo
|
|
4
|
+
* ================================================================
|
|
5
|
+
* 旅游助手交互式演示 — 每一步展示数据库里的实际变化
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node typescript/examples/travel_assistant/travel_assistant.js
|
|
9
|
+
* node typescript/examples/travel_assistant/travel_assistant.js .agentledger-ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createInterface } from 'node:readline';
|
|
13
|
+
import {
|
|
14
|
+
Runtime, JSONStore, LocalBlobStore, PolicyEngine, BudgetController,
|
|
15
|
+
BudgetLimits, RetryableAgentError,
|
|
16
|
+
exportEvidence, replay, costAttribution,
|
|
17
|
+
} from '../../src/index.js';
|
|
18
|
+
|
|
19
|
+
// ════════════════════════════════════════════════════════════
|
|
20
|
+
// ANSI Colors
|
|
21
|
+
// ════════════════════════════════════════════════════════════
|
|
22
|
+
const C = {
|
|
23
|
+
R: '\x1b[91m', G: '\x1b[92m', Y: '\x1b[93m', B: '\x1b[94m',
|
|
24
|
+
M: '\x1b[95m', C: '\x1b[96m', BOLD: '\x1b[1m', DIM: '\x1b[2m', RST: '\x1b[0m',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ════════════════════════════════════════════════════════════
|
|
28
|
+
// Mock data
|
|
29
|
+
// ════════════════════════════════════════════════════════════
|
|
30
|
+
const MOCK_FLIGHTS = [
|
|
31
|
+
{ id: 'FL-002', from_city: 'Beijing', from_code: 'PEK', to_city: 'Tokyo', to_code: 'NRT', date: '2025-06-15', airline: 'JAL', price_usd: 580 },
|
|
32
|
+
];
|
|
33
|
+
const MOCK_HOTELS = [
|
|
34
|
+
{ id: 'HT-002', city: 'Tokyo', name: 'APA Hotel Shinjuku', nightly_usd: 85, stars: 3 },
|
|
35
|
+
];
|
|
36
|
+
const MOCK_WEATHER = { Tokyo: { temp_c: 24, condition: 'Partly Cloudy', humidity: 65 } };
|
|
37
|
+
const bookingDB = {};
|
|
38
|
+
|
|
39
|
+
// ════════════════════════════════════════════════════════════
|
|
40
|
+
// Tool implementations
|
|
41
|
+
// ════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
function searchFlights(args) {
|
|
44
|
+
const origin = (args.from || '').trim().toLowerCase();
|
|
45
|
+
const dest = (args.to || '').trim().toLowerCase();
|
|
46
|
+
const results = MOCK_FLIGHTS.filter(f =>
|
|
47
|
+
(f.from_city.toLowerCase().includes(origin) || f.from_code.toLowerCase().includes(origin)) &&
|
|
48
|
+
(f.to_city.toLowerCase().includes(dest) || f.to_code.toLowerCase().includes(dest))
|
|
49
|
+
);
|
|
50
|
+
return { results, count: results.length };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function searchHotels(args) {
|
|
54
|
+
const city = (args.city || '').trim().toLowerCase();
|
|
55
|
+
const results = MOCK_HOTELS.filter(h => h.city.toLowerCase() === city);
|
|
56
|
+
return { results, count: results.length };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function checkWeather(args) {
|
|
60
|
+
const city = (args.city || '').trim();
|
|
61
|
+
return { city, ...(MOCK_WEATHER[city] || { temp_c: 20, condition: 'Unknown' }) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function bookFlight(args) {
|
|
65
|
+
const ref = `BK-F-${args.flight_id}-${(args.passenger || '').slice(0, 3).toUpperCase()}`;
|
|
66
|
+
if (bookingDB[ref]) return bookingDB[ref];
|
|
67
|
+
const f = MOCK_FLIGHTS.find(f => f.id === args.flight_id);
|
|
68
|
+
if (!f) throw new Error(`Flight not found: ${args.flight_id}`);
|
|
69
|
+
const booking = { booking_ref: ref, type: 'flight', airline: f.airline, price_usd: f.price_usd, status: 'confirmed', external_id: ref };
|
|
70
|
+
bookingDB[ref] = booking;
|
|
71
|
+
return booking;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function bookHotel(args) {
|
|
75
|
+
const ref = `BK-H-${args.hotel_id}-${(args.guest || '').slice(0, 3).toUpperCase()}`;
|
|
76
|
+
if (bookingDB[ref]) return bookingDB[ref];
|
|
77
|
+
const h = MOCK_HOTELS.find(h => h.id === args.hotel_id);
|
|
78
|
+
if (!h) throw new Error(`Hotel not found: ${args.hotel_id}`);
|
|
79
|
+
const booking = { booking_ref: ref, type: 'hotel', name: h.name, price_total_usd: h.nightly_usd * 5, status: 'confirmed', external_id: ref };
|
|
80
|
+
bookingDB[ref] = booking;
|
|
81
|
+
return booking;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ════════════════════════════════════════════════════════════
|
|
85
|
+
// Agent function
|
|
86
|
+
// ════════════════════════════════════════════════════════════
|
|
87
|
+
|
|
88
|
+
async function travelPlanner(ctx, state) {
|
|
89
|
+
// Phase 1: Research
|
|
90
|
+
const flights = await ctx.callTool('travel.search_flights', { from: 'Beijing', to: 'Tokyo' });
|
|
91
|
+
const hotels = await ctx.callTool('travel.search_hotels', { city: 'Tokyo' });
|
|
92
|
+
const weather = await ctx.callTool('travel.check_weather', { city: 'Tokyo' });
|
|
93
|
+
await ctx.writeState('research', { flights: flights.count, hotels: hotels.count, weather: weather.temp_c });
|
|
94
|
+
|
|
95
|
+
// Phase 2: Book flight (approval required)
|
|
96
|
+
const flight = await ctx.callTool('travel.book_flight', {
|
|
97
|
+
flight_id: 'FL-002', passenger: 'Demo User',
|
|
98
|
+
_logical_operation: 'book-demo-flight',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Phase 3: Simulated crash on attempt 2
|
|
102
|
+
if (ctx.attempt === 2) {
|
|
103
|
+
throw new RetryableAgentError('after flight booking');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Phase 4: Book hotel (approval required)
|
|
107
|
+
const hotel = await ctx.callTool('travel.book_hotel', {
|
|
108
|
+
hotel_id: 'HT-002', check_in: '2025-06-15', check_out: '2025-06-20',
|
|
109
|
+
guest: 'Demo User', _logical_operation: 'book-demo-hotel',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await ctx.writeState('bookings', { flight: flight.booking_ref, hotel: hotel.booking_ref });
|
|
113
|
+
await ctx.writeState('trip_status', 'confirmed');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ════════════════════════════════════════════════════════════
|
|
117
|
+
// Display helpers
|
|
118
|
+
// ════════════════════════════════════════════════════════════
|
|
119
|
+
|
|
120
|
+
function wait(msg = 'Press Enter to continue') {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
process.stdout.write(`\n${C.DIM} ⏎ ${msg}...${C.RST}`);
|
|
123
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
124
|
+
rl.once('line', () => { rl.close(); resolve(); });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function showRows(label, headers, rows, color = C.C) {
|
|
129
|
+
if (!rows || rows.length === 0) {
|
|
130
|
+
console.log(`\n ${color}${label}:${C.RST} ${C.DIM}(empty)${C.RST}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
console.log(`\n ${color}${label} (${rows.length} rows):${C.RST}`);
|
|
134
|
+
for (const row of rows) {
|
|
135
|
+
const items = headers.map((h, i) => `${h}=${C.BOLD}${row[i]}${C.RST}`);
|
|
136
|
+
console.log(` ${C.DIM}${items.join(' | ')}${C.RST}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function showDB(store, runID) {
|
|
141
|
+
// Runs
|
|
142
|
+
const runData = store._data?.runs?.[runID]; // eslint-disable-line no-underscore-dangle
|
|
143
|
+
const runRows = [];
|
|
144
|
+
if (runData) {
|
|
145
|
+
const shortID = runData.run_id.length > 24 ? runData.run_id.slice(0, 24) + '...' : runData.run_id;
|
|
146
|
+
runRows.push([shortID, runData.status, String(runData.state_version)]);
|
|
147
|
+
}
|
|
148
|
+
showRows('Runs', ['run_id', 'status', 'state_version'], runRows, C.B);
|
|
149
|
+
|
|
150
|
+
// Steps
|
|
151
|
+
const steps = store.steps(runID);
|
|
152
|
+
const stepRows = steps.map(s => {
|
|
153
|
+
const shortID = s.step_id.length > 24 ? s.step_id.slice(0, 24) + '...' : s.step_id;
|
|
154
|
+
return [shortID, s.status, String(s.attempt)];
|
|
155
|
+
});
|
|
156
|
+
showRows('Steps', ['step_id', 'status', 'attempt'], stepRows, C.B);
|
|
157
|
+
|
|
158
|
+
// Tool Ledger
|
|
159
|
+
const ledger = store.ledger(runID);
|
|
160
|
+
const ledgerRows = ledger.map(tl => {
|
|
161
|
+
const key = tl.idempotency_key;
|
|
162
|
+
const parts = key.split(':');
|
|
163
|
+
const shortKey = parts.length >= 2 ? parts[parts.length - 2] + ':' + parts[parts.length - 1].slice(0, 10) : key.slice(0, 25);
|
|
164
|
+
return [tl.tool_name, tl.status, shortKey];
|
|
165
|
+
});
|
|
166
|
+
showRows('Tool Ledger', ['tool', 'status', 'idemp_key'], ledgerRows, C.Y);
|
|
167
|
+
|
|
168
|
+
// Approval requests
|
|
169
|
+
const approvals = store.approvalRequests(runID);
|
|
170
|
+
const approvalRows = approvals.map(a => [a.tool_name, a.status, a.approved_by || '-']);
|
|
171
|
+
showRows('Approval Requests', ['tool', 'status', 'approved_by'], approvalRows, C.R);
|
|
172
|
+
|
|
173
|
+
console.log();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ════════════════════════════════════════════════════════════
|
|
177
|
+
// Main
|
|
178
|
+
// ════════════════════════════════════════════════════════════
|
|
179
|
+
|
|
180
|
+
async function main() {
|
|
181
|
+
const root = process.argv[2] || '.agentledger-ts';
|
|
182
|
+
|
|
183
|
+
// Intro
|
|
184
|
+
console.log(`\n${C.BOLD}${C.C} ╔════════════════════════════════════════════════════╗${C.RST}`);
|
|
185
|
+
console.log(`${C.BOLD}${C.C} ║ AgentLedger Travel Assistant (TS) — Interactive Demo ║${C.RST}`);
|
|
186
|
+
console.log(`${C.BOLD}${C.C} ║ See real database state at every step ║${C.RST}`);
|
|
187
|
+
console.log(`${C.BOLD}${C.C} ╚════════════════════════════════════════════════════╝${C.RST}`);
|
|
188
|
+
|
|
189
|
+
console.log(`\n ${C.DIM}AgentLedger — Durable Execution Runtime for AI Agents${C.RST}`);
|
|
190
|
+
console.log(` ┌────────────────────────────────────────────────────┐`);
|
|
191
|
+
console.log(` │ ${C.G}✓${C.RST} Durable execution — crash recovery │`);
|
|
192
|
+
console.log(` │ ${C.G}✓${C.RST} Tool Ledger — idempotent replay │`);
|
|
193
|
+
console.log(` │ ${C.G}✓${C.RST} Approval gates — human-in-the-loop │`);
|
|
194
|
+
console.log(` │ ${C.G}✓${C.RST} Policy engine — role-based access │`);
|
|
195
|
+
console.log(` │ ${C.G}✓${C.RST} Budget control — tool call limits │`);
|
|
196
|
+
console.log(` │ ${C.G}✓${C.RST} Evidence export — full audit trail │`);
|
|
197
|
+
console.log(` └────────────────────────────────────────────────────┘`);
|
|
198
|
+
|
|
199
|
+
await wait('Press Enter to start / 按 Enter 开始');
|
|
200
|
+
|
|
201
|
+
// ════════════════════════════════════════════════════════
|
|
202
|
+
// Step 1: Setup
|
|
203
|
+
// ════════════════════════════════════════════════════════
|
|
204
|
+
console.log(`\n${C.BOLD}${C.B}${'═'.repeat(60)}${C.RST}`);
|
|
205
|
+
console.log(`${C.BOLD}${C.B} Step 1: Initialize — Register tools, configure policy${C.RST}`);
|
|
206
|
+
console.log(`${C.BOLD}${C.B}${'═'.repeat(60)}${C.RST}`);
|
|
207
|
+
|
|
208
|
+
const store = await JSONStore.open(`${root}/state.json`);
|
|
209
|
+
const rt = new Runtime(store);
|
|
210
|
+
rt.setBudget({ maxToolCalls: 25 });
|
|
211
|
+
|
|
212
|
+
for (const t of ['travel.search_flights', 'travel.search_hotels', 'travel.check_weather',
|
|
213
|
+
'travel.book_flight', 'travel.book_hotel']) {
|
|
214
|
+
rt.policy.allowTool('TravelPlanner', t);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
rt.registerTool({
|
|
218
|
+
name: 'travel.search_flights', version: 'v1', sideEffect: 'none', riskLevel: 'low',
|
|
219
|
+
inputSchema: { type: 'object', required: ['from', 'to'] },
|
|
220
|
+
outputSchema: { type: 'object' },
|
|
221
|
+
func: searchFlights,
|
|
222
|
+
});
|
|
223
|
+
rt.registerTool({
|
|
224
|
+
name: 'travel.search_hotels', version: 'v1', sideEffect: 'none', riskLevel: 'low',
|
|
225
|
+
inputSchema: { type: 'object', required: ['city'] },
|
|
226
|
+
outputSchema: { type: 'object' },
|
|
227
|
+
func: searchHotels,
|
|
228
|
+
});
|
|
229
|
+
rt.registerTool({
|
|
230
|
+
name: 'travel.check_weather', version: 'v1', sideEffect: 'none', riskLevel: 'low',
|
|
231
|
+
inputSchema: { type: 'object', required: ['city'] },
|
|
232
|
+
outputSchema: { type: 'object' },
|
|
233
|
+
func: checkWeather,
|
|
234
|
+
});
|
|
235
|
+
rt.registerTool({
|
|
236
|
+
name: 'travel.book_flight', version: 'v1', sideEffect: 'external_write', riskLevel: 'high',
|
|
237
|
+
idempotencyRequired: true, approvalRequired: true,
|
|
238
|
+
inputSchema: { type: 'object', required: ['flight_id', 'passenger'] },
|
|
239
|
+
outputSchema: { type: 'object' },
|
|
240
|
+
func: bookFlight,
|
|
241
|
+
});
|
|
242
|
+
rt.registerTool({
|
|
243
|
+
name: 'travel.book_hotel', version: 'v1', sideEffect: 'external_write', riskLevel: 'high',
|
|
244
|
+
idempotencyRequired: true, approvalRequired: true,
|
|
245
|
+
inputSchema: { type: 'object', required: ['hotel_id', 'guest'] },
|
|
246
|
+
outputSchema: { type: 'object' },
|
|
247
|
+
func: bookHotel,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const { runId } = await rt.createRun({ trip: 'Tokyo', budget_usd: 3000 });
|
|
251
|
+
console.log(`\n ${C.B}Run created: ${C.BOLD}${runId}${C.RST}`);
|
|
252
|
+
showDB(store, runId);
|
|
253
|
+
await wait('Press Enter to continue');
|
|
254
|
+
|
|
255
|
+
// ════════════════════════════════════════════════════════
|
|
256
|
+
// Step 2: Attempt 1 — Approval interception
|
|
257
|
+
// ════════════════════════════════════════════════════════
|
|
258
|
+
console.log(`\n${C.BOLD}${C.R}${'═'.repeat(60)}${C.RST}`);
|
|
259
|
+
console.log(`${C.BOLD}${C.R} Step 2: Attempt 1 — Agent runs → Approval triggered${C.RST}`);
|
|
260
|
+
console.log(`${C.BOLD}${C.R}${'═'.repeat(60)}${C.RST}`);
|
|
261
|
+
console.log(`\n ${C.DIM}Agent executing: search flights → search hotels → check weather → book flight...${C.RST}`);
|
|
262
|
+
|
|
263
|
+
await rt.runOnce({ runId, workerId: 'worker-node', agentRole: 'TravelPlanner', agent: travelPlanner });
|
|
264
|
+
|
|
265
|
+
console.log(`\n ${C.R}book_flight triggered approval! Runtime paused, waiting for human.${C.RST}`);
|
|
266
|
+
showDB(store, runId);
|
|
267
|
+
console.log(` ${C.R}Note: Tool Ledger has RESERVED entry, approval status is PENDING${C.RST}`);
|
|
268
|
+
await wait('Press Enter to approve / 按 Enter 审批');
|
|
269
|
+
|
|
270
|
+
for (const req of store.approvalRequests(runId)) {
|
|
271
|
+
if (req.status === 'PENDING') {
|
|
272
|
+
await store.approveRequest(req.approval_id, { approver: 'traveler', reason: 'Within budget, approved' });
|
|
273
|
+
console.log(`\n ${C.G}✅ Approved: ${req.tool_name} — by traveler${C.RST}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
showDB(store, runId);
|
|
277
|
+
await wait('Press Enter to continue');
|
|
278
|
+
|
|
279
|
+
// ════════════════════════════════════════════════════════
|
|
280
|
+
// Step 3: Attempt 2 — Execute + Crash
|
|
281
|
+
// ════════════════════════════════════════════════════════
|
|
282
|
+
console.log(`\n${C.BOLD}${C.Y}${'═'.repeat(60)}${C.RST}`);
|
|
283
|
+
console.log(`${C.BOLD}${C.Y} Step 3: Attempt 2 — Approved → Execute booking → Simulated crash${C.RST}`);
|
|
284
|
+
console.log(`${C.BOLD}${C.Y}${'═'.repeat(60)}${C.RST}`);
|
|
285
|
+
console.log(`\n ${C.DIM}Re-running agent (approval passed, book_flight will execute)...${C.RST}`);
|
|
286
|
+
|
|
287
|
+
await rt.runOnce({ runId, workerId: 'worker-node', agentRole: 'TravelPlanner', agent: travelPlanner });
|
|
288
|
+
|
|
289
|
+
console.log(`\n ${C.Y}Agent booked flight, then crashed before committing state!${C.RST}`);
|
|
290
|
+
console.log(` ${C.Y}Flight is booked in external system, but agent state was NOT persisted.${C.RST}`);
|
|
291
|
+
showDB(store, runId);
|
|
292
|
+
console.log(` ${C.Y}Key: Tool Ledger book_flight status = ${C.G}SUCCEEDED${C.Y} (external side effect executed)${C.RST}`);
|
|
293
|
+
console.log(` ${C.Y} Step status = retry_scheduled (state not committed, waiting for retry)${C.RST}`);
|
|
294
|
+
await wait('Press Enter to continue');
|
|
295
|
+
|
|
296
|
+
// ════════════════════════════════════════════════════════
|
|
297
|
+
// Step 4: Attempt 3 — Recovery + Hotel approval
|
|
298
|
+
// ════════════════════════════════════════════════════════
|
|
299
|
+
console.log(`\n${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
|
|
300
|
+
console.log(`${C.BOLD}${C.G} Step 4: Attempt 3 — Crash recovery → Tool Ledger idempotent replay${C.RST}`);
|
|
301
|
+
console.log(`${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
|
|
302
|
+
console.log(`\n ${C.DIM}Agent re-executes. book_flight: Tool Ledger sees SUCCEEDED record...${C.RST}`);
|
|
303
|
+
console.log(` ${C.DIM}${C.G}→ Returns cached result, no duplicate API call, no double charge!${C.RST}`);
|
|
304
|
+
|
|
305
|
+
await rt.runOnce({ runId, workerId: 'worker-node', agentRole: 'TravelPlanner', agent: travelPlanner });
|
|
306
|
+
|
|
307
|
+
console.log(`\n ${C.G}✅ Flight idempotent replay successful! (no duplicate _book_flight call)${C.RST}`);
|
|
308
|
+
console.log(` ${C.R}Hotel booking → triggers approval again${C.RST}`);
|
|
309
|
+
|
|
310
|
+
for (const req of store.approvalRequests(runId)) {
|
|
311
|
+
if (req.status === 'PENDING') {
|
|
312
|
+
await store.approveRequest(req.approval_id, { approver: 'traveler', reason: 'Hotel within budget, approved' });
|
|
313
|
+
console.log(`\n ${C.G}✅ Approved: ${req.tool_name} — by traveler${C.RST}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
showDB(store, runId);
|
|
317
|
+
await wait('Press Enter to continue');
|
|
318
|
+
|
|
319
|
+
// ════════════════════════════════════════════════════════
|
|
320
|
+
// Step 5: Attempt 4 — Complete
|
|
321
|
+
// ════════════════════════════════════════════════════════
|
|
322
|
+
console.log(`\n${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
|
|
323
|
+
console.log(`${C.BOLD}${C.G} Step 5: Attempt 4 — Hotel approved → Full execution → State committed${C.RST}`);
|
|
324
|
+
console.log(`${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
|
|
325
|
+
|
|
326
|
+
const ok = await rt.runOnce({ runId, workerId: 'worker-node', agentRole: 'TravelPlanner', agent: travelPlanner });
|
|
327
|
+
if (!ok) {
|
|
328
|
+
console.error('Recovery failed');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
if (Object.keys(bookingDB).length !== 2) {
|
|
332
|
+
console.error(`Expected 2 bookings, got ${Object.keys(bookingDB).length}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
console.log(`\n ${C.G}✅ Travel planning complete! State persisted to database.${C.RST}`);
|
|
337
|
+
showDB(store, runId);
|
|
338
|
+
console.log(` ${C.G}Step status = completed, State has bookings + trip_status${C.RST}`);
|
|
339
|
+
console.log(` ${C.G}External bookings: [${Object.keys(bookingDB).join(', ')}] (2 total, no duplicates)${C.RST}`);
|
|
340
|
+
await wait('Press Enter to continue');
|
|
341
|
+
|
|
342
|
+
// ════════════════════════════════════════════════════════
|
|
343
|
+
// Step 6: Evidence + Cost + Replay
|
|
344
|
+
// ════════════════════════════════════════════════════════
|
|
345
|
+
console.log(`\n${C.BOLD}${C.M}${'═'.repeat(60)}${C.RST}`);
|
|
346
|
+
console.log(`${C.BOLD}${C.M} Step 6: Evidence export + Cost attribution + Replay verification${C.RST}`);
|
|
347
|
+
console.log(`${C.BOLD}${C.M}${'═'.repeat(60)}${C.RST}`);
|
|
348
|
+
|
|
349
|
+
const bundle = exportEvidence(store, runId);
|
|
350
|
+
const replayResult = replay(store, runId);
|
|
351
|
+
const cost = costAttribution(store, runId);
|
|
352
|
+
|
|
353
|
+
console.log(`\n ${C.M}Cost attribution: ${cost.total?.tool_calls ?? 0} tool calls${C.RST}`);
|
|
354
|
+
console.log(` ${C.M}Replay: ${replayResult.event_count} events, safe=${C.G}${replayResult.replay_safe}${C.RST}`);
|
|
355
|
+
console.log(` ${C.M}Evidence bundle: ${bundle.events.length} events total${C.RST}`);
|
|
356
|
+
|
|
357
|
+
// Final summary
|
|
358
|
+
console.log(`\n${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
|
|
359
|
+
console.log(`${C.BOLD}${C.G} Summary: What AgentLedger (TypeScript) did in this demo${C.RST}`);
|
|
360
|
+
console.log(`${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
|
|
361
|
+
console.log(`
|
|
362
|
+
┌──────────────────────────────────────────────────────────┐
|
|
363
|
+
│ │
|
|
364
|
+
│ ${C.G}✓ Durable execution${C.RST} Crash → auto retry, state preserved │
|
|
365
|
+
│ Step: retry_scheduled → completed │
|
|
366
|
+
│ │
|
|
367
|
+
│ ${C.G}✓ Tool Ledger${C.RST} Idempotent replay, flight booked ${C.BOLD}1x${C.RST} only │
|
|
368
|
+
│ SUCCEEDED → cached result on retry │
|
|
369
|
+
│ │
|
|
370
|
+
│ ${C.G}✓ Approval gates${C.RST} Flight + hotel each trigger approval │
|
|
371
|
+
│ approval_requests records in store │
|
|
372
|
+
│ │
|
|
373
|
+
│ ${C.G}✓ Policy engine${C.RST} Each tool call checked by policy │
|
|
374
|
+
│ TravelPlanner role allowed │
|
|
375
|
+
│ │
|
|
376
|
+
│ ${C.G}✓ Budget control${C.RST} Tracked ${cost.total?.tool_calls ?? 0} tool calls │
|
|
377
|
+
│ BudgetController.beforeToolCall() │
|
|
378
|
+
│ │
|
|
379
|
+
│ ${C.G}✓ Evidence export${C.RST} ${bundle.events.length} events recorded │
|
|
380
|
+
│ events stored in JSON store │
|
|
381
|
+
│ │
|
|
382
|
+
│ ${C.G}✓ Cost attribution${C.RST} Auto-recorded per run │
|
|
383
|
+
│ CostAttribution by agent │
|
|
384
|
+
│ │
|
|
385
|
+
│ ${C.G}✓ Replay engine${C.RST} Event hash verification passed │
|
|
386
|
+
│ Verify history without re-running │
|
|
387
|
+
│ │
|
|
388
|
+
└──────────────────────────────────────────────────────────┘
|
|
389
|
+
`);
|
|
390
|
+
|
|
391
|
+
console.log(` ${C.DIM}Storage file: ${root}/state.json${C.RST}`);
|
|
392
|
+
console.log(` ${C.DIM}Run: node typescript/examples/travel_assistant/travel_assistant.js${C.RST}`);
|
|
393
|
+
console.log();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
main().catch(err => { console.error(err); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentledger-runtime",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Dependency-free Node/TypeScript-compatible runtime for AgentLedger.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"types": "src/index.d.ts",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test",
|
|
11
|
+
"check": "node --check src/index.js && node --check test/runtime.test.js",
|
|
12
|
+
"conformance": "node src/cli.js conformance",
|
|
13
|
+
"contract:validate": "node src/cli.js contract validate"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"agentledger-ts": "src/cli.js"
|
|
20
|
+
}
|
|
21
|
+
}
|