dialai 1.0.0 → 1.2.0

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.
Files changed (34) hide show
  1. package/.claude/skills/dial-machine/SKILL.md +401 -0
  2. package/.claude/skills/dial-machine/references/api-reference.md +515 -0
  3. package/.claude/skills/dial-machine/references/patterns.md +628 -0
  4. package/.claude/skills/spec-for-ralph/SKILL.md +542 -0
  5. package/.claude/specs/llm-audit-log.md +280 -0
  6. package/LICENSE +1 -1
  7. package/README.md +1 -1
  8. package/dist/dialai/api.d.ts +2 -6
  9. package/dist/dialai/api.d.ts.map +1 -1
  10. package/dist/dialai/api.js +22 -6
  11. package/dist/dialai/api.js.map +1 -1
  12. package/dist/dialai/llm.d.ts +6 -4
  13. package/dist/dialai/llm.d.ts.map +1 -1
  14. package/dist/dialai/llm.js +96 -31
  15. package/dist/dialai/llm.js.map +1 -1
  16. package/dist/dialai/migrations/002-llm-audit-log.d.ts +8 -0
  17. package/dist/dialai/migrations/002-llm-audit-log.d.ts.map +1 -0
  18. package/dist/dialai/migrations/002-llm-audit-log.js +41 -0
  19. package/dist/dialai/migrations/002-llm-audit-log.js.map +1 -0
  20. package/dist/dialai/migrations/migrate.d.ts.map +1 -1
  21. package/dist/dialai/migrations/migrate.js +2 -0
  22. package/dist/dialai/migrations/migrate.js.map +1 -1
  23. package/dist/dialai/store-memory.d.ts.map +1 -1
  24. package/dist/dialai/store-memory.js +22 -0
  25. package/dist/dialai/store-memory.js.map +1 -1
  26. package/dist/dialai/store-postgres.d.ts.map +1 -1
  27. package/dist/dialai/store-postgres.js +54 -1
  28. package/dist/dialai/store-postgres.js.map +1 -1
  29. package/dist/dialai/store.d.ts +3 -1
  30. package/dist/dialai/store.d.ts.map +1 -1
  31. package/dist/dialai/store.js.map +1 -1
  32. package/dist/dialai/types.d.ts +54 -0
  33. package/dist/dialai/types.d.ts.map +1 -1
  34. package/package.json +3 -2
@@ -0,0 +1,628 @@
1
+ # DIAL Machine Patterns
2
+
3
+ Copy-paste-ready patterns for common DIAL machine configurations.
4
+
5
+ ## 1. Minimal Machine
6
+
7
+ Single transition with `runSession()` defaults (auto-registers `firstAvailable` proposer and `firstProposal` arbiter):
8
+
9
+ ```typescript
10
+ import { runSession } from "dialai";
11
+ import type { MachineDefinition } from "dialai";
12
+
13
+ const machine: MachineDefinition = {
14
+ machineName: "simple-task",
15
+ initialState: "pending",
16
+ goalState: "done",
17
+ states: {
18
+ pending: {
19
+ prompt: "Should we complete this task?",
20
+ transitions: { complete: "done" },
21
+ },
22
+ done: {},
23
+ },
24
+ };
25
+
26
+ const session = await runSession(machine);
27
+ console.log(session.currentState); // "done"
28
+ ```
29
+
30
+ ## 2. Linear Pipeline
31
+
32
+ Multi-step sequential workflow:
33
+
34
+ ```typescript
35
+ const pipeline: MachineDefinition = {
36
+ machineName: "data-pipeline",
37
+ initialState: "queued",
38
+ goalState: "complete",
39
+ states: {
40
+ queued: {
41
+ prompt: "Start processing?",
42
+ transitions: { start: "processing" },
43
+ },
44
+ processing: {
45
+ prompt: "Processing complete. Validate results?",
46
+ transitions: { validate: "validating" },
47
+ },
48
+ validating: {
49
+ prompt: "Validation passed. Finalize?",
50
+ transitions: { finalize: "complete" },
51
+ },
52
+ complete: {},
53
+ },
54
+ };
55
+
56
+ const session = await runSession(pipeline);
57
+ // queued -> processing -> validating -> complete
58
+ console.log(session.history.length); // 3
59
+ ```
60
+
61
+ ## 3. Branching with Rejection Loops
62
+
63
+ Approve/reject with loop back:
64
+
65
+ ```typescript
66
+ const review: MachineDefinition = {
67
+ machineName: "code-review",
68
+ initialState: "draft",
69
+ goalState: "merged",
70
+ states: {
71
+ draft: {
72
+ prompt: "Review this PR. Approve or request changes?",
73
+ transitions: {
74
+ approve: "merged",
75
+ request_changes: "revision",
76
+ },
77
+ },
78
+ revision: {
79
+ prompt: "Author has addressed feedback. Approve or request more changes?",
80
+ transitions: {
81
+ approve: "merged",
82
+ request_changes: "revision",
83
+ },
84
+ },
85
+ merged: {},
86
+ },
87
+ };
88
+ ```
89
+
90
+ ## 4. Human-in-the-Loop
91
+
92
+ AI proposes, human forces via `submitArbitration`:
93
+
94
+ ```typescript
95
+ import {
96
+ createSession,
97
+ registerProposer,
98
+ registerArbiter,
99
+ submitProposal,
100
+ submitArbitration,
101
+ getSession,
102
+ } from "dialai";
103
+
104
+ const machine: MachineDefinition = {
105
+ machineName: "content-moderation",
106
+ initialState: "pending_review",
107
+ goalState: "resolved",
108
+ states: {
109
+ pending_review: {
110
+ prompt: "Review this content. Approve, flag, or remove?",
111
+ transitions: {
112
+ approve: "resolved",
113
+ flag: "flagged",
114
+ remove: "removed",
115
+ },
116
+ },
117
+ flagged: {
118
+ prompt: "Flagged content. Escalate or resolve?",
119
+ transitions: {
120
+ escalate: "escalated",
121
+ resolve: "resolved",
122
+ },
123
+ },
124
+ escalated: {
125
+ prompt: "Senior review. Approve or remove?",
126
+ transitions: {
127
+ approve: "resolved",
128
+ remove: "removed",
129
+ },
130
+ },
131
+ resolved: {},
132
+ removed: {},
133
+ },
134
+ };
135
+
136
+ // Create session and register AI proposer + human specialist
137
+ const session = await createSession(machine);
138
+
139
+ await registerProposer({
140
+ specialistId: "ai-moderator",
141
+ machineName: "content-moderation",
142
+ strategyFnName: "firstAvailable",
143
+ });
144
+
145
+ await registerProposer({
146
+ specialistId: "human-moderator",
147
+ machineName: "content-moderation",
148
+ strategyFnName: "firstAvailable",
149
+ isHuman: true,
150
+ });
151
+
152
+ await registerArbiter({
153
+ specialistId: "mod-arbiter",
154
+ machineName: "content-moderation",
155
+ strategyFnName: "alignmentMargin",
156
+ });
157
+
158
+ // AI submits its proposal
159
+ await submitProposal({
160
+ sessionId: session.sessionId,
161
+ specialistId: "ai-moderator",
162
+ });
163
+
164
+ // Human overrides with a forced decision
165
+ const result = await submitArbitration({
166
+ sessionId: session.sessionId,
167
+ specialistId: "human-moderator",
168
+ transitionName: "flag",
169
+ reasoning: "Content needs further review",
170
+ });
171
+
172
+ console.log(result.executed); // true
173
+ console.log(result.isHuman); // true
174
+
175
+ const updated = await getSession(session.sessionId);
176
+ console.log(updated.currentState); // "flagged"
177
+ ```
178
+
179
+ ## 5. Multi-Agent Consensus
180
+
181
+ Multiple proposers with `alignmentMargin` arbiter:
182
+
183
+ ```typescript
184
+ import {
185
+ clear,
186
+ createSession,
187
+ registerProposer,
188
+ registerArbiter,
189
+ submitProposal,
190
+ submitArbitration,
191
+ getSession,
192
+ } from "dialai";
193
+
194
+ await clear();
195
+
196
+ const machine: MachineDefinition = {
197
+ machineName: "investment-decision",
198
+ initialState: "analysis",
199
+ goalState: "executed",
200
+ consensusThreshold: 0.6,
201
+ states: {
202
+ analysis: {
203
+ prompt: "Analyze this investment. Buy, hold, or sell?",
204
+ transitions: {
205
+ buy: "executed",
206
+ hold: "monitoring",
207
+ sell: "executed",
208
+ },
209
+ },
210
+ monitoring: {
211
+ prompt: "Re-evaluate position. Buy or sell?",
212
+ transitions: {
213
+ buy: "executed",
214
+ sell: "executed",
215
+ },
216
+ },
217
+ executed: {},
218
+ },
219
+ };
220
+
221
+ const session = await createSession(machine);
222
+
223
+ // Register multiple AI proposers with different strategies
224
+ await registerProposer({
225
+ specialistId: "bull-analyst",
226
+ machineName: "investment-decision",
227
+ strategyFn: async (ctx) => ({
228
+ transitionName: "buy",
229
+ toState: ctx.transitions["buy"],
230
+ reasoning: "Bullish indicators suggest buying",
231
+ }),
232
+ });
233
+
234
+ await registerProposer({
235
+ specialistId: "bear-analyst",
236
+ machineName: "investment-decision",
237
+ strategyFn: async (ctx) => ({
238
+ transitionName: "hold",
239
+ toState: ctx.transitions["hold"],
240
+ reasoning: "Market uncertainty suggests holding",
241
+ }),
242
+ });
243
+
244
+ await registerProposer({
245
+ specialistId: "quant-analyst",
246
+ machineName: "investment-decision",
247
+ strategyFn: async (ctx) => ({
248
+ transitionName: "buy",
249
+ toState: ctx.transitions["buy"],
250
+ reasoning: "Quantitative signals are positive",
251
+ }),
252
+ });
253
+
254
+ // Register alignment-based arbiter
255
+ await registerArbiter({
256
+ specialistId: "investment-arbiter",
257
+ machineName: "investment-decision",
258
+ strategyFnName: "alignmentMargin",
259
+ threshold: 0.6,
260
+ });
261
+
262
+ // Submit all proposals
263
+ await submitProposal({ sessionId: session.sessionId, specialistId: "bull-analyst" });
264
+ await submitProposal({ sessionId: session.sessionId, specialistId: "bear-analyst" });
265
+ await submitProposal({ sessionId: session.sessionId, specialistId: "quant-analyst" });
266
+
267
+ // Arbitrate
268
+ const result = await submitArbitration({ sessionId: session.sessionId });
269
+ console.log(result.executed); // depends on alignment scores
270
+ console.log(result.guardReason); // explains consensus decision
271
+ ```
272
+
273
+ ## 6. LLM-Powered Proposer
274
+
275
+ Use `contextFn` + `modelId` to have DIAL call an LLM:
276
+
277
+ ```typescript
278
+ await registerProposer({
279
+ specialistId: "llm-reviewer",
280
+ machineName: "document-review",
281
+ contextFn: async (ctx) => {
282
+ return `You are a document reviewer. The document is in state "${ctx.currentState}".
283
+
284
+ Prompt: ${ctx.prompt}
285
+
286
+ Available actions:
287
+ ${Object.entries(ctx.transitions)
288
+ .map(([name, target]) => `- "${name}" -> goes to "${target}"`)
289
+ .join("\n")}
290
+
291
+ Previous actions taken:
292
+ ${ctx.history.length > 0 ? ctx.history.map((h) => `- ${h.transitionName}: ${h.reasoning}`).join("\n") : "None"}
293
+
294
+ ${ctx.metaJson ? `Additional context: ${JSON.stringify(ctx.metaJson)}` : ""}
295
+
296
+ Choose the best action and explain your reasoning.`;
297
+ },
298
+ modelId: "anthropic/claude-sonnet-4",
299
+ });
300
+ ```
301
+
302
+ Requires `OPENROUTER_API_TOKEN` in the environment (or set `DIALAI_LLM_BASE_URL` for a different OpenAI-compatible provider).
303
+
304
+ ## 7. Per-State Specialists
305
+
306
+ Different specialists for different states in the machine definition:
307
+
308
+ ```typescript
309
+ const machine: MachineDefinition = {
310
+ machineName: "hiring-pipeline",
311
+ initialState: "screening",
312
+ goalState: "hired",
313
+ states: {
314
+ screening: {
315
+ prompt: "Screen this candidate. Pass or reject?",
316
+ transitions: {
317
+ pass: "interview",
318
+ reject: "rejected",
319
+ },
320
+ specialists: [
321
+ { role: "proposer", specialistId: "hr-screener", strategyFnName: "firstAvailable" },
322
+ { role: "arbiter", specialistId: "screening-arbiter", strategyFnName: "firstProposal" },
323
+ ],
324
+ },
325
+ interview: {
326
+ prompt: "Interview complete. Hire or reject?",
327
+ transitions: {
328
+ hire: "hired",
329
+ reject: "rejected",
330
+ },
331
+ specialists: [
332
+ { role: "proposer", specialistId: "interviewer-1", strategyFnName: "firstAvailable" },
333
+ { role: "proposer", specialistId: "interviewer-2", strategyFnName: "lastAvailable" },
334
+ { role: "arbiter", specialistId: "hiring-arbiter", strategyFnName: "alignmentMargin" },
335
+ ],
336
+ },
337
+ hired: {},
338
+ rejected: {},
339
+ },
340
+ };
341
+ ```
342
+
343
+ ## 8. Embedded Specialists in JSON
344
+
345
+ Complete runnable JSON machine with specialists (save as `.json` and run with `npx dialai`):
346
+
347
+ ```json
348
+ {
349
+ "machineName": "approval-flow",
350
+ "initialState": "submitted",
351
+ "goalState": "approved",
352
+ "specialists": [
353
+ {
354
+ "role": "proposer",
355
+ "specialistId": "auto-approver",
356
+ "strategyFnName": "firstAvailable"
357
+ },
358
+ {
359
+ "role": "arbiter",
360
+ "specialistId": "flow-arbiter",
361
+ "strategyFnName": "firstProposal"
362
+ }
363
+ ],
364
+ "states": {
365
+ "submitted": {
366
+ "prompt": "Review submission. Approve or reject?",
367
+ "transitions": {
368
+ "approve": "approved",
369
+ "reject": "rejected"
370
+ }
371
+ },
372
+ "approved": {},
373
+ "rejected": {}
374
+ }
375
+ }
376
+ ```
377
+
378
+ ```bash
379
+ npx dialai approval-flow.json
380
+ ```
381
+
382
+ ## 9. Session Metadata
383
+
384
+ Pass runtime context via `metaJson`:
385
+
386
+ ```typescript
387
+ import { createSession, runSession } from "dialai";
388
+
389
+ // Pass metadata at session creation
390
+ const session = await createSession(machine, {
391
+ documentId: "doc-12345",
392
+ submittedBy: "user@example.com",
393
+ priority: "high",
394
+ });
395
+
396
+ // Access in strategy functions via ctx.metaJson
397
+ await registerProposer({
398
+ specialistId: "priority-aware",
399
+ machineName: "document-review",
400
+ strategyFn: async (ctx) => {
401
+ const priority = ctx.metaJson?.priority as string;
402
+
403
+ if (priority === "high") {
404
+ // Fast-track high priority items
405
+ return {
406
+ transitionName: "approve",
407
+ toState: ctx.transitions["approve"],
408
+ reasoning: "High priority item - fast-tracking approval",
409
+ };
410
+ }
411
+
412
+ const [name, target] = Object.entries(ctx.transitions)[0];
413
+ return { transitionName: name, toState: target, reasoning: "Standard processing" };
414
+ },
415
+ });
416
+ ```
417
+
418
+ ## 10. Testing Patterns
419
+
420
+ ### Basic vitest Setup
421
+
422
+ ```typescript
423
+ import { clear, runSession, createSession, registerProposer, registerArbiter } from "dialai";
424
+ import { describe, it, beforeEach, expect } from "vitest";
425
+ import type { MachineDefinition } from "dialai";
426
+
427
+ const machine: MachineDefinition = {
428
+ machineName: "test-workflow",
429
+ initialState: "start",
430
+ goalState: "end",
431
+ states: {
432
+ start: {
433
+ prompt: "Begin?",
434
+ transitions: { proceed: "middle", skip: "end" },
435
+ },
436
+ middle: {
437
+ prompt: "Continue?",
438
+ transitions: { finish: "end" },
439
+ },
440
+ end: {},
441
+ },
442
+ };
443
+
444
+ describe("test-workflow", () => {
445
+ beforeEach(async () => {
446
+ await clear();
447
+ });
448
+
449
+ it("reaches goal state", async () => {
450
+ const session = await runSession(machine);
451
+ expect(session.currentState).toBe("end");
452
+ });
453
+
454
+ it("records transition history", async () => {
455
+ const session = await runSession(machine);
456
+ expect(session.history.length).toBeGreaterThan(0);
457
+ expect(session.history.every((h) => h.transitionName)).toBe(true);
458
+ });
459
+ });
460
+ ```
461
+
462
+ ### Test a Specific Transition
463
+
464
+ ```typescript
465
+ it("takes the skip transition when configured", async () => {
466
+ await clear();
467
+
468
+ await registerProposer({
469
+ specialistId: "skipper",
470
+ machineName: "test-workflow",
471
+ strategyFn: async (ctx) => ({
472
+ transitionName: "skip",
473
+ toState: ctx.transitions["skip"],
474
+ reasoning: "Skipping to end",
475
+ }),
476
+ });
477
+
478
+ const session = await runSession(machine);
479
+ expect(session.history).toHaveLength(1);
480
+ expect(session.history[0].transitionName).toBe("skip");
481
+ expect(session.currentState).toBe("end");
482
+ });
483
+ ```
484
+
485
+ ### Test Multi-Step Path
486
+
487
+ ```typescript
488
+ it("follows the full path through middle", async () => {
489
+ await clear();
490
+
491
+ await registerProposer({
492
+ specialistId: "full-path",
493
+ machineName: "test-workflow",
494
+ strategyFn: async (ctx) => {
495
+ if (ctx.currentState === "start") {
496
+ return {
497
+ transitionName: "proceed",
498
+ toState: ctx.transitions["proceed"],
499
+ reasoning: "Going through middle",
500
+ };
501
+ }
502
+ return {
503
+ transitionName: "finish",
504
+ toState: ctx.transitions["finish"],
505
+ reasoning: "Finishing up",
506
+ };
507
+ },
508
+ });
509
+
510
+ const session = await runSession(machine);
511
+ expect(session.history).toHaveLength(2);
512
+ expect(session.history[0].transitionName).toBe("proceed");
513
+ expect(session.history[1].transitionName).toBe("finish");
514
+ });
515
+ ```
516
+
517
+ ## 11. Anti-Patterns
518
+
519
+ ### Goal state with transitions
520
+
521
+ The goal state should have no transitions. Adding transitions to the goal state means the session will never be considered terminal:
522
+
523
+ ```typescript
524
+ // WRONG
525
+ states: {
526
+ done: {
527
+ transitions: { restart: "start" }, // goal state should have no transitions
528
+ },
529
+ }
530
+
531
+ // RIGHT
532
+ states: {
533
+ done: {}, // goal state is empty
534
+ }
535
+ ```
536
+
537
+ ### Mixing execution modes
538
+
539
+ Each specialist must have exactly one execution mode. Combining them causes a registration error:
540
+
541
+ ```typescript
542
+ // WRONG - strategyFn + strategyFnName
543
+ await registerProposer({
544
+ specialistId: "confused",
545
+ machineName: "my-machine",
546
+ strategyFn: async (ctx) => ({ ... }),
547
+ strategyFnName: "firstAvailable", // ERROR: two execution modes
548
+ });
549
+
550
+ // WRONG - strategyFn + modelId
551
+ await registerProposer({
552
+ specialistId: "confused",
553
+ machineName: "my-machine",
554
+ strategyFn: async (ctx) => ({ ... }),
555
+ modelId: "anthropic/claude-sonnet-4", // ERROR: modelId only for contextFn/contextWebhookUrl
556
+ });
557
+ ```
558
+
559
+ ### Forgetting `clear()` in tests
560
+
561
+ Without `clear()`, specialists and sessions from previous tests leak into the next test:
562
+
563
+ ```typescript
564
+ // WRONG
565
+ describe("my tests", () => {
566
+ it("test 1", async () => {
567
+ await registerProposer({ specialistId: "bot", ... });
568
+ // ...
569
+ });
570
+
571
+ it("test 2", async () => {
572
+ // ERROR: "Specialist already exists: bot"
573
+ await registerProposer({ specialistId: "bot", ... });
574
+ });
575
+ });
576
+
577
+ // RIGHT
578
+ describe("my tests", () => {
579
+ beforeEach(async () => {
580
+ await clear();
581
+ });
582
+ // ...
583
+ });
584
+ ```
585
+
586
+ ### `contextFn` without `modelId`
587
+
588
+ When using LLM mode, both `contextFn` and `modelId` are required:
589
+
590
+ ```typescript
591
+ // WRONG
592
+ await registerProposer({
593
+ specialistId: "llm-bot",
594
+ machineName: "my-machine",
595
+ contextFn: async (ctx) => "some prompt",
596
+ // ERROR: contextFn requires modelId
597
+ });
598
+
599
+ // RIGHT
600
+ await registerProposer({
601
+ specialistId: "llm-bot",
602
+ machineName: "my-machine",
603
+ contextFn: async (ctx) => "some prompt",
604
+ modelId: "anthropic/claude-sonnet-4",
605
+ });
606
+ ```
607
+
608
+ ### Webhook without `webhookTokenName`
609
+
610
+ Webhook URLs require authentication:
611
+
612
+ ```typescript
613
+ // WRONG
614
+ await registerProposer({
615
+ specialistId: "webhook-bot",
616
+ machineName: "my-machine",
617
+ strategyWebhookUrl: "https://api.example.com/propose",
618
+ // ERROR: webhookTokenName required
619
+ });
620
+
621
+ // RIGHT
622
+ await registerProposer({
623
+ specialistId: "webhook-bot",
624
+ machineName: "my-machine",
625
+ strategyWebhookUrl: "https://api.example.com/propose",
626
+ webhookTokenName: "MY_API_TOKEN",
627
+ });
628
+ ```