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,401 @@
1
+ ---
2
+ name: dial-machine
3
+ description: >
4
+ This skill should be used when the user asks to "implement a dial machine",
5
+ "create a dial workflow", "add dial to my project", "run a dial machine",
6
+ "set up dialai", "configure specialists", "build a state machine with AI and
7
+ human decision-makers", or mentions dialai, DIAL framework, progressive collapse,
8
+ proposers and arbiters, or alignment-based consensus in a consumer project.
9
+ ---
10
+
11
+ # DIAL Machine Skill
12
+
13
+ DIAL (Dynamic Integration between AI and Labor) is a TypeScript framework for coordinating AI and human specialists making decisions within state machines. Specialists are either **proposers** (suggest state transitions) or **arbiters** (evaluate consensus among proposals). The framework tracks alignment scores between AI and human decisions, enabling progressive collapse — high-alignment specialists eventually run autonomously without human oversight.
14
+
15
+ For full API types and function signatures, see `references/api-reference.md`. For copy-paste-ready machine patterns, see `references/patterns.md`.
16
+
17
+ ## Project Setup
18
+
19
+ 1. Check if `dialai` is already in `package.json` dependencies. If not, install it:
20
+
21
+ ```bash
22
+ npm install dialai
23
+ ```
24
+
25
+ 2. Confirm the project has `"type": "module"` in `package.json`. DIAL is ESM-only.
26
+
27
+ 3. Import from the top-level package:
28
+
29
+ ```typescript
30
+ import { runSession, createSession, registerProposer, registerArbiter } from "dialai";
31
+ import type { MachineDefinition, ProposerContext, ArbiterContext } from "dialai";
32
+ ```
33
+
34
+ ## Define a Machine
35
+
36
+ A `MachineDefinition` requires four fields:
37
+
38
+ | Field | Type | Description |
39
+ |---|---|---|
40
+ | `machineName` | `string` | Unique identifier for this machine type |
41
+ | `initialState` | `string` | State where sessions start |
42
+ | `goalState` | `string` | Rest state where the session is headed (must have no transitions) |
43
+ | `states` | `Record<string, StateDefinition>` | Map of state names to definitions |
44
+
45
+ Optional fields: `specialists` (array of `SpecialistDefinition`), `consensusThreshold` (number, 0-1).
46
+
47
+ Each `StateDefinition` has:
48
+ - `prompt` (string) — the decision question for specialists in this state
49
+ - `transitions` (Record<string, string>) — map of transition names to target states
50
+ - `consensusThreshold` (number) — override threshold for this state
51
+ - `specialists` (SpecialistDefinition[]) — per-state specialist declarations
52
+
53
+ ### JSON Example
54
+
55
+ ```json
56
+ {
57
+ "machineName": "document-review",
58
+ "initialState": "draft",
59
+ "goalState": "published",
60
+ "states": {
61
+ "draft": {
62
+ "prompt": "Review the document. Approve or request changes?",
63
+ "transitions": {
64
+ "approve": "published",
65
+ "request_changes": "revision"
66
+ }
67
+ },
68
+ "revision": {
69
+ "prompt": "Changes made. Approve now?",
70
+ "transitions": {
71
+ "approve": "published",
72
+ "request_changes": "revision"
73
+ }
74
+ },
75
+ "published": {}
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### TypeScript Example
81
+
82
+ ```typescript
83
+ import type { MachineDefinition } from "dialai";
84
+
85
+ const machine: MachineDefinition = {
86
+ machineName: "document-review",
87
+ initialState: "draft",
88
+ goalState: "published",
89
+ states: {
90
+ draft: {
91
+ prompt: "Review the document. Approve or request changes?",
92
+ transitions: {
93
+ approve: "published",
94
+ request_changes: "revision",
95
+ },
96
+ },
97
+ revision: {
98
+ prompt: "Changes made. Approve now?",
99
+ transitions: {
100
+ approve: "published",
101
+ request_changes: "revision",
102
+ },
103
+ },
104
+ published: {},
105
+ },
106
+ };
107
+ ```
108
+
109
+ ## Configure Specialists
110
+
111
+ Specialists have two roles:
112
+ - **Proposer** — suggests which transition to take (`role: "proposer"`)
113
+ - **Arbiter** — evaluates proposals and determines consensus (`role: "arbiter"`)
114
+
115
+ ### Attachment Methods
116
+
117
+ **1. Machine-level JSON** — embedded in the machine definition:
118
+
119
+ ```json
120
+ {
121
+ "machineName": "my-machine",
122
+ "initialState": "start",
123
+ "goalState": "end",
124
+ "specialists": [
125
+ { "role": "proposer", "specialistId": "ai-1", "strategyFnName": "firstAvailable" },
126
+ { "role": "arbiter", "specialistId": "judge", "strategyFnName": "firstProposal" }
127
+ ],
128
+ "states": {
129
+ "start": {
130
+ "prompt": "Proceed?",
131
+ "transitions": { "go": "end" }
132
+ },
133
+ "end": {}
134
+ }
135
+ }
136
+ ```
137
+
138
+ **2. Per-state** — different specialists for different states:
139
+
140
+ ```json
141
+ {
142
+ "states": {
143
+ "triage": {
144
+ "prompt": "How should we handle this?",
145
+ "transitions": { "escalate": "review", "resolve": "done" },
146
+ "specialists": [
147
+ { "role": "proposer", "specialistId": "triage-bot", "strategyFnName": "firstAvailable" },
148
+ { "role": "arbiter", "specialistId": "triage-arbiter", "strategyFnName": "firstProposal" }
149
+ ]
150
+ },
151
+ "review": {
152
+ "prompt": "Senior review complete?",
153
+ "transitions": { "approve": "done" },
154
+ "specialists": [
155
+ { "role": "proposer", "specialistId": "senior-bot", "strategyFnName": "firstAvailable" },
156
+ { "role": "arbiter", "specialistId": "senior-arbiter", "strategyFnName": "alignmentMargin" }
157
+ ]
158
+ }
159
+ }
160
+ }
161
+ ```
162
+
163
+ **3. Programmatic** — registered via API calls:
164
+
165
+ ```typescript
166
+ import { registerProposer, registerArbiter } from "dialai";
167
+
168
+ await registerProposer({
169
+ specialistId: "ai-reviewer",
170
+ machineName: "document-review",
171
+ strategyFnName: "firstAvailable",
172
+ });
173
+
174
+ await registerArbiter({
175
+ specialistId: "consensus-judge",
176
+ machineName: "document-review",
177
+ strategyFnName: "alignmentMargin",
178
+ threshold: 0.6,
179
+ });
180
+ ```
181
+
182
+ ### Built-in Strategies
183
+
184
+ | Strategy | Role | Behavior |
185
+ |---|---|---|
186
+ | `firstAvailable` | proposer | Returns the first transition in the map |
187
+ | `lastAvailable` | proposer | Returns the last transition in the map |
188
+ | `random` | proposer | Returns a random transition |
189
+ | `firstProposal` | arbiter | Accepts the first proposal by timestamp |
190
+ | `alignmentMargin` | arbiter | Alignment-weighted margin consensus (see below) |
191
+
192
+ **`alignmentMargin` algorithm**: Groups proposals by transition name, scores each group by summing proposer alignment scores, computes `margin = (leader - runnerUp) / totalAlignment`. Consensus when `margin >= threshold`. Single proposal auto-approves. Cold start (all alignment = 0) requires human input.
193
+
194
+ ### Execution Modes
195
+
196
+ Each specialist must have exactly one execution mode:
197
+
198
+ | Mode | Fields | Description |
199
+ |---|---|---|
200
+ | Built-in strategy | `strategyFnName` | Name of a built-in strategy |
201
+ | Custom function | `strategyFn` | Local async function |
202
+ | Webhook | `strategyWebhookUrl` + `webhookTokenName` | External HTTP endpoint |
203
+ | LLM (local context) | `contextFn` + `modelId` | You provide context, DIAL calls the LLM |
204
+ | LLM (webhook context) | `contextWebhookUrl` + `modelId` + `webhookTokenName` | Webhook provides context, DIAL calls the LLM |
205
+
206
+ Arbiters support the first three modes only (no LLM modes).
207
+
208
+ ## Run the Machine
209
+
210
+ ### Method 1: CLI
211
+
212
+ ```bash
213
+ npx dialai machine.json
214
+ ```
215
+
216
+ This registers default specialists (`firstAvailable` proposer, `firstProposal` arbiter) if none are specified in the JSON, and runs the machine to completion.
217
+
218
+ ### Method 2: `runSession()` One-Liner
219
+
220
+ ```typescript
221
+ import { runSession } from "dialai";
222
+
223
+ const session = await runSession(machine);
224
+ console.log(session.currentState); // goalState
225
+ console.log(session.history); // TransitionRecord[]
226
+ ```
227
+
228
+ `runSession` creates a session, registers specialists from the machine definition (or defaults), and loops `tick()` until the session reaches `goalState` or needs human intervention.
229
+
230
+ ### Method 3: Manual Decision Cycle
231
+
232
+ ```typescript
233
+ import {
234
+ createSession,
235
+ getSession,
236
+ registerProposer,
237
+ registerArbiter,
238
+ submitProposal,
239
+ submitArbitration,
240
+ } from "dialai";
241
+
242
+ // 1. Create session
243
+ const session = await createSession(machine);
244
+
245
+ // 2. Register specialists
246
+ await registerProposer({
247
+ specialistId: "ai-1",
248
+ machineName: machine.machineName,
249
+ strategyFnName: "firstAvailable",
250
+ });
251
+
252
+ await registerArbiter({
253
+ specialistId: "arbiter-1",
254
+ machineName: machine.machineName,
255
+ strategyFnName: "firstProposal",
256
+ });
257
+
258
+ // 3. Submit proposals (omit transitionName to invoke the registered strategy)
259
+ await submitProposal({
260
+ sessionId: session.sessionId,
261
+ specialistId: "ai-1",
262
+ });
263
+
264
+ // 4. Submit arbitration (evaluates consensus, executes transition if reached)
265
+ const result = await submitArbitration({
266
+ sessionId: session.sessionId,
267
+ });
268
+
269
+ console.log(result.executed); // true
270
+ console.log(result.toState); // next state
271
+
272
+ // 5. Fetch updated session
273
+ const updated = await getSession(session.sessionId);
274
+ console.log(updated.currentState);
275
+ ```
276
+
277
+ ## Custom Strategy Functions
278
+
279
+ ### Proposer `strategyFn`
280
+
281
+ Receives a `ProposerContext` and returns `{ transitionName, toState, reasoning }`:
282
+
283
+ ```typescript
284
+ await registerProposer({
285
+ specialistId: "smart-proposer",
286
+ machineName: "my-machine",
287
+ strategyFn: async (ctx: ProposerContext) => {
288
+ // ctx.currentState — current state name
289
+ // ctx.prompt — decision prompt for this state
290
+ // ctx.transitions — Record<string, string> of available transitions
291
+ // ctx.history — TransitionRecord[] of previous transitions
292
+ // ctx.metaJson — session-level metadata
293
+
294
+ const [name, target] = Object.entries(ctx.transitions)[0];
295
+ return {
296
+ transitionName: name,
297
+ toState: target,
298
+ reasoning: "Chose first transition based on custom logic",
299
+ };
300
+ },
301
+ });
302
+ ```
303
+
304
+ ### Arbiter `strategyFn`
305
+
306
+ Receives an `ArbiterContext` and returns `{ consensusReached, winningProposalId?, reasoning }`:
307
+
308
+ ```typescript
309
+ await registerArbiter({
310
+ specialistId: "custom-arbiter",
311
+ machineName: "my-machine",
312
+ strategyFn: async (ctx: ArbiterContext) => {
313
+ // ctx.proposals — Proposal[] in this round
314
+ // ctx.alignmentScores — Record<string, number> by specialistId
315
+ // ctx.threshold — configured threshold
316
+ // ctx.currentState — current state name
317
+ // ctx.prompt — decision prompt
318
+ // ctx.history — TransitionRecord[]
319
+ // ctx.metaJson — session-level metadata
320
+
321
+ if (ctx.proposals.length === 0) {
322
+ return { consensusReached: false, reasoning: "No proposals" };
323
+ }
324
+
325
+ // Simple: pick the first proposal
326
+ return {
327
+ consensusReached: true,
328
+ winningProposalId: ctx.proposals[0].proposalId,
329
+ reasoning: "Custom arbiter selected first proposal",
330
+ };
331
+ },
332
+ });
333
+ ```
334
+
335
+ ### LLM Mode
336
+
337
+ Use `contextFn` + `modelId` to let DIAL call an LLM with your context:
338
+
339
+ ```typescript
340
+ await registerProposer({
341
+ specialistId: "llm-proposer",
342
+ machineName: "my-machine",
343
+ contextFn: async (ctx: ProposerContext) => {
344
+ // Return a string prompt for the LLM
345
+ return `You are reviewing a document in state "${ctx.currentState}".
346
+ The prompt is: ${ctx.prompt}
347
+ Available transitions: ${JSON.stringify(ctx.transitions)}
348
+ Previous history: ${ctx.history.map(h => h.transitionName).join(" -> ")}
349
+
350
+ Choose the best transition and explain why.`;
351
+ },
352
+ modelId: "anthropic/claude-sonnet-4",
353
+ });
354
+ ```
355
+
356
+ Set `OPENROUTER_API_TOKEN` in the environment (or `DIALAI_LLM_BASE_URL` for a different OpenAI-compatible provider).
357
+
358
+ ## Testing
359
+
360
+ Use vitest with `clear()` to reset state between tests:
361
+
362
+ ```typescript
363
+ import { clear, runSession } from "dialai";
364
+ import { describe, it, beforeEach, expect } from "vitest";
365
+ import type { MachineDefinition } from "dialai";
366
+
367
+ const machine: MachineDefinition = {
368
+ machineName: "test-machine",
369
+ initialState: "start",
370
+ goalState: "end",
371
+ states: {
372
+ start: {
373
+ prompt: "Proceed?",
374
+ transitions: { go: "end" },
375
+ },
376
+ end: {},
377
+ },
378
+ };
379
+
380
+ describe("my machine", () => {
381
+ beforeEach(async () => {
382
+ await clear(); // Reset all sessions, specialists, proposals
383
+ });
384
+
385
+ it("reaches goal state", async () => {
386
+ const session = await runSession(machine);
387
+ expect(session.currentState).toBe("end");
388
+ });
389
+
390
+ it("records transition history", async () => {
391
+ const session = await runSession(machine);
392
+ expect(session.history).toHaveLength(1);
393
+ expect(session.history[0].transitionName).toBe("go");
394
+ });
395
+ });
396
+ ```
397
+
398
+ ## Additional Resources
399
+
400
+ - `references/api-reference.md` — Full API types, function signatures, built-in strategies, environment variables
401
+ - `references/patterns.md` — Copy-paste-ready machine patterns (minimal, pipeline, branching, human-in-the-loop, multi-agent, LLM-powered, testing, anti-patterns)