agentledger-runtime 1.0.3 → 1.0.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # AgentLedger Node / TypeScript Runtime
2
2
 
3
- This directory contains the dependency-free Node/TypeScript-compatible runtime-core baseline for AgentLedger 1.0.2.
3
+ This directory contains the dependency-free Node/TypeScript-compatible runtime-core baseline for AgentLedger 1.0.5.
4
4
 
5
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
6
 
@@ -20,3 +20,20 @@ node src/cli.js conformance
20
20
  ```
21
21
 
22
22
  Package surface: `agentledger-runtime`. See `../README.md` for package metadata and API examples.
23
+
24
+
25
+ ## Travel Assistant
26
+
27
+ `travel_assistant/travel_assistant.js` is a larger interactive demo. Treat it as an example app, not part of the npm package release gate.
28
+
29
+ Run from the repository root:
30
+
31
+ ```bash
32
+ node typescript/examples/travel_assistant/travel_assistant.js
33
+ ```
34
+
35
+ Or pass a custom local state root:
36
+
37
+ ```bash
38
+ node typescript/examples/travel_assistant/travel_assistant.js .agentledger-ts
39
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentledger-runtime",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "private": false,
5
5
  "description": "Dependency-free Node/TypeScript-compatible runtime for AgentLedger.",
6
6
  "type": "module",
@@ -17,5 +17,11 @@
17
17
  },
18
18
  "bin": {
19
19
  "agentledger-ts": "src/cli.js"
20
- }
20
+ },
21
+ "files": [
22
+ "src/",
23
+ "examples/README.md",
24
+ "examples/quickstart/",
25
+ "README.md"
26
+ ]
21
27
  }
package/src/cli.js CHANGED
@@ -194,7 +194,7 @@ export function validateFixtures() {
194
194
  }
195
195
 
196
196
  function usage() {
197
- return `AgentLedger TypeScript Runtime 1.0.3\n\nUsage:\n agentledger-ts doctor\n agentledger-ts version\n agentledger-ts quickstart\n agentledger-ts conformance\n agentledger-ts contract validate\n agentledger-ts contract export\n\nProject: https://github.com/yaogdu/AgentLedger`;
197
+ return `AgentLedger TypeScript Runtime 1.0.5\n\nUsage:\n agentledger-ts doctor\n agentledger-ts version\n agentledger-ts quickstart\n agentledger-ts conformance\n agentledger-ts contract validate\n agentledger-ts contract export\n\nProject: https://github.com/yaogdu/AgentLedger`;
198
198
  }
199
199
 
200
200
  export async function runRuntimeSmoke() {
@@ -577,11 +577,11 @@ export async function main(args = process.argv.slice(2)) {
577
577
  return 0;
578
578
  }
579
579
  if (args.length === 1 && args[0] === 'version') {
580
- console.log('agentledger-ts 1.0.3');
580
+ console.log('agentledger-ts 1.0.5');
581
581
  return 0;
582
582
  }
583
583
  if (args.length === 1 && args[0] === 'doctor') {
584
- console.log(JSON.stringify({ language: 'typescript', version: '1.0.3', status: 'ok', runtime_core_parity: true }, null, 2));
584
+ console.log(JSON.stringify({ language: 'typescript', version: '1.0.5', status: 'ok', runtime_core_parity: true }, null, 2));
585
585
  return 0;
586
586
  }
587
587
  if (args.length === 1 && args[0] === 'quickstart') {
@@ -1,395 +0,0 @@
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, RetryableAgentError,
15
- exportEvidence, replay, costAttribution,
16
- } from '../../src/index.js';
17
-
18
- // ════════════════════════════════════════════════════════════
19
- // ANSI Colors
20
- // ════════════════════════════════════════════════════════════
21
- const C = {
22
- R: '\x1b[91m', G: '\x1b[92m', Y: '\x1b[93m', B: '\x1b[94m',
23
- M: '\x1b[95m', C: '\x1b[96m', BOLD: '\x1b[1m', DIM: '\x1b[2m', RST: '\x1b[0m',
24
- };
25
-
26
- // ════════════════════════════════════════════════════════════
27
- // Mock data
28
- // ════════════════════════════════════════════════════════════
29
- const MOCK_FLIGHTS = [
30
- { id: 'FL-002', from_city: 'Beijing', from_code: 'PEK', to_city: 'Tokyo', to_code: 'NRT', date: '2025-06-15', airline: 'JAL', price_usd: 580 },
31
- ];
32
- const MOCK_HOTELS = [
33
- { id: 'HT-002', city: 'Tokyo', name: 'APA Hotel Shinjuku', nightly_usd: 85, stars: 3 },
34
- ];
35
- const MOCK_WEATHER = { Tokyo: { temp_c: 24, condition: 'Partly Cloudy', humidity: 65 } };
36
- const bookingDB = {};
37
-
38
- // ════════════════════════════════════════════════════════════
39
- // Tool implementations
40
- // ════════════════════════════════════════════════════════════
41
-
42
- function searchFlights(args) {
43
- const origin = (args.from || '').trim().toLowerCase();
44
- const dest = (args.to || '').trim().toLowerCase();
45
- const results = MOCK_FLIGHTS.filter(f =>
46
- (f.from_city.toLowerCase().includes(origin) || f.from_code.toLowerCase().includes(origin)) &&
47
- (f.to_city.toLowerCase().includes(dest) || f.to_code.toLowerCase().includes(dest))
48
- );
49
- return { results, count: results.length };
50
- }
51
-
52
- function searchHotels(args) {
53
- const city = (args.city || '').trim().toLowerCase();
54
- const results = MOCK_HOTELS.filter(h => h.city.toLowerCase() === city);
55
- return { results, count: results.length };
56
- }
57
-
58
- function checkWeather(args) {
59
- const city = (args.city || '').trim();
60
- return { city, ...(MOCK_WEATHER[city] || { temp_c: 20, condition: 'Unknown' }) };
61
- }
62
-
63
- function bookFlight(args) {
64
- const ref = `BK-F-${args.flight_id}-${(args.passenger || '').slice(0, 3).toUpperCase()}`;
65
- if (bookingDB[ref]) return bookingDB[ref];
66
- const f = MOCK_FLIGHTS.find(f => f.id === args.flight_id);
67
- if (!f) throw new Error(`Flight not found: ${args.flight_id}`);
68
- const booking = { booking_ref: ref, type: 'flight', airline: f.airline, price_usd: f.price_usd, status: 'confirmed', external_id: ref };
69
- bookingDB[ref] = booking;
70
- return booking;
71
- }
72
-
73
- function bookHotel(args) {
74
- const ref = `BK-H-${args.hotel_id}-${(args.guest || '').slice(0, 3).toUpperCase()}`;
75
- if (bookingDB[ref]) return bookingDB[ref];
76
- const h = MOCK_HOTELS.find(h => h.id === args.hotel_id);
77
- if (!h) throw new Error(`Hotel not found: ${args.hotel_id}`);
78
- const booking = { booking_ref: ref, type: 'hotel', name: h.name, price_total_usd: h.nightly_usd * 5, status: 'confirmed', external_id: ref };
79
- bookingDB[ref] = booking;
80
- return booking;
81
- }
82
-
83
- // ════════════════════════════════════════════════════════════
84
- // Agent function
85
- // ════════════════════════════════════════════════════════════
86
-
87
- async function travelPlanner(ctx, state) {
88
- // Phase 1: Research
89
- const flights = await ctx.callTool('travel.search_flights', { from: 'Beijing', to: 'Tokyo' });
90
- const hotels = await ctx.callTool('travel.search_hotels', { city: 'Tokyo' });
91
- const weather = await ctx.callTool('travel.check_weather', { city: 'Tokyo' });
92
- await ctx.writeState('research', { flights: flights.count, hotels: hotels.count, weather: weather.temp_c });
93
-
94
- // Phase 2: Book flight (approval required)
95
- const flight = await ctx.callTool('travel.book_flight', {
96
- flight_id: 'FL-002', passenger: 'Demo User',
97
- _logical_operation: 'book-demo-flight',
98
- });
99
-
100
- // Phase 3: Simulated crash on attempt 2
101
- if (ctx.attempt === 2) {
102
- throw new RetryableAgentError('after flight booking');
103
- }
104
-
105
- // Phase 4: Book hotel (approval required)
106
- const hotel = await ctx.callTool('travel.book_hotel', {
107
- hotel_id: 'HT-002', check_in: '2025-06-15', check_out: '2025-06-20',
108
- guest: 'Demo User', _logical_operation: 'book-demo-hotel',
109
- });
110
-
111
- await ctx.writeState('bookings', { flight: flight.booking_ref, hotel: hotel.booking_ref });
112
- await ctx.writeState('trip_status', 'confirmed');
113
- }
114
-
115
- // ════════════════════════════════════════════════════════════
116
- // Display helpers
117
- // ════════════════════════════════════════════════════════════
118
-
119
- function wait(msg = 'Press Enter to continue') {
120
- return new Promise((resolve) => {
121
- process.stdout.write(`\n${C.DIM} ⏎ ${msg}...${C.RST}`);
122
- const rl = createInterface({ input: process.stdin, output: process.stdout });
123
- rl.once('line', () => { rl.close(); resolve(); });
124
- });
125
- }
126
-
127
- function showRows(label, headers, rows, color = C.C) {
128
- if (!rows || rows.length === 0) {
129
- console.log(`\n ${color}${label}:${C.RST} ${C.DIM}(empty)${C.RST}`);
130
- return;
131
- }
132
- console.log(`\n ${color}${label} (${rows.length} rows):${C.RST}`);
133
- for (const row of rows) {
134
- const items = headers.map((h, i) => `${h}=${C.BOLD}${row[i]}${C.RST}`);
135
- console.log(` ${C.DIM}${items.join(' | ')}${C.RST}`);
136
- }
137
- }
138
-
139
- function showDB(store, runID) {
140
- // Runs
141
- const runData = store.run(runID);
142
- const runRows = [];
143
- if (runData) {
144
- const shortID = runData.run_id.length > 24 ? runData.run_id.slice(0, 24) + '...' : runData.run_id;
145
- runRows.push([shortID, runData.status, String(runData.state_version)]);
146
- }
147
- showRows('Runs', ['run_id', 'status', 'state_version'], runRows, C.B);
148
-
149
- // Steps
150
- const steps = store.steps(runID);
151
- const stepRows = steps.map(s => {
152
- const shortID = s.step_id.length > 24 ? s.step_id.slice(0, 24) + '...' : s.step_id;
153
- return [shortID, s.status, String(s.attempt)];
154
- });
155
- showRows('Steps', ['step_id', 'status', 'attempt'], stepRows, C.B);
156
-
157
- // Tool Ledger
158
- const ledger = store.ledger(runID);
159
- const ledgerRows = ledger.map(tl => {
160
- const key = tl.idempotency_key;
161
- const parts = key.split(':');
162
- const shortKey = parts.length >= 2 ? parts[parts.length - 2] + ':' + parts[parts.length - 1].slice(0, 10) : key.slice(0, 25);
163
- return [tl.tool_name, tl.status, shortKey];
164
- });
165
- showRows('Tool Ledger', ['tool', 'status', 'idemp_key'], ledgerRows, C.Y);
166
-
167
- // Approval requests
168
- const approvals = store.approvalRequests(runID);
169
- const approvalRows = approvals.map(a => [a.tool_name, a.status, a.approved_by || '-']);
170
- showRows('Approval Requests', ['tool', 'status', 'approved_by'], approvalRows, C.R);
171
-
172
- console.log();
173
- }
174
-
175
- // ════════════════════════════════════════════════════════════
176
- // Main
177
- // ════════════════════════════════════════════════════════════
178
-
179
- async function main() {
180
- const root = process.argv[2] || '.agentledger-ts';
181
-
182
- // Intro
183
- console.log(`\n${C.BOLD}${C.C} ╔════════════════════════════════════════════════════╗${C.RST}`);
184
- console.log(`${C.BOLD}${C.C} ║ AgentLedger Travel Assistant (TS) — Interactive Demo ║${C.RST}`);
185
- console.log(`${C.BOLD}${C.C} ║ See real database state at every step ║${C.RST}`);
186
- console.log(`${C.BOLD}${C.C} ╚════════════════════════════════════════════════════╝${C.RST}`);
187
-
188
- console.log(`\n ${C.DIM}AgentLedger — Durable Execution Runtime for AI Agents${C.RST}`);
189
- console.log(` ┌────────────────────────────────────────────────────┐`);
190
- console.log(` │ ${C.G}✓${C.RST} Durable execution — crash recovery │`);
191
- console.log(` │ ${C.G}✓${C.RST} Tool Ledger — idempotent replay │`);
192
- console.log(` │ ${C.G}✓${C.RST} Approval gates — human-in-the-loop │`);
193
- console.log(` │ ${C.G}✓${C.RST} Policy engine — role-based access │`);
194
- console.log(` │ ${C.G}✓${C.RST} Budget control — tool call limits │`);
195
- console.log(` │ ${C.G}✓${C.RST} Evidence export — full audit trail │`);
196
- console.log(` └────────────────────────────────────────────────────┘`);
197
-
198
- await wait('Press Enter to start / 按 Enter 开始');
199
-
200
- // ════════════════════════════════════════════════════════
201
- // Step 1: Setup
202
- // ════════════════════════════════════════════════════════
203
- console.log(`\n${C.BOLD}${C.B}${'═'.repeat(60)}${C.RST}`);
204
- console.log(`${C.BOLD}${C.B} Step 1: Initialize — Register tools, configure policy${C.RST}`);
205
- console.log(`${C.BOLD}${C.B}${'═'.repeat(60)}${C.RST}`);
206
-
207
- const store = await JSONStore.open(`${root}/state.json`);
208
- const rt = new Runtime(store);
209
- rt.setBudget({ maxToolCalls: 25 });
210
-
211
- for (const t of ['travel.search_flights', 'travel.search_hotels', 'travel.check_weather',
212
- 'travel.book_flight', 'travel.book_hotel']) {
213
- rt.policy.allowTool('TravelPlanner', t);
214
- }
215
-
216
- rt.registerTool({
217
- name: 'travel.search_flights', version: 'v1', sideEffect: 'none', riskLevel: 'low',
218
- inputSchema: { type: 'object', required: ['from', 'to'] },
219
- outputSchema: { type: 'object' },
220
- func: searchFlights,
221
- });
222
- rt.registerTool({
223
- name: 'travel.search_hotels', version: 'v1', sideEffect: 'none', riskLevel: 'low',
224
- inputSchema: { type: 'object', required: ['city'] },
225
- outputSchema: { type: 'object' },
226
- func: searchHotels,
227
- });
228
- rt.registerTool({
229
- name: 'travel.check_weather', version: 'v1', sideEffect: 'none', riskLevel: 'low',
230
- inputSchema: { type: 'object', required: ['city'] },
231
- outputSchema: { type: 'object' },
232
- func: checkWeather,
233
- });
234
- rt.registerTool({
235
- name: 'travel.book_flight', version: 'v1', sideEffect: 'external_write', riskLevel: 'high',
236
- idempotencyRequired: true, approvalRequired: true,
237
- inputSchema: { type: 'object', required: ['flight_id', 'passenger'] },
238
- outputSchema: { type: 'object' },
239
- func: bookFlight,
240
- });
241
- rt.registerTool({
242
- name: 'travel.book_hotel', version: 'v1', sideEffect: 'external_write', riskLevel: 'high',
243
- idempotencyRequired: true, approvalRequired: true,
244
- inputSchema: { type: 'object', required: ['hotel_id', 'guest'] },
245
- outputSchema: { type: 'object' },
246
- func: bookHotel,
247
- });
248
-
249
- const { runId } = await rt.createRun({ trip: 'Tokyo', budget_usd: 3000 });
250
- console.log(`\n ${C.B}Run created: ${C.BOLD}${runId}${C.RST}`);
251
- showDB(store, runId);
252
- await wait('Press Enter to continue');
253
-
254
- // ════════════════════════════════════════════════════════
255
- // Step 2: Attempt 1 — Approval interception
256
- // ════════════════════════════════════════════════════════
257
- console.log(`\n${C.BOLD}${C.R}${'═'.repeat(60)}${C.RST}`);
258
- console.log(`${C.BOLD}${C.R} Step 2: Attempt 1 — Agent runs → Approval triggered${C.RST}`);
259
- console.log(`${C.BOLD}${C.R}${'═'.repeat(60)}${C.RST}`);
260
- console.log(`\n ${C.DIM}Agent executing: search flights → search hotels → check weather → book flight...${C.RST}`);
261
-
262
- await rt.runOnce({ runId, workerId: 'worker-node', agentRole: 'TravelPlanner', agent: travelPlanner });
263
-
264
- console.log(`\n ${C.R}book_flight triggered approval! Runtime paused, waiting for human.${C.RST}`);
265
- showDB(store, runId);
266
- console.log(` ${C.R}Note: Tool Ledger has RESERVED entry, approval status is PENDING${C.RST}`);
267
- await wait('Press Enter to approve / 按 Enter 审批');
268
-
269
- for (const req of store.approvalRequests(runId)) {
270
- if (req.status === 'PENDING') {
271
- await store.approveRequest(req.approval_id, { approver: 'traveler', reason: 'Within budget, approved' });
272
- console.log(`\n ${C.G}✅ Approved: ${req.tool_name} — by traveler${C.RST}`);
273
- }
274
- }
275
- showDB(store, runId);
276
- await wait('Press Enter to continue');
277
-
278
- // ════════════════════════════════════════════════════════
279
- // Step 3: Attempt 2 — Execute + Crash
280
- // ════════════════════════════════════════════════════════
281
- console.log(`\n${C.BOLD}${C.Y}${'═'.repeat(60)}${C.RST}`);
282
- console.log(`${C.BOLD}${C.Y} Step 3: Attempt 2 — Approved → Execute booking → Simulated crash${C.RST}`);
283
- console.log(`${C.BOLD}${C.Y}${'═'.repeat(60)}${C.RST}`);
284
- console.log(`\n ${C.DIM}Re-running agent (approval passed, book_flight will execute)...${C.RST}`);
285
-
286
- await rt.runOnce({ runId, workerId: 'worker-node', agentRole: 'TravelPlanner', agent: travelPlanner });
287
-
288
- console.log(`\n ${C.Y}Agent booked flight, then crashed before committing state!${C.RST}`);
289
- console.log(` ${C.Y}Flight is booked in external system, but agent state was NOT persisted.${C.RST}`);
290
- showDB(store, runId);
291
- console.log(` ${C.Y}Key: Tool Ledger book_flight status = ${C.G}SUCCEEDED${C.Y} (external side effect executed)${C.RST}`);
292
- console.log(` ${C.Y} Step status = retry_scheduled (state not committed, waiting for retry)${C.RST}`);
293
- await wait('Press Enter to continue');
294
-
295
- // ════════════════════════════════════════════════════════
296
- // Step 4: Attempt 3 — Recovery + Hotel approval
297
- // ════════════════════════════════════════════════════════
298
- console.log(`\n${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
299
- console.log(`${C.BOLD}${C.G} Step 4: Attempt 3 — Crash recovery → Tool Ledger idempotent replay${C.RST}`);
300
- console.log(`${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
301
- console.log(`\n ${C.DIM}Agent re-executes. book_flight: Tool Ledger sees SUCCEEDED record...${C.RST}`);
302
- console.log(` ${C.DIM}${C.G}→ Returns cached result, no duplicate API call, no double charge!${C.RST}`);
303
-
304
- await rt.runOnce({ runId, workerId: 'worker-node', agentRole: 'TravelPlanner', agent: travelPlanner });
305
-
306
- console.log(`\n ${C.G}✅ Flight idempotent replay successful! (no duplicate _book_flight call)${C.RST}`);
307
- console.log(` ${C.R}Hotel booking → triggers approval again${C.RST}`);
308
-
309
- for (const req of store.approvalRequests(runId)) {
310
- if (req.status === 'PENDING') {
311
- await store.approveRequest(req.approval_id, { approver: 'traveler', reason: 'Hotel within budget, approved' });
312
- console.log(`\n ${C.G}✅ Approved: ${req.tool_name} — by traveler${C.RST}`);
313
- }
314
- }
315
- showDB(store, runId);
316
- await wait('Press Enter to continue');
317
-
318
- // ════════════════════════════════════════════════════════
319
- // Step 5: Attempt 4 — Complete
320
- // ════════════════════════════════════════════════════════
321
- console.log(`\n${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
322
- console.log(`${C.BOLD}${C.G} Step 5: Attempt 4 — Hotel approved → Full execution → State committed${C.RST}`);
323
- console.log(`${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
324
-
325
- const ok = await rt.runOnce({ runId, workerId: 'worker-node', agentRole: 'TravelPlanner', agent: travelPlanner });
326
- if (!ok) {
327
- console.error('Recovery failed');
328
- process.exit(1);
329
- }
330
- if (Object.keys(bookingDB).length !== 2) {
331
- console.error(`Expected 2 bookings, got ${Object.keys(bookingDB).length}`);
332
- process.exit(1);
333
- }
334
-
335
- console.log(`\n ${C.G}✅ Travel planning complete! State persisted to database.${C.RST}`);
336
- showDB(store, runId);
337
- console.log(` ${C.G}Step status = completed, State has bookings + trip_status${C.RST}`);
338
- console.log(` ${C.G}External bookings: [${Object.keys(bookingDB).join(', ')}] (2 total, no duplicates)${C.RST}`);
339
- await wait('Press Enter to continue');
340
-
341
- // ════════════════════════════════════════════════════════
342
- // Step 6: Evidence + Cost + Replay
343
- // ════════════════════════════════════════════════════════
344
- console.log(`\n${C.BOLD}${C.M}${'═'.repeat(60)}${C.RST}`);
345
- console.log(`${C.BOLD}${C.M} Step 6: Evidence export + Cost attribution + Replay verification${C.RST}`);
346
- console.log(`${C.BOLD}${C.M}${'═'.repeat(60)}${C.RST}`);
347
-
348
- const bundle = exportEvidence(store, runId);
349
- const replayResult = replay(store, runId);
350
- const cost = costAttribution(store, runId);
351
-
352
- console.log(`\n ${C.M}Cost attribution: ${cost.total?.tool_calls ?? 0} tool calls${C.RST}`);
353
- console.log(` ${C.M}Replay: ${replayResult.event_count} events, safe=${C.G}${replayResult.replay_safe}${C.RST}`);
354
- console.log(` ${C.M}Evidence bundle: ${bundle.events.length} events total${C.RST}`);
355
-
356
- // Final summary
357
- console.log(`\n${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
358
- console.log(`${C.BOLD}${C.G} Summary: What AgentLedger (TypeScript) did in this demo${C.RST}`);
359
- console.log(`${C.BOLD}${C.G}${'═'.repeat(60)}${C.RST}`);
360
- console.log(`
361
- ┌──────────────────────────────────────────────────────────┐
362
- │ │
363
- │ ${C.G}✓ Durable execution${C.RST} Crash → auto retry, state preserved │
364
- │ Step: retry_scheduled → completed │
365
- │ │
366
- │ ${C.G}✓ Tool Ledger${C.RST} Idempotent replay, flight booked ${C.BOLD}1x${C.RST} only │
367
- │ SUCCEEDED → cached result on retry │
368
- │ │
369
- │ ${C.G}✓ Approval gates${C.RST} Flight + hotel each trigger approval │
370
- │ approval_requests records in store │
371
- │ │
372
- │ ${C.G}✓ Policy engine${C.RST} Each tool call checked by policy │
373
- │ TravelPlanner role allowed │
374
- │ │
375
- │ ${C.G}✓ Budget control${C.RST} Tracked ${cost.total?.tool_calls ?? 0} tool calls │
376
- │ BudgetController.beforeToolCall() │
377
- │ │
378
- │ ${C.G}✓ Evidence export${C.RST} ${bundle.events.length} events recorded │
379
- │ events stored in JSON store │
380
- │ │
381
- │ ${C.G}✓ Cost attribution${C.RST} Auto-recorded per run │
382
- │ CostAttribution by agent │
383
- │ │
384
- │ ${C.G}✓ Replay engine${C.RST} Event hash verification passed │
385
- │ Verify history without re-running │
386
- │ │
387
- └──────────────────────────────────────────────────────────┘
388
- `);
389
-
390
- console.log(` ${C.DIM}Storage file: ${root}/state.json${C.RST}`);
391
- console.log(` ${C.DIM}Run: node typescript/examples/travel_assistant/travel_assistant.js${C.RST}`);
392
- console.log();
393
- }
394
-
395
- main().catch(err => { console.error(err); process.exit(1); });
@@ -1,272 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { mkdtemp, readFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- import { tmpdir } from 'node:os';
5
- import test from 'node:test';
6
- import { JSONStore, LocalBlobStore, LocalWorker, RetryableAgentError, Runtime, WorkerService, exportEvidence, replay, costAttribution, failureAttribution } from '../src/index.js';
7
-
8
- test('runtime creates durable run, evidence, and replay summary', async () => {
9
- const dir = await mkdtemp(join(tmpdir(), 'agentledger-ts-'));
10
- const path = join(dir, 'state.json');
11
- const rt = await Runtime.local(path);
12
- rt.registerTool({ name: 'docs.echo', func: async (args) => ({ echo: args.text }) });
13
- const { runId } = await rt.createRun({ input: 'hello' });
14
- const ok = await rt.runOnce({
15
- runId,
16
- workerId: 'worker-a',
17
- agentRole: 'Researcher',
18
- agent: async (ctx, state) => {
19
- const result = await ctx.callTool('docs.echo', { text: state.input });
20
- await ctx.writeState('tool_result', result);
21
- },
22
- });
23
- assert.equal(ok, true);
24
- await readFile(path, 'utf8');
25
- const reopened = await JSONStore.open(path);
26
- assert.deepEqual(reopened.finalState(runId).tool_result, { echo: 'hello' });
27
- const bundle = exportEvidence(reopened, runId);
28
- assert.equal(bundle.schema_version, 'agentledger.evidence.v1');
29
- assert.ok(bundle.bundle_hash);
30
- const summary = replay(reopened, runId);
31
- assert.equal(summary.replay_safe, true);
32
- assert.equal(summary.event_count, bundle.events.length);
33
- assert.equal(summary.tool_call_count, 2);
34
- });
35
-
36
- test('local blob store roundtrips JSON-compatible values', async () => {
37
- const dir = await mkdtemp(join(tmpdir(), 'agentledger-ts-blobs-'));
38
- const blobs = await LocalBlobStore.open(dir);
39
- const value = { hello: 'world', nested: { n: 1 } };
40
- const first = await blobs.putJSON(value);
41
- const second = await blobs.putJSON(value);
42
- assert.ok(first.digest.startsWith('sha256:'));
43
- assert.ok(first.ref.startsWith('blob://sha256/'));
44
- assert.deepEqual(first, second);
45
- assert.deepEqual(await blobs.getJSON(first.ref), value);
46
- await assert.rejects(() => blobs.getJSON('unsupported://blob'), /unsupported blob ref/);
47
- });
48
-
49
- test('tool schema validation rejects invalid input before execution', async () => {
50
- const rt = new Runtime(JSONStore.memory());
51
- let calls = 0;
52
- rt.registerTool({
53
- name: 'docs.echo',
54
- inputSchema: { type: 'object', required: ['text'], additionalProperties: false, properties: { text: { type: 'string', minLength: 1 } } },
55
- outputSchema: { type: 'object', required: ['echo'], additionalProperties: false, properties: { echo: { type: 'string' } } },
56
- func: async (args) => { calls += 1; return { echo: args.text }; },
57
- });
58
- const { runId } = await rt.createRun({});
59
- await assert.rejects(() => rt.runOnce({ runId, agentRole: 'SchemaAgent', agent: (ctx) => ctx.callTool('docs.echo', {}) }), /required/);
60
- assert.equal(calls, 0);
61
- assert.equal(rt.store.events(runId).some((event) => event.type === 'tool_call_failed' && event.payload.phase === 'input_validation'), true);
62
- });
63
-
64
- test('tool ledger idempotency reuses side effect response across retry', async () => {
65
- const rt = new Runtime(JSONStore.memory());
66
- let calls = 0;
67
- rt.registerTool({
68
- name: 'github.create_pr',
69
- sideEffect: 'external',
70
- idempotencyRequired: true,
71
- func: async (args) => {
72
- calls += 1;
73
- return { external_id: 'pr-123', title: args.title };
74
- },
75
- });
76
- const { runId } = await rt.createRun({ title: 'runtime parity' });
77
- const agent = async (ctx, state) => {
78
- const result = await ctx.callTool('github.create_pr', { title: state.title });
79
- if (ctx.attempt === 1) throw new RetryableAgentError('crash after side effect');
80
- await ctx.writeState('pr', result);
81
- };
82
- assert.equal(await rt.runOnce({ runId, agent }), false);
83
- assert.equal(await rt.runOnce({ runId, workerId: 'worker-b', agent }), true);
84
- assert.equal(calls, 1);
85
- assert.equal(rt.store.ledger(runId).length, 1);
86
- assert.equal(rt.store.ledger(runId)[0].status, 'SUCCEEDED');
87
- });
88
-
89
- test('policy denies unapproved high-risk tool before execution', async () => {
90
- const rt = new Runtime(JSONStore.memory());
91
- let calls = 0;
92
- rt.registerTool({ name: 'repo.write', riskLevel: 'high', func: async () => { calls += 1; return { ok: true }; } });
93
- const { runId } = await rt.createRun({});
94
- await assert.rejects(() => rt.runOnce({ runId, agentRole: 'Reviewer', agent: (ctx) => ctx.callTool('repo.write', { path: 'README.md' }) }), /high-risk tool denied/);
95
- assert.equal(calls, 0);
96
- assert.equal(rt.store.events(runId).some((event) => event.type === 'tool_permission_decided' && event.payload.allowed === false), true);
97
- assert.equal(rt.store.steps(runId)[0].status, 'failed');
98
- });
99
-
100
- test('approval pauses and resumes step', async () => {
101
- const rt = new Runtime(JSONStore.memory());
102
- let calls = 0;
103
- rt.registerTool({
104
- name: 'github.create_pr',
105
- riskLevel: 'high',
106
- approvalRequired: true,
107
- sideEffect: 'external',
108
- idempotencyRequired: true,
109
- func: async () => { calls += 1; return { external_id: 'pr-42' }; },
110
- });
111
- const { runId } = await rt.createRun({});
112
- const agent = async (ctx) => {
113
- const result = await ctx.callTool('github.create_pr', { title: 'safe' });
114
- await ctx.writeState('pr', result);
115
- };
116
- assert.equal(await rt.runOnce({ runId, workerId: 'worker-a', agentRole: 'Coder', agent }), false);
117
- assert.equal(calls, 0);
118
- const approvals = rt.store.approvalRequests(runId);
119
- assert.equal(approvals.length, 1);
120
- assert.equal(approvals[0].status, 'PENDING');
121
- assert.equal(rt.store.steps(runId)[0].status, 'waiting_human');
122
- await rt.store.approveRequest(approvals[0].approval_id, { approver: 'alice', reason: 'reviewed' });
123
- assert.equal(await rt.runOnce({ runId, workerId: 'worker-b', agentRole: 'Coder', agent }), true);
124
- assert.equal(calls, 1);
125
-
126
- const { runId: deniedRun } = await rt.createRun({});
127
- assert.equal(await rt.runOnce({ runId: deniedRun, workerId: 'worker-c', agentRole: 'Coder', agent }), false);
128
- await rt.store.denyRequest(rt.store.approvalRequests(deniedRun)[0].approval_id, { approver: 'bob', reason: 'not allowed' });
129
- assert.equal(rt.store.steps(deniedRun)[0].status, 'failed');
130
- });
131
-
132
- test('sandbox-required tool fails closed without executor', async () => {
133
- const rt = new Runtime(JSONStore.memory());
134
- let calls = 0;
135
- rt.registerTool({ name: 'shell.exec', sandboxRequired: true, func: async () => { calls += 1; return { ok: true }; } });
136
- const { runId } = await rt.createRun({});
137
- await assert.rejects(() => rt.runOnce({ runId, agentRole: 'Executor', agent: (ctx) => ctx.callTool('shell.exec', { argv: ['echo', 'hi'] }) }), /sandbox executor/);
138
- assert.equal(calls, 0);
139
- assert.equal(rt.store.events(runId).some((event) => event.type === 'sandbox_started'), true);
140
- assert.equal(rt.store.events(runId).some((event) => event.type === 'tool_call_failed'), true);
141
- });
142
-
143
- test('cost budget and failure attribution are recorded', async () => {
144
- const rt = new Runtime(JSONStore.memory());
145
- rt.setBudget({ maxToolCalls: 1 });
146
- let calls = 0;
147
- rt.registerTool({ name: 'docs.echo', func: async (args) => { calls += 1; return { echo: args.text }; } });
148
- const { runId } = await rt.createRun({});
149
- await assert.rejects(() => rt.runOnce({
150
- runId,
151
- agentRole: 'Researcher',
152
- agent: async (ctx) => {
153
- await ctx.recordModelCall({ model: 'gpt-test', inputTokens: 10, outputTokens: 5, totalUsd: 0.01 });
154
- await ctx.callTool('docs.echo', { text: 'first' });
155
- await ctx.callTool('docs.echo', { text: 'second' });
156
- },
157
- }), /tool call budget exceeded/);
158
- assert.equal(calls, 1);
159
- const summary = rt.store.costSummary(runId);
160
- assert.equal(summary.tool_calls, 1);
161
- assert.equal(summary.model_tokens, 15);
162
- assert.equal(summary.total_usd, 0.01);
163
- const cost = costAttribution(rt.store, runId);
164
- assert.equal(cost.by_agent.Researcher.tool_calls, 1);
165
- assert.equal(cost.by_agent.Researcher.model_tokens, 15);
166
- const failure = failureAttribution(rt.store, runId);
167
- assert.equal(failure.summary.failed_step_count, 1);
168
- assert.equal(failure.failure_events.some((event) => event.type === 'budget_check_failed'), true);
169
- assert.equal(failure.failure_events.some((event) => event.type === 'failure_classified'), true);
170
- });
171
-
172
- test('media and stream artifacts are indexed in evidence and replay', async () => {
173
- const rt = new Runtime(JSONStore.memory());
174
- const { runId } = await rt.createRun({});
175
- const ok = await rt.runOnce({
176
- runId,
177
- workerId: 'worker-media',
178
- agentRole: 'MediaAgent',
179
- agent: async (ctx) => {
180
- const frame = await ctx.createMediaArtifact('frame-0001', 'frame', {
181
- uri: 's3://media/demo/frame-0001.jpg',
182
- mediaMetadata: { mime_type: 'image/jpeg', frame_index: 1 },
183
- lineage: { source_blob_refs: ['s3://media/demo/input.mp4'], tool_call_ids: ['video.extract_frames'] },
184
- });
185
- const checkpoint = await ctx.createStreamCheckpoint('camera-checkpoint', {
186
- streamId: 'camera-1',
187
- consumerId: 'vision-agent',
188
- offset: 7,
189
- watermark: 1.5,
190
- chunk: { streamId: 'camera-1', chunkId: 'chunk-7', offset: 7, contentRef: 'blob://sha256/chunk-7.json', sequence: 7 },
191
- backpressure: { recommended_pause_ms: 100 },
192
- });
193
- await ctx.writeState('artifacts', { frame, checkpoint });
194
- },
195
- });
196
- assert.equal(ok, true);
197
- const bundle = exportEvidence(rt.store, runId);
198
- assert.equal(bundle.summary.artifact_count, 2);
199
- assert.equal(bundle.summary.media_artifact_count, 1);
200
- assert.equal(bundle.summary.stream_checkpoint_count, 1);
201
- assert.equal(bundle.media_artifacts[0].kind, 'frame');
202
- assert.equal(bundle.stream_checkpoints[0].stream_id, 'camera-1');
203
- const summary = replay(rt.store, runId);
204
- assert.equal(summary.artifact_count, 2);
205
- assert.equal(summary.media_artifact_count, 1);
206
- assert.equal(summary.stream_checkpoint_count, 1);
207
- });
208
-
209
- test('lease recovery fences previous owner', async () => {
210
- const store = JSONStore.memory();
211
- const { runId, stepId } = await store.createRun({});
212
- const claim = await store.claimStep({ workerId: 'stale-worker', runId, leaseSeconds: 0 });
213
- assert.equal(await store.recoverExpiredLeases(), 1);
214
- await assert.rejects(() => store.commitStatePatch({ runId, stepId, leaseToken: claim.lease_token, baseVersion: 0, patch: { late: true } }), /invalid or stale lease token/);
215
- const next = await store.claimStep({ workerId: 'new-worker', runId, leaseSeconds: 60 });
216
- assert.equal(next.step_id, stepId);
217
- });
218
-
219
- test('cancellation fences worker', async () => {
220
- const store = JSONStore.memory();
221
- const { runId, stepId } = await store.createRun({});
222
- const claim = await store.claimStep({ workerId: 'worker', runId, leaseSeconds: 60 });
223
- assert.equal(await store.cancelRun(runId, 'operator requested'), 1);
224
- await assert.rejects(() => store.commitStatePatch({ runId, stepId, leaseToken: claim.lease_token, baseVersion: 0, patch: { late: true } }), /invalid or stale lease token/);
225
- });
226
-
227
- test('contract fixture is readable and includes TypeScript target', async () => {
228
- const contract = JSON.parse(await readFile(new URL('../../contracts/agentledger.runtime.v1.json', import.meta.url), 'utf8'));
229
- assert.equal(contract.contract_version, '1.0');
230
- assert.ok(contract.language_targets.some((target) => target.language === 'typescript'));
231
- });
232
-
233
- test('shared runtime baseline fixture covers preview scenarios', async () => {
234
- const fixture = JSON.parse(await readFile(new URL('../../contracts/conformance/runtime_baseline.v1.json', import.meta.url), 'utf8'));
235
- assert.equal(fixture.schema_version, 'agentledger.conformance.runtime_baseline.v1');
236
- assert.equal(fixture.contract_version, '1.0');
237
- const names = new Set(fixture.required_scenarios.map((scenario) => scenario.name));
238
- for (const name of ['durable_run_evidence_replay', 'tool_ledger_idempotent_retry', 'lease_recovery_fences_stale_worker', 'cancellation_fences_worker']) assert.equal(names.has(name), true, `missing shared fixture scenario ${name}`);
239
- for (const scenario of fixture.required_scenarios) assert.ok(scenario.required_assertions.length > 0, `scenario ${scenario.name} should define assertions`);
240
- });
241
-
242
- test('shared parity fixtures cover implemented scenarios', async () => {
243
- const fixtures = {
244
- 'policy_approval_sandbox.v1.json': ['agentledger.conformance.policy_approval_sandbox.v1', 'policy_denies_unapproved_high_risk_tool', 'approval_pauses_and_resumes_step', 'sandbox_required_tool_fails_closed'],
245
- 'cost_failure_attribution.v1.json': ['agentledger.conformance.cost_failure_attribution.v1', 'tool_and_model_cost_attributed_to_run_step_role', 'budget_exhaustion_blocks_execution', 'failure_attribution_classifies_agent_tool_model_runtime'],
246
- 'local_persistence.v1.json': ['agentledger.conformance.local_persistence.v1', 'local_store_round_trips_completed_run', 'local_store_preserves_evidence_replay_chain', 'local_store_uses_atomic_snapshot_write'],
247
- 'local_blob_store.v1.json': ['agentledger.conformance.local_blob_store.v1', 'blob_roundtrip_json_value', 'blob_content_address_is_stable', 'blob_bad_ref_is_rejected'],
248
- 'tool_schema_validation.v1.json': ['agentledger.conformance.tool_schema_validation.v1', 'invalid_tool_input_rejected_before_execution', 'valid_tool_input_and_output_pass', 'invalid_tool_output_rejected'],
249
- 'worker_service.v1.json': ['agentledger.conformance.worker_service.v1', 'local_worker_runs_until_terminal', 'worker_service_stops_after_idle_poll', 'worker_loop_recovers_expired_leases'],
250
- 'media_stream_artifacts.v1.json': ['agentledger.conformance.media_stream_artifacts.v1', 'media_artifact_ref_is_indexed_in_evidence', 'stream_checkpoint_ref_is_indexed_in_evidence'],
251
- 'evidence_consumers.v1.json': ['agentledger.conformance.evidence_consumers.v1', 'trace_spans_from_evidence', 'evidence_diff_detects_state_and_event_changes', 'divergence_report_lists_changed_dimensions', 'static_debug_summary_is_exportable'],
252
- 'static_debug_html.v1.json': ['agentledger.conformance.static_debug_html.v1', 'static_debug_html_contains_run_events_and_state'],
253
- 'ops_readiness.v1.json': ['agentledger.conformance.ops_readiness.v1', 'retention_plan_is_non_destructive_and_counts_evidence', 'backup_readiness_reports_required_checks'],
254
- 'storage_schema.v1.json': ['agentledger.conformance.storage_schema.v1', 'latest_schema_version_and_ddl_are_available'],
255
- 'mcp_adapters.v1.json': ['agentledger.conformance.mcp_adapters.v1', 'in_memory_mcp_tool_server_lists_and_calls_tools', 'mcp_tool_descriptor_maps_to_tool_spec', 'in_memory_mcp_context_server_reads_resources'],
256
- 'framework_adapters.v1.json': ['agentledger.conformance.framework_adapters.v1', 'function_adapter_maps_run_spec_and_invokes_agent', 'method_framework_adapter_uses_first_available_method_and_writes_output'],
257
- 'otlp_trace_export.v1.json': ['agentledger.conformance.otlp_trace_export.v1', 'otlp_json_contains_resource_scope_and_spans'],
258
- 'simple_api.v1.json': ['agentledger.conformance.simple_api.v1', 'simple_run_returns_output_and_state'],
259
- 'boundary_lint.v1.json': ['agentledger.conformance.boundary_lint.v1', 'direct_shell_and_http_calls_are_reported', 'ignored_lines_are_not_reported'],
260
- 'scheduler.v1.json': ['agentledger.conformance.scheduler.v1', 'scheduler_status_reports_run_steps_and_cost', 'scheduler_recover_and_cancel_delegate_to_store'],
261
- 'adversarial_review.v1.json': ['agentledger.conformance.adversarial_review.v1', 'clean_evidence_passes_blocker_review', 'pending_high_risk_approval_blocks_review', 'max_total_usd_limit_blocks_review'],
262
- 'evidence_regression.v1.json': ['agentledger.conformance.evidence_regression.v1', 'evidence_health_checks_pass_for_clean_bundle', 'regression_detects_final_state_and_event_type_changes', 'regression_cost_delta_limit_blocks'],
263
- 'failure_injection.v1.json': ['agentledger.conformance.failure_injection.v1', 'retry_exhaustion_marks_run_failed', 'lease_fencing_rejects_stale_commit', 'cancellation_fencing_rejects_late_commit', 'side_effect_idempotency_executes_once_across_retry'],
264
- 'shadow.v1.json': ['agentledger.conformance.shadow.v1', 'shadow_state_diff_reports_changed_keys', 'shadow_report_carries_source_shadow_and_ok'],
265
- 'repro.v1.json': ['agentledger.conformance.repro.v1', 'builtin_golden_names_are_available', 'minimal_success_golden_is_valid_evidence', 'golden_regression_detects_changed_final_state'],
266
- 'time_travel.v1.json': ['agentledger.conformance.time_travel.v1', 'timeline_reconstructs_state_at_selected_seq', 'timeline_marks_state_changed_frames', 'time_travel_report_exports_static_html'],
267
- };
268
- for (const [file, required] of Object.entries(fixtures)) {
269
- const body = await readFile(new URL(`../../contracts/conformance/${file}`, import.meta.url), 'utf8');
270
- for (const token of required) assert.ok(body.includes(token), `fixture ${file} missing ${token}`);
271
- }
272
- });