ocpipe 0.1.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/README.md +334 -0
- package/agent.ts +176 -0
- package/example/correction.ts +85 -0
- package/example/index.ts +31 -0
- package/example/module.ts +20 -0
- package/example/signature.ts +18 -0
- package/index.ts +127 -0
- package/module.ts +50 -0
- package/package.json +48 -0
- package/parsing.ts +865 -0
- package/pipeline.ts +213 -0
- package/predict.ts +271 -0
- package/signature.ts +97 -0
- package/state.ts +39 -0
- package/testing.ts +180 -0
- package/types.ts +260 -0
package/README.md
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# DSTS
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<h3>Declarative Self-Improving TypeScript</h3>
|
|
5
|
+
<p>A DSPy-inspired SDK for building LLM workflow pipelines with <a href="https://opencode.ai">OpenCode</a>.</p>
|
|
6
|
+
<p>
|
|
7
|
+
<a href="https://github.com/s4wave/dsts">GitHub</a> |
|
|
8
|
+
<a href="https://github.com/s4wave/dsts/blob/main/GETTING_STARTED.md">Getting Started</a> |
|
|
9
|
+
<a href="https://github.com/s4wave/dsts/blob/main/LICENSE">MIT License</a>
|
|
10
|
+
</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div align="center">
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Signature → Predict → Module → Pipeline
|
|
17
|
+
│ │ │ │
|
|
18
|
+
what execute compose orchestrate
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
DSTS separates the **what** (Signatures declare input/output contracts), the **how** (Modules compose predictors), and the **when** (Pipelines orchestrate execution). This separation enables clean composition, rich debugging, and maintainable LLM workflow code.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- **Type-safe signatures** - Define input/output contracts with Zod schemas
|
|
28
|
+
- **Automatic prompt generation** - Signatures become structured prompts
|
|
29
|
+
- **JSON output parsing** - Automatic extraction and validation of JSON responses
|
|
30
|
+
- **Session continuity** - Reuse OpenCode sessions across steps
|
|
31
|
+
- **Checkpointing** - Automatic state persistence after each step
|
|
32
|
+
- **Retry logic** - Configurable retries with parse error handling
|
|
33
|
+
- **Sub-pipelines** - Compose complex workflows from smaller pieces
|
|
34
|
+
- **Testing utilities** - Mock backends for unit testing
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { signature, field, SignatureModule, Pipeline, createBaseState } from 'dsts'
|
|
40
|
+
import { z } from 'zod'
|
|
41
|
+
|
|
42
|
+
// 1. Define a signature (the contract)
|
|
43
|
+
const ParseIntent = signature({
|
|
44
|
+
doc: 'Parse user intent from a natural language description.',
|
|
45
|
+
inputs: {
|
|
46
|
+
description: field.string('User description in natural language'),
|
|
47
|
+
},
|
|
48
|
+
outputs: {
|
|
49
|
+
intent: field.string('Parsed intent category'),
|
|
50
|
+
confidence: field.number('Confidence score 0-1'),
|
|
51
|
+
keywords: field.array(z.string(), 'Extracted keywords'),
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// 2. Create a module (the logic)
|
|
56
|
+
class IntentParser extends SignatureModule<typeof ParseIntent> {
|
|
57
|
+
constructor() {
|
|
58
|
+
super(ParseIntent)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async forward(input, ctx) {
|
|
62
|
+
const result = await this.predictor.execute(input, ctx)
|
|
63
|
+
return result.data // Full signature output
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. Run in a pipeline (the orchestration)
|
|
68
|
+
const pipeline = new Pipeline({
|
|
69
|
+
name: 'my-workflow',
|
|
70
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
71
|
+
defaultAgent: 'general',
|
|
72
|
+
checkpointDir: './ckpt',
|
|
73
|
+
logDir: './logs',
|
|
74
|
+
}, createBaseState)
|
|
75
|
+
|
|
76
|
+
const result = await pipeline.run(new IntentParser(), { description: 'Hello world' })
|
|
77
|
+
console.log(result.data.intent)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Core Concepts
|
|
81
|
+
|
|
82
|
+
### Signatures
|
|
83
|
+
|
|
84
|
+
A Signature declares **what** an LLM interaction does - its inputs, outputs, and purpose. This is separate from *how* it executes.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { signature, field } from 'dsts'
|
|
88
|
+
import { z } from 'zod'
|
|
89
|
+
|
|
90
|
+
const AnalyzeCode = signature({
|
|
91
|
+
doc: 'Analyze code for potential issues and improvements.',
|
|
92
|
+
inputs: {
|
|
93
|
+
code: field.string('Source code to analyze'),
|
|
94
|
+
language: field.enum(['typescript', 'python', 'rust'] as const),
|
|
95
|
+
},
|
|
96
|
+
outputs: {
|
|
97
|
+
issues: field.array(z.object({
|
|
98
|
+
severity: z.enum(['error', 'warning', 'info']),
|
|
99
|
+
message: z.string(),
|
|
100
|
+
line: z.number(),
|
|
101
|
+
}), 'List of issues found'),
|
|
102
|
+
suggestions: field.array(z.string(), 'Improvement suggestions'),
|
|
103
|
+
score: field.number('Code quality score 0-100'),
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Field helpers:**
|
|
109
|
+
- `field.string(desc?)` - String field
|
|
110
|
+
- `field.number(desc?)` - Number field
|
|
111
|
+
- `field.boolean(desc?)` - Boolean field
|
|
112
|
+
- `field.array(itemType, desc?)` - Array field
|
|
113
|
+
- `field.object(shape, desc?)` - Object field
|
|
114
|
+
- `field.enum(values, desc?)` - Enum field
|
|
115
|
+
- `field.optional(field)` - Optional wrapper
|
|
116
|
+
- `field.nullable(field)` - Nullable wrapper
|
|
117
|
+
- `field.custom(zodType, desc?)` - Custom Zod type
|
|
118
|
+
|
|
119
|
+
### Predict
|
|
120
|
+
|
|
121
|
+
`Predict` is the bridge between a Signature (the contract) and OpenCode (the execution). It handles prompt generation, response parsing, and validation.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { Predict } from 'dsts'
|
|
125
|
+
|
|
126
|
+
// Basic usage
|
|
127
|
+
const predict = new Predict(AnalyzeCode)
|
|
128
|
+
const result = await predict.execute({ code: '...', language: 'typescript' }, ctx)
|
|
129
|
+
|
|
130
|
+
// With configuration
|
|
131
|
+
const predict = new Predict(AnalyzeCode, {
|
|
132
|
+
agent: 'code-reviewer', // Override default agent
|
|
133
|
+
model: { providerID: 'anthropic', modelID: 'claude-opus-4-5' },
|
|
134
|
+
newSession: true, // Don't reuse existing session
|
|
135
|
+
template: (inputs) => `...`, // Custom prompt template
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Output format:**
|
|
140
|
+
|
|
141
|
+
The LLM is prompted to return a JSON object:
|
|
142
|
+
```
|
|
143
|
+
OUTPUT FORMAT:
|
|
144
|
+
Return a JSON object with EXACTLY these field names and types.
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"issues": <array<object{severity, message, line}>>, // List of issues found
|
|
149
|
+
"suggestions": <array<string>>, // Improvement suggestions
|
|
150
|
+
"score": <number> // Code quality score 0-100
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Module
|
|
156
|
+
|
|
157
|
+
A Module encapsulates a logical unit of work that may use one or more Predictors. Modules can call other Modules, enabling composition.
|
|
158
|
+
|
|
159
|
+
**SignatureModule** - For simple modules that wrap a single signature with pass-through types:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { SignatureModule } from 'dsts'
|
|
163
|
+
|
|
164
|
+
class IntentParser extends SignatureModule<typeof ParseIntent> {
|
|
165
|
+
constructor() {
|
|
166
|
+
super(ParseIntent)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async forward(input, ctx) {
|
|
170
|
+
const result = await this.predictor.execute(input, ctx)
|
|
171
|
+
return result.data // Types inferred from ParseIntent
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Module** - For complex modules with multiple predictors or transformed outputs:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { Module } from 'dsts'
|
|
180
|
+
|
|
181
|
+
class CodeAnalyzer extends Module<
|
|
182
|
+
{ code: string; language: string },
|
|
183
|
+
{ issues: Issue[]; score: number }
|
|
184
|
+
> {
|
|
185
|
+
private analyze = this.predict(AnalyzeCode)
|
|
186
|
+
private suggest = this.predict(SuggestFixes, { agent: 'code-fixer' })
|
|
187
|
+
|
|
188
|
+
async forward(input: { code: string; language: string }, ctx: ExecutionContext) {
|
|
189
|
+
// First, analyze the code
|
|
190
|
+
const analysis = await this.analyze.execute(input, ctx)
|
|
191
|
+
|
|
192
|
+
// If there are critical issues, get fix suggestions
|
|
193
|
+
if (analysis.data.issues.some(i => i.severity === 'error')) {
|
|
194
|
+
const fixes = await this.suggest.execute({
|
|
195
|
+
code: input.code,
|
|
196
|
+
issues: analysis.data.issues,
|
|
197
|
+
}, ctx)
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
issues: analysis.data.issues,
|
|
201
|
+
fixes: fixes.data.suggestions,
|
|
202
|
+
score: analysis.data.score,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
issues: analysis.data.issues,
|
|
208
|
+
score: analysis.data.score,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Pipeline
|
|
215
|
+
|
|
216
|
+
Pipeline is the top-level orchestrator. It manages execution context, state, checkpointing, logging, and retry logic.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import { Pipeline, createBaseState } from 'dsts'
|
|
220
|
+
|
|
221
|
+
// Create pipeline with configuration
|
|
222
|
+
const pipeline = new Pipeline({
|
|
223
|
+
name: 'code-review',
|
|
224
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
225
|
+
defaultAgent: 'general',
|
|
226
|
+
checkpointDir: './ckpt',
|
|
227
|
+
logDir: './logs',
|
|
228
|
+
retry: { maxAttempts: 2, onParseError: true },
|
|
229
|
+
timeoutSec: 300,
|
|
230
|
+
}, createBaseState)
|
|
231
|
+
|
|
232
|
+
// Run modules
|
|
233
|
+
const result = await pipeline.run(new CodeAnalyzer(), {
|
|
234
|
+
code: sourceCode,
|
|
235
|
+
language: 'typescript',
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// Run with step options
|
|
239
|
+
const result = await pipeline.run(new CodeAnalyzer(), input, {
|
|
240
|
+
name: 'analyze-main', // Custom step name
|
|
241
|
+
model: { providerID: 'anthropic', modelID: 'claude-opus-4-5' }, // Override model
|
|
242
|
+
newSession: true, // Fresh session
|
|
243
|
+
retry: { maxAttempts: 3 }, // Override retry
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Access state
|
|
247
|
+
console.log(pipeline.state.steps) // Completed steps
|
|
248
|
+
console.log(pipeline.getSessionId()) // Current OpenCode session
|
|
249
|
+
|
|
250
|
+
// Resume from checkpoint
|
|
251
|
+
const resumed = await Pipeline.loadCheckpoint(config, sessionId)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### State Management
|
|
255
|
+
|
|
256
|
+
DSTS automatically checkpoints state after each step:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import { createBaseState, extendBaseState } from 'dsts'
|
|
260
|
+
|
|
261
|
+
// Basic state
|
|
262
|
+
const state = createBaseState()
|
|
263
|
+
// { sessionId, startedAt, phase, steps, subPipelines }
|
|
264
|
+
|
|
265
|
+
// Extended state for your workflow
|
|
266
|
+
interface MyState extends BaseState {
|
|
267
|
+
inputPath: string
|
|
268
|
+
results: AnalysisResult[]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const pipeline = new Pipeline(config, () => ({
|
|
272
|
+
...createBaseState(),
|
|
273
|
+
inputPath: '/path/to/input',
|
|
274
|
+
results: [],
|
|
275
|
+
}))
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Testing
|
|
279
|
+
|
|
280
|
+
DSTS provides testing utilities for unit testing without hitting real LLMs:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
import { MockAgentBackend, createMockContext, generateMockOutputs } from 'dsts'
|
|
284
|
+
import { vi } from 'vitest'
|
|
285
|
+
|
|
286
|
+
// Create mock backend
|
|
287
|
+
const mock = new MockAgentBackend()
|
|
288
|
+
|
|
289
|
+
// Add mock responses
|
|
290
|
+
mock.addJsonResponse({
|
|
291
|
+
intent: 'greeting',
|
|
292
|
+
confidence: 0.95,
|
|
293
|
+
keywords: ['hello', 'world'],
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
// Mock the agent module
|
|
297
|
+
vi.mock('./agent.js', () => ({
|
|
298
|
+
runAgent: mock.createRunner(),
|
|
299
|
+
}))
|
|
300
|
+
|
|
301
|
+
// Create test context
|
|
302
|
+
const ctx = createMockContext({
|
|
303
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
// Auto-generate mock outputs from schema
|
|
307
|
+
const mockData = generateMockOutputs(ParseIntent.outputs)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Why Not ChainOfThought or ReAct?
|
|
311
|
+
|
|
312
|
+
Unlike DSPy, DSTS does not provide `ChainOfThought` or `ReAct` variants. This is intentional:
|
|
313
|
+
|
|
314
|
+
- **OpenCode agents already do chain-of-thought reasoning** - they think before acting
|
|
315
|
+
- **OpenCode agents already do ReAct** - they have access to tools and use them iteratively
|
|
316
|
+
- **Adding these would duplicate functionality** and create confusion
|
|
317
|
+
|
|
318
|
+
If you need tool access, configure your OpenCode agent appropriately. The agent handles the complexity; DSTS just structures the input/output contract.
|
|
319
|
+
|
|
320
|
+
## Requirements
|
|
321
|
+
|
|
322
|
+
- [Bun](https://bun.sh) runtime
|
|
323
|
+
- [OpenCode](https://opencode.ai) CLI installed and configured
|
|
324
|
+
- [Zod](https://zod.dev) for schema validation
|
|
325
|
+
|
|
326
|
+
## Installation
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
bun add dsts zod
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## License
|
|
333
|
+
|
|
334
|
+
MIT - see [LICENSE](https://github.com/s4wave/dsts/blob/main/LICENSE) for details.
|
package/agent.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSTS SDK OpenCode agent integration.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the OpenCode CLI for running LLM agents with session management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process'
|
|
8
|
+
import { mkdir } from 'fs/promises'
|
|
9
|
+
import { PROJECT_ROOT, TMP_DIR } from '../paths.js'
|
|
10
|
+
import type { RunAgentOptions, RunAgentResult } from './types.js'
|
|
11
|
+
|
|
12
|
+
/** runAgent executes an OpenCode agent with a prompt, streaming output in real-time. */
|
|
13
|
+
export async function runAgent(
|
|
14
|
+
options: RunAgentOptions,
|
|
15
|
+
): Promise<RunAgentResult> {
|
|
16
|
+
const { prompt, agent, model, sessionId, timeoutSec = 300 } = options
|
|
17
|
+
|
|
18
|
+
const modelStr = `${model.providerID}/${model.modelID}`
|
|
19
|
+
const sessionInfo = sessionId ? `[session:${sessionId}]` : '[new session]'
|
|
20
|
+
const promptPreview = prompt.slice(0, 50).replace(/\n/g, ' ')
|
|
21
|
+
|
|
22
|
+
console.error(
|
|
23
|
+
`\n>>> OpenCode [${agent}] [${modelStr}] ${sessionInfo}: ${promptPreview}...`,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const args = ['run', '--format', 'default', '--agent', agent, '--model', modelStr]
|
|
27
|
+
|
|
28
|
+
if (sessionId) {
|
|
29
|
+
args.push('--session', sessionId)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const proc = spawn('opencode', args, {
|
|
34
|
+
cwd: PROJECT_ROOT,
|
|
35
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
let newSessionId = sessionId || ''
|
|
39
|
+
const stdoutChunks: string[] = []
|
|
40
|
+
|
|
41
|
+
// Stream stderr in real-time (OpenCode progress output)
|
|
42
|
+
proc.stderr.on('data', (data: Buffer) => {
|
|
43
|
+
const text = data.toString()
|
|
44
|
+
|
|
45
|
+
// Parse session ID from output
|
|
46
|
+
for (const line of text.split('\n')) {
|
|
47
|
+
if (line.startsWith('[session:')) {
|
|
48
|
+
newSessionId = line.trim().slice(9, -1)
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
// Filter noise
|
|
52
|
+
if (line.includes('baseline-browser-mapping')) continue
|
|
53
|
+
if (line.startsWith('$ bun run')) continue
|
|
54
|
+
if (line.trim()) {
|
|
55
|
+
process.stderr.write(line + '\n')
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Collect stdout
|
|
61
|
+
proc.stdout.on('data', (data: Buffer) => {
|
|
62
|
+
const text = data.toString()
|
|
63
|
+
stdoutChunks.push(text)
|
|
64
|
+
process.stderr.write(text)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
// Send prompt to stdin
|
|
68
|
+
proc.stdin.write(prompt)
|
|
69
|
+
proc.stdin.end()
|
|
70
|
+
|
|
71
|
+
// Timeout handling
|
|
72
|
+
const timeout = setTimeout(() => {
|
|
73
|
+
proc.kill()
|
|
74
|
+
reject(new Error(`Timeout after ${timeoutSec}s`))
|
|
75
|
+
}, timeoutSec * 1000)
|
|
76
|
+
|
|
77
|
+
proc.on('close', async (code) => {
|
|
78
|
+
clearTimeout(timeout)
|
|
79
|
+
|
|
80
|
+
if (code !== 0) {
|
|
81
|
+
reject(new Error(`OpenCode exited with code ${code}`))
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Export session to get structured response
|
|
86
|
+
let response = stdoutChunks.join('').trim()
|
|
87
|
+
|
|
88
|
+
if (newSessionId) {
|
|
89
|
+
const exported = await exportSession(newSessionId)
|
|
90
|
+
if (exported) {
|
|
91
|
+
response = exported
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sessionStr = newSessionId || 'none'
|
|
96
|
+
console.error(
|
|
97
|
+
`<<< OpenCode done (${response.length} chars) [session:${sessionStr}]`,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
resolve({
|
|
101
|
+
text: response,
|
|
102
|
+
sessionId: newSessionId,
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
proc.on('error', (err) => {
|
|
107
|
+
clearTimeout(timeout)
|
|
108
|
+
reject(err)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** exportSession exports a session and extracts assistant text responses. */
|
|
114
|
+
async function exportSession(sessionId: string): Promise<string | null> {
|
|
115
|
+
const tmpPath = `${TMP_DIR}/opencode_export_${Date.now()}.json`
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await mkdir(TMP_DIR, { recursive: true })
|
|
119
|
+
const proc = Bun.spawn(
|
|
120
|
+
[
|
|
121
|
+
'opencode',
|
|
122
|
+
'session',
|
|
123
|
+
'export',
|
|
124
|
+
sessionId,
|
|
125
|
+
'--format',
|
|
126
|
+
'json',
|
|
127
|
+
'-o',
|
|
128
|
+
tmpPath,
|
|
129
|
+
],
|
|
130
|
+
{
|
|
131
|
+
cwd: PROJECT_ROOT,
|
|
132
|
+
stdout: 'pipe',
|
|
133
|
+
stderr: 'pipe',
|
|
134
|
+
},
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
await proc.exited
|
|
138
|
+
|
|
139
|
+
const file = Bun.file(tmpPath)
|
|
140
|
+
if (!(await file.exists())) return null
|
|
141
|
+
|
|
142
|
+
const data = (await file.json()) as {
|
|
143
|
+
messages?: Array<{
|
|
144
|
+
info?: { role?: string }
|
|
145
|
+
parts?: Array<{ type?: string; text?: string }>
|
|
146
|
+
}>
|
|
147
|
+
}
|
|
148
|
+
await Bun.write(tmpPath, '') // Clean up
|
|
149
|
+
|
|
150
|
+
// Extract all assistant text parts
|
|
151
|
+
const messages = data.messages || []
|
|
152
|
+
const textParts: string[] = []
|
|
153
|
+
|
|
154
|
+
for (const msg of messages) {
|
|
155
|
+
if (msg.info?.role === 'assistant') {
|
|
156
|
+
for (const part of msg.parts || []) {
|
|
157
|
+
if (part.type === 'text' && part.text) {
|
|
158
|
+
textParts.push(part.text)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return textParts.length > 0 ? textParts.join('\n') : null
|
|
165
|
+
} catch {
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** logStep logs a step header for workflow progress. */
|
|
171
|
+
export function logStep(step: number, title: string, detail = ''): void {
|
|
172
|
+
const detailStr = detail ? ` (${detail})` : ''
|
|
173
|
+
console.log(`\n${'='.repeat(60)}`)
|
|
174
|
+
console.log(`STEP ${step}: ${title}${detailStr}`)
|
|
175
|
+
console.log('='.repeat(60))
|
|
176
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-correction example.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates DSTS's automatic schema correction.
|
|
5
|
+
* This example uses a schema with specific field names that LLMs
|
|
6
|
+
* sometimes get wrong (e.g., "type" instead of "issue_type").
|
|
7
|
+
*
|
|
8
|
+
* DSTS supports two correction methods:
|
|
9
|
+
* - 'json-patch' (default): RFC 6902 JSON Patch, no external dependencies
|
|
10
|
+
* - 'jq': jq-style expressions, requires jq binary installed
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* bun run example/correction.ts # Uses default (json-patch)
|
|
14
|
+
* bun run example/correction.ts --jq # Uses jq method
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { z } from 'zod'
|
|
18
|
+
import { Pipeline, createBaseState, signature, field, SignatureModule } from '../index.js'
|
|
19
|
+
import type { CorrectionMethod, ExecutionContext } from '../types.js'
|
|
20
|
+
|
|
21
|
+
// A signature with field names that LLMs often get wrong
|
|
22
|
+
const AnalyzeIssue = signature({
|
|
23
|
+
doc: `Analyze the given code issue and categorize it.
|
|
24
|
+
|
|
25
|
+
IMPORTANT: Use the EXACT field names specified in the schema.`,
|
|
26
|
+
inputs: {
|
|
27
|
+
description: field.string('Description of the code issue'),
|
|
28
|
+
},
|
|
29
|
+
outputs: {
|
|
30
|
+
// LLMs often return "type" instead of "issue_type"
|
|
31
|
+
issue_type: field.enum(['bug', 'feature', 'refactor', 'docs'] as const, 'Category of the issue'),
|
|
32
|
+
// LLMs often return "priority" instead of "severity"
|
|
33
|
+
severity: field.enum(['low', 'medium', 'high', 'critical'] as const, 'How severe is the issue'),
|
|
34
|
+
// LLMs often return "description" or "reason" instead of "explanation"
|
|
35
|
+
explanation: field.string('Detailed explanation of the issue'),
|
|
36
|
+
// LLMs often return just "tags" or "labels"
|
|
37
|
+
suggested_tags: field.array(z.string(), 'Tags to apply to this issue'),
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
class IssueAnalyzer extends SignatureModule<typeof AnalyzeIssue> {
|
|
42
|
+
constructor(method: CorrectionMethod = 'json-patch') {
|
|
43
|
+
super(AnalyzeIssue, {
|
|
44
|
+
correction: { method },
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async forward(input: { description: string }, ctx: ExecutionContext) {
|
|
49
|
+
const result = await this.predictor.execute(input, ctx)
|
|
50
|
+
return result.data
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
// Check for --jq flag
|
|
56
|
+
const method: CorrectionMethod = process.argv.includes('--jq') ? 'jq' : 'json-patch'
|
|
57
|
+
|
|
58
|
+
const pipeline = new Pipeline(
|
|
59
|
+
{
|
|
60
|
+
name: 'correction-demo',
|
|
61
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-haiku-4-5' },
|
|
62
|
+
defaultAgent: 'code',
|
|
63
|
+
checkpointDir: './ckpt',
|
|
64
|
+
logDir: './logs',
|
|
65
|
+
},
|
|
66
|
+
createBaseState,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
console.log('=== Auto-Correction Demo ===')
|
|
70
|
+
console.log(`Correction method: ${method}`)
|
|
71
|
+
console.log('This example uses field names that LLMs often get wrong.')
|
|
72
|
+
console.log('Watch the correction rounds fix schema mismatches.\n')
|
|
73
|
+
|
|
74
|
+
const result = await pipeline.run(new IssueAnalyzer(method), {
|
|
75
|
+
description: 'The login button does not respond when clicked on mobile devices',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
console.log('\n=== Final Result ===')
|
|
79
|
+
console.log(`Issue Type: ${result.data.issue_type}`)
|
|
80
|
+
console.log(`Severity: ${result.data.severity}`)
|
|
81
|
+
console.log(`Explanation: ${result.data.explanation}`)
|
|
82
|
+
console.log(`Tags: ${result.data.suggested_tags.join(', ')}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main().catch(console.error)
|
package/example/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hello World example runner.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates running a DSTS module in a pipeline.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Pipeline, createBaseState } from '../index.js'
|
|
8
|
+
import { Greeter } from './module.js'
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
// Create a pipeline with configuration
|
|
12
|
+
const pipeline = new Pipeline(
|
|
13
|
+
{
|
|
14
|
+
name: 'hello-world',
|
|
15
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-haiku-4-5' },
|
|
16
|
+
defaultAgent: 'code',
|
|
17
|
+
checkpointDir: './ckpt',
|
|
18
|
+
logDir: './logs',
|
|
19
|
+
},
|
|
20
|
+
createBaseState,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
// Run the greeter module
|
|
24
|
+
const result = await pipeline.run(new Greeter(), { name: 'World' })
|
|
25
|
+
|
|
26
|
+
console.log('\n=== Result ===')
|
|
27
|
+
console.log(`Greeting: ${result.data.greeting}`)
|
|
28
|
+
console.log(`Emoji: ${result.data.emoji}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
main().catch(console.error)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hello World module.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the Greet signature with execution logic.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { SignatureModule } from '../index.js'
|
|
8
|
+
import type { ExecutionContext } from '../types.js'
|
|
9
|
+
import { Greet } from './signature.js'
|
|
10
|
+
|
|
11
|
+
export class Greeter extends SignatureModule<typeof Greet> {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(Greet)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async forward(input: { name: string }, ctx: ExecutionContext) {
|
|
17
|
+
const result = await this.predictor.execute(input, ctx)
|
|
18
|
+
return result.data
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hello World signature.
|
|
3
|
+
*
|
|
4
|
+
* Defines the input/output contract for greeting generation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { signature, field } from '../index.js'
|
|
8
|
+
|
|
9
|
+
export const Greet = signature({
|
|
10
|
+
doc: 'Generate a friendly greeting for the given name.',
|
|
11
|
+
inputs: {
|
|
12
|
+
name: field.string('The name of the person to greet'),
|
|
13
|
+
},
|
|
14
|
+
outputs: {
|
|
15
|
+
greeting: field.string('A friendly greeting message'),
|
|
16
|
+
emoji: field.string('An appropriate emoji for the greeting'),
|
|
17
|
+
},
|
|
18
|
+
})
|