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.
- package/.claude/skills/dial-machine/SKILL.md +401 -0
- package/.claude/skills/dial-machine/references/api-reference.md +515 -0
- package/.claude/skills/dial-machine/references/patterns.md +628 -0
- package/.claude/skills/spec-for-ralph/SKILL.md +542 -0
- package/.claude/specs/llm-audit-log.md +280 -0
- package/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/dialai/api.d.ts +2 -6
- package/dist/dialai/api.d.ts.map +1 -1
- package/dist/dialai/api.js +22 -6
- package/dist/dialai/api.js.map +1 -1
- package/dist/dialai/llm.d.ts +6 -4
- package/dist/dialai/llm.d.ts.map +1 -1
- package/dist/dialai/llm.js +96 -31
- package/dist/dialai/llm.js.map +1 -1
- package/dist/dialai/migrations/002-llm-audit-log.d.ts +8 -0
- package/dist/dialai/migrations/002-llm-audit-log.d.ts.map +1 -0
- package/dist/dialai/migrations/002-llm-audit-log.js +41 -0
- package/dist/dialai/migrations/002-llm-audit-log.js.map +1 -0
- package/dist/dialai/migrations/migrate.d.ts.map +1 -1
- package/dist/dialai/migrations/migrate.js +2 -0
- package/dist/dialai/migrations/migrate.js.map +1 -1
- package/dist/dialai/store-memory.d.ts.map +1 -1
- package/dist/dialai/store-memory.js +22 -0
- package/dist/dialai/store-memory.js.map +1 -1
- package/dist/dialai/store-postgres.d.ts.map +1 -1
- package/dist/dialai/store-postgres.js +54 -1
- package/dist/dialai/store-postgres.js.map +1 -1
- package/dist/dialai/store.d.ts +3 -1
- package/dist/dialai/store.d.ts.map +1 -1
- package/dist/dialai/store.js.map +1 -1
- package/dist/dialai/types.d.ts +54 -0
- package/dist/dialai/types.d.ts.map +1 -1
- 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
|
+
```
|