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/pipeline.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSTS SDK Pipeline orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Manages execution context, state, checkpointing, logging, retry logic, and sub-pipelines.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeFile, readFile, readdir, mkdir } from 'fs/promises'
|
|
8
|
+
import type { Module } from './module.js'
|
|
9
|
+
import type {
|
|
10
|
+
BaseState,
|
|
11
|
+
ExecutionContext,
|
|
12
|
+
PipelineConfig,
|
|
13
|
+
RunOptions,
|
|
14
|
+
StepResult,
|
|
15
|
+
} from './types.js'
|
|
16
|
+
import { logStep } from './agent.js'
|
|
17
|
+
import { JsonParseError, SchemaValidationError } from './parsing.js'
|
|
18
|
+
|
|
19
|
+
/** Pipeline orchestrates workflow execution with state management. */
|
|
20
|
+
export class Pipeline<S extends BaseState> {
|
|
21
|
+
state: S
|
|
22
|
+
private ctx: ExecutionContext
|
|
23
|
+
private stepNumber = 0
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
public readonly config: PipelineConfig,
|
|
27
|
+
createState: () => S,
|
|
28
|
+
) {
|
|
29
|
+
this.state = createState()
|
|
30
|
+
this.ctx = {
|
|
31
|
+
sessionId: undefined,
|
|
32
|
+
defaultModel: config.defaultModel,
|
|
33
|
+
defaultAgent: config.defaultAgent,
|
|
34
|
+
timeoutSec: config.timeoutSec ?? 300,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** run executes a module and records the result. */
|
|
39
|
+
async run<I, O>(
|
|
40
|
+
module: Module<I, O>,
|
|
41
|
+
input: I,
|
|
42
|
+
options?: RunOptions,
|
|
43
|
+
): Promise<StepResult<O>> {
|
|
44
|
+
const stepName = options?.name ?? module.constructor.name
|
|
45
|
+
this.stepNumber++
|
|
46
|
+
logStep(this.stepNumber, stepName)
|
|
47
|
+
|
|
48
|
+
// Handle session control
|
|
49
|
+
if (options?.newSession) {
|
|
50
|
+
this.ctx.sessionId = undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle model override
|
|
54
|
+
const originalModel = this.ctx.defaultModel
|
|
55
|
+
if (options?.model) {
|
|
56
|
+
this.ctx.defaultModel = options.model
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const startTime = Date.now()
|
|
60
|
+
let lastError: Error | undefined
|
|
61
|
+
const retryConfig = options?.retry ?? this.config.retry ?? { maxAttempts: 1 }
|
|
62
|
+
|
|
63
|
+
for (let attempt = 1; attempt <= retryConfig.maxAttempts; attempt++) {
|
|
64
|
+
try {
|
|
65
|
+
const data = await module.forward(input, this.ctx)
|
|
66
|
+
|
|
67
|
+
// Restore original model
|
|
68
|
+
this.ctx.defaultModel = originalModel
|
|
69
|
+
|
|
70
|
+
const result: StepResult<O> = {
|
|
71
|
+
data,
|
|
72
|
+
stepName,
|
|
73
|
+
duration: Date.now() - startTime,
|
|
74
|
+
sessionId: this.ctx.sessionId ?? '',
|
|
75
|
+
model: options?.model ?? this.config.defaultModel,
|
|
76
|
+
attempt,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Record step for checkpointing
|
|
80
|
+
this.state.steps.push({
|
|
81
|
+
stepName,
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
result: result as StepResult<unknown>,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Update opencode session in state
|
|
87
|
+
this.state.opencodeSessionId = this.ctx.sessionId
|
|
88
|
+
|
|
89
|
+
await this.saveCheckpoint()
|
|
90
|
+
return result
|
|
91
|
+
} catch (err) {
|
|
92
|
+
lastError = err as Error
|
|
93
|
+
const isParseError = err instanceof JsonParseError
|
|
94
|
+
const isSchemaError = err instanceof SchemaValidationError
|
|
95
|
+
|
|
96
|
+
// Don't retry SchemaValidationError - corrections already attempted
|
|
97
|
+
if (isSchemaError) {
|
|
98
|
+
console.error(
|
|
99
|
+
`Step ${stepName} failed with schema validation error (corrections exhausted): ${lastError.message}`,
|
|
100
|
+
)
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
attempt < retryConfig.maxAttempts &&
|
|
106
|
+
(!isParseError || retryConfig.onParseError)
|
|
107
|
+
) {
|
|
108
|
+
console.error(
|
|
109
|
+
`Step ${stepName} failed (attempt ${attempt}/${retryConfig.maxAttempts}): ${lastError.message}`,
|
|
110
|
+
)
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Save checkpoint before throwing
|
|
118
|
+
await this.saveCheckpoint()
|
|
119
|
+
throw lastError
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** runSub executes a sub-pipeline with its own session. */
|
|
123
|
+
async runSub<SS extends BaseState, T>(
|
|
124
|
+
subConfig: PipelineConfig,
|
|
125
|
+
createState: () => SS,
|
|
126
|
+
executor: (sub: Pipeline<SS>) => Promise<T>,
|
|
127
|
+
): Promise<StepResult<T>> {
|
|
128
|
+
this.stepNumber++
|
|
129
|
+
logStep(this.stepNumber, `sub:${subConfig.name}`)
|
|
130
|
+
|
|
131
|
+
const subPipeline = new Pipeline(subConfig, createState)
|
|
132
|
+
const startTime = Date.now()
|
|
133
|
+
|
|
134
|
+
const data = await executor(subPipeline)
|
|
135
|
+
|
|
136
|
+
// Record sub-pipeline reference
|
|
137
|
+
this.state.subPipelines.push({
|
|
138
|
+
name: subConfig.name,
|
|
139
|
+
sessionId: subPipeline.ctx.sessionId ?? '',
|
|
140
|
+
timestamp: new Date().toISOString(),
|
|
141
|
+
state: subPipeline.state,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
await this.saveCheckpoint()
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
data,
|
|
148
|
+
stepName: `sub:${subConfig.name}`,
|
|
149
|
+
duration: Date.now() - startTime,
|
|
150
|
+
sessionId: subPipeline.ctx.sessionId ?? '',
|
|
151
|
+
model: subConfig.defaultModel,
|
|
152
|
+
attempt: 1,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** getSessionId returns the current OpenCode session ID. */
|
|
157
|
+
getSessionId(): string | undefined {
|
|
158
|
+
return this.ctx.sessionId
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** setPhase updates the current phase in state. */
|
|
162
|
+
setPhase(phase: string): void {
|
|
163
|
+
this.state.phase = phase
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** saveCheckpoint persists the current state to disk. */
|
|
167
|
+
async saveCheckpoint(): Promise<void> {
|
|
168
|
+
await mkdir(this.config.checkpointDir, { recursive: true })
|
|
169
|
+
const path = `${this.config.checkpointDir}/${this.config.name}_${this.state.sessionId}.json`
|
|
170
|
+
await writeFile(path, JSON.stringify(this.state, null, 2))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** loadCheckpoint loads a pipeline from a checkpoint file. */
|
|
174
|
+
static async loadCheckpoint<S extends BaseState>(
|
|
175
|
+
config: PipelineConfig,
|
|
176
|
+
sessionId: string,
|
|
177
|
+
): Promise<Pipeline<S> | null> {
|
|
178
|
+
const path = `${config.checkpointDir}/${config.name}_${sessionId}.json`
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const content = await readFile(path, 'utf-8')
|
|
182
|
+
const state = JSON.parse(content) as S
|
|
183
|
+
const pipeline = new Pipeline<S>(config, () => state)
|
|
184
|
+
pipeline.state = state
|
|
185
|
+
|
|
186
|
+
// Restore context from state
|
|
187
|
+
if (state.opencodeSessionId) {
|
|
188
|
+
pipeline.ctx.sessionId = state.opencodeSessionId
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Restore step number
|
|
192
|
+
pipeline.stepNumber = state.steps.length
|
|
193
|
+
|
|
194
|
+
return pipeline
|
|
195
|
+
} catch {
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** listCheckpoints lists all checkpoint files for a pipeline name. */
|
|
201
|
+
static async listCheckpoints(config: PipelineConfig): Promise<string[]> {
|
|
202
|
+
try {
|
|
203
|
+
const allFiles = await readdir(config.checkpointDir)
|
|
204
|
+
const prefix = `${config.name}_`
|
|
205
|
+
const files = allFiles
|
|
206
|
+
.filter((f) => f.startsWith(prefix) && f.endsWith('.json'))
|
|
207
|
+
.map((f) => `${config.checkpointDir}/${f}`)
|
|
208
|
+
return files.sort().reverse() // Most recent first
|
|
209
|
+
} catch {
|
|
210
|
+
return []
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
package/predict.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSTS SDK Predict class.
|
|
3
|
+
*
|
|
4
|
+
* Executes a signature by generating a prompt, calling OpenCode, and parsing the response.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import type {
|
|
9
|
+
CorrectionConfig,
|
|
10
|
+
CorrectionMethod,
|
|
11
|
+
ExecutionContext,
|
|
12
|
+
FieldConfig,
|
|
13
|
+
FieldError,
|
|
14
|
+
InferInputs,
|
|
15
|
+
InferOutputs,
|
|
16
|
+
ModelConfig,
|
|
17
|
+
PredictResult,
|
|
18
|
+
SignatureDef,
|
|
19
|
+
} from './types.js'
|
|
20
|
+
import { runAgent } from './agent.js'
|
|
21
|
+
import {
|
|
22
|
+
tryParseResponse,
|
|
23
|
+
// jq-style patches
|
|
24
|
+
buildPatchPrompt,
|
|
25
|
+
buildBatchPatchPrompt,
|
|
26
|
+
extractPatch,
|
|
27
|
+
applyJqPatch,
|
|
28
|
+
// JSON Patch (RFC 6902)
|
|
29
|
+
buildJsonPatchPrompt,
|
|
30
|
+
buildBatchJsonPatchPrompt,
|
|
31
|
+
extractJsonPatch,
|
|
32
|
+
applyJsonPatch,
|
|
33
|
+
SchemaValidationError,
|
|
34
|
+
} from './parsing.js'
|
|
35
|
+
|
|
36
|
+
/** Configuration for a Predict instance. */
|
|
37
|
+
export interface PredictConfig {
|
|
38
|
+
/** Override the pipeline's default agent. */
|
|
39
|
+
agent?: string
|
|
40
|
+
/** Override the pipeline's default model. */
|
|
41
|
+
model?: ModelConfig
|
|
42
|
+
/** Start a fresh session (default: false, reuses context). */
|
|
43
|
+
newSession?: boolean
|
|
44
|
+
/** Custom prompt template function. */
|
|
45
|
+
template?: (inputs: Record<string, unknown>) => string
|
|
46
|
+
/** Schema correction options (enabled by default, set to false to disable). */
|
|
47
|
+
correction?: CorrectionConfig | false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Predict executes a signature by calling an LLM and parsing the response. */
|
|
51
|
+
export class Predict<S extends SignatureDef<any, any>> {
|
|
52
|
+
constructor(
|
|
53
|
+
public readonly sig: S,
|
|
54
|
+
public readonly config: PredictConfig = {},
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
/** execute runs the prediction with the given inputs. */
|
|
58
|
+
async execute(
|
|
59
|
+
inputs: InferInputs<S>,
|
|
60
|
+
ctx: ExecutionContext,
|
|
61
|
+
): Promise<PredictResult<InferOutputs<S>>> {
|
|
62
|
+
const prompt = this.buildPrompt(inputs as Record<string, unknown>)
|
|
63
|
+
const startTime = Date.now()
|
|
64
|
+
|
|
65
|
+
const agentResult = await runAgent({
|
|
66
|
+
prompt,
|
|
67
|
+
agent: this.config.agent ?? ctx.defaultAgent,
|
|
68
|
+
model: this.config.model ?? ctx.defaultModel,
|
|
69
|
+
sessionId: this.config.newSession ? undefined : ctx.sessionId,
|
|
70
|
+
timeoutSec: ctx.timeoutSec,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Update context with new session ID for continuity
|
|
74
|
+
ctx.sessionId = agentResult.sessionId
|
|
75
|
+
|
|
76
|
+
const parseResult = tryParseResponse<InferOutputs<S>>(
|
|
77
|
+
agentResult.text,
|
|
78
|
+
this.sig.outputs,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// If parsing succeeded, return the result
|
|
82
|
+
if (parseResult.ok && parseResult.data) {
|
|
83
|
+
return {
|
|
84
|
+
data: parseResult.data,
|
|
85
|
+
raw: agentResult.text,
|
|
86
|
+
sessionId: agentResult.sessionId,
|
|
87
|
+
duration: Date.now() - startTime,
|
|
88
|
+
model: this.config.model ?? ctx.defaultModel,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Parsing failed - attempt correction if enabled
|
|
93
|
+
if (this.config.correction !== false && parseResult.errors && parseResult.json) {
|
|
94
|
+
const corrected = await this.correctFields(
|
|
95
|
+
parseResult.json,
|
|
96
|
+
parseResult.errors,
|
|
97
|
+
ctx,
|
|
98
|
+
agentResult.sessionId,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if (corrected) {
|
|
102
|
+
return {
|
|
103
|
+
data: corrected,
|
|
104
|
+
raw: agentResult.text,
|
|
105
|
+
sessionId: agentResult.sessionId,
|
|
106
|
+
duration: Date.now() - startTime,
|
|
107
|
+
model: this.config.model ?? ctx.defaultModel,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Correction failed or disabled - throw SchemaValidationError (non-retryable)
|
|
113
|
+
const errors = parseResult.errors ?? []
|
|
114
|
+
const errorMessages = errors.map((e) => `${e.path}: ${e.message}`).join('; ') || 'Unknown error'
|
|
115
|
+
const correctionAttempts = this.config.correction !== false ? (typeof this.config.correction === 'object' ? this.config.correction.maxRounds ?? 3 : 3) : 0
|
|
116
|
+
throw new SchemaValidationError(`Schema validation failed: ${errorMessages}`, errors, correctionAttempts)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** correctFields attempts to fix field errors using same-session patches with retries. */
|
|
120
|
+
private async correctFields(
|
|
121
|
+
json: Record<string, unknown>,
|
|
122
|
+
initialErrors: FieldError[],
|
|
123
|
+
ctx: ExecutionContext,
|
|
124
|
+
sessionId: string,
|
|
125
|
+
): Promise<InferOutputs<S> | null> {
|
|
126
|
+
const correctionConfig = typeof this.config.correction === 'object' ? this.config.correction : {}
|
|
127
|
+
const method: CorrectionMethod = correctionConfig.method ?? 'json-patch'
|
|
128
|
+
const maxFields = correctionConfig.maxFields ?? 5
|
|
129
|
+
const maxRounds = correctionConfig.maxRounds ?? 3
|
|
130
|
+
const correctionModel = correctionConfig.model
|
|
131
|
+
|
|
132
|
+
let currentJson = JSON.parse(JSON.stringify(json)) as Record<string, unknown>
|
|
133
|
+
let currentErrors = initialErrors
|
|
134
|
+
|
|
135
|
+
for (let round = 1; round <= maxRounds; round++) {
|
|
136
|
+
const errorsToFix = currentErrors.slice(0, maxFields)
|
|
137
|
+
|
|
138
|
+
if (errorsToFix.length === 0) {
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.error(`\n>>> Correction round ${round}/${maxRounds} [${method}]: fixing ${errorsToFix.length} field(s)...`)
|
|
143
|
+
|
|
144
|
+
// Build prompt based on correction method
|
|
145
|
+
const patchPrompt = method === 'jq'
|
|
146
|
+
? (errorsToFix.length === 1
|
|
147
|
+
? buildPatchPrompt(errorsToFix[0]!, currentJson, this.sig.outputs)
|
|
148
|
+
: buildBatchPatchPrompt(errorsToFix, currentJson))
|
|
149
|
+
: (errorsToFix.length === 1
|
|
150
|
+
? buildJsonPatchPrompt(errorsToFix[0]!, currentJson, this.sig.outputs)
|
|
151
|
+
: buildBatchJsonPatchPrompt(errorsToFix, currentJson))
|
|
152
|
+
|
|
153
|
+
// Use same session (model has context) unless correction model specified
|
|
154
|
+
const patchResult = await runAgent({
|
|
155
|
+
prompt: patchPrompt,
|
|
156
|
+
model: correctionModel ?? ctx.defaultModel,
|
|
157
|
+
sessionId: correctionModel ? undefined : sessionId,
|
|
158
|
+
agent: ctx.defaultAgent,
|
|
159
|
+
timeoutSec: 60, // Short timeout for simple patches
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Extract and apply the patch based on method
|
|
163
|
+
if (method === 'jq') {
|
|
164
|
+
const patch = extractPatch(patchResult.text)
|
|
165
|
+
console.error(` jq patch: ${patch}`)
|
|
166
|
+
currentJson = applyJqPatch(currentJson, patch)
|
|
167
|
+
} else {
|
|
168
|
+
const operations = extractJsonPatch(patchResult.text)
|
|
169
|
+
console.error(` JSON Patch: ${JSON.stringify(operations)}`)
|
|
170
|
+
currentJson = applyJsonPatch(currentJson, operations)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Re-validate the corrected JSON
|
|
174
|
+
const revalidated = tryParseResponse<InferOutputs<S>>(
|
|
175
|
+
JSON.stringify(currentJson),
|
|
176
|
+
this.sig.outputs,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if (revalidated.ok && revalidated.data) {
|
|
180
|
+
console.error(` Schema correction successful after ${round} round(s)!`)
|
|
181
|
+
return revalidated.data
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Update errors for next round
|
|
185
|
+
currentErrors = revalidated.errors ?? []
|
|
186
|
+
|
|
187
|
+
if (currentErrors.length === 0) {
|
|
188
|
+
// No errors but also no data? Shouldn't happen, but handle gracefully
|
|
189
|
+
console.error(` Unexpected state: no errors but validation failed`)
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.error(` Round ${round} complete, ${currentErrors.length} error(s) remaining`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.error(` Schema correction failed after ${maxRounds} rounds`)
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** buildPrompt generates the prompt from inputs. */
|
|
201
|
+
private buildPrompt(inputs: Record<string, unknown>): string {
|
|
202
|
+
// Allow custom template override
|
|
203
|
+
if (this.config.template) {
|
|
204
|
+
return this.config.template(inputs)
|
|
205
|
+
}
|
|
206
|
+
return this.generatePrompt(inputs)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** generatePrompt creates a structured prompt from the signature and inputs. */
|
|
210
|
+
private generatePrompt(inputs: Record<string, unknown>): string {
|
|
211
|
+
const lines: string[] = []
|
|
212
|
+
|
|
213
|
+
// Task description from signature doc
|
|
214
|
+
lines.push(this.sig.doc)
|
|
215
|
+
lines.push('')
|
|
216
|
+
|
|
217
|
+
// Input fields as JSON
|
|
218
|
+
const inputsWithDescriptions: Record<string, unknown> = {}
|
|
219
|
+
for (const [name, config] of Object.entries(this.sig.inputs) as [
|
|
220
|
+
string,
|
|
221
|
+
FieldConfig,
|
|
222
|
+
][]) {
|
|
223
|
+
inputsWithDescriptions[name] = inputs[name]
|
|
224
|
+
}
|
|
225
|
+
lines.push('INPUTS:')
|
|
226
|
+
lines.push('```json')
|
|
227
|
+
lines.push(JSON.stringify(inputsWithDescriptions, null, 2))
|
|
228
|
+
lines.push('```')
|
|
229
|
+
lines.push('')
|
|
230
|
+
|
|
231
|
+
// Output format with JSON Schema
|
|
232
|
+
lines.push('OUTPUT FORMAT:')
|
|
233
|
+
lines.push('Return a JSON object matching this schema EXACTLY.')
|
|
234
|
+
lines.push('IMPORTANT: For optional fields, OMIT the field entirely - do NOT use null.')
|
|
235
|
+
lines.push('')
|
|
236
|
+
lines.push('```json')
|
|
237
|
+
lines.push(JSON.stringify(this.buildOutputJsonSchema(), null, 2))
|
|
238
|
+
lines.push('```')
|
|
239
|
+
|
|
240
|
+
return lines.join('\n')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** buildOutputJsonSchema creates a JSON Schema from the output field definitions. */
|
|
244
|
+
private buildOutputJsonSchema(): Record<string, unknown> {
|
|
245
|
+
// Build a Zod object from the output fields
|
|
246
|
+
const shape: Record<string, z.ZodType> = {}
|
|
247
|
+
for (const [name, config] of Object.entries(this.sig.outputs) as [string, FieldConfig][]) {
|
|
248
|
+
shape[name] = config.type
|
|
249
|
+
}
|
|
250
|
+
const outputSchema = z.object(shape)
|
|
251
|
+
|
|
252
|
+
// Convert to JSON Schema
|
|
253
|
+
const jsonSchema = z.toJSONSchema(outputSchema)
|
|
254
|
+
|
|
255
|
+
// Add field descriptions from our config (toJSONSchema uses .describe() metadata)
|
|
256
|
+
// Since our FieldConfig has a separate desc field, merge it in
|
|
257
|
+
const props = jsonSchema.properties as Record<string, Record<string, unknown>> | undefined
|
|
258
|
+
if (props) {
|
|
259
|
+
for (const [name, config] of Object.entries(this.sig.outputs) as [string, FieldConfig][]) {
|
|
260
|
+
if (config.desc && props[name]) {
|
|
261
|
+
// Only add if not already set by .describe()
|
|
262
|
+
if (!props[name].description) {
|
|
263
|
+
props[name].description = config.desc
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return jsonSchema
|
|
270
|
+
}
|
|
271
|
+
}
|
package/signature.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSTS SDK signature definition.
|
|
3
|
+
*
|
|
4
|
+
* Signatures declare input/output contracts for LLM interactions using Zod for validation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import type { FieldConfig, SignatureDef } from './types.js'
|
|
9
|
+
|
|
10
|
+
/** signature creates a new signature definition. */
|
|
11
|
+
export function signature<
|
|
12
|
+
I extends Record<string, FieldConfig>,
|
|
13
|
+
O extends Record<string, FieldConfig>,
|
|
14
|
+
>(def: { doc: string; inputs: I; outputs: O }): SignatureDef<I, O> {
|
|
15
|
+
return def
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** field provides helper functions for creating common field configurations. */
|
|
19
|
+
export const field = {
|
|
20
|
+
/** string creates a string field. */
|
|
21
|
+
string: (desc?: string): FieldConfig<z.ZodString> => ({
|
|
22
|
+
type: z.string(),
|
|
23
|
+
desc,
|
|
24
|
+
}),
|
|
25
|
+
|
|
26
|
+
/** number creates a number field. */
|
|
27
|
+
number: (desc?: string): FieldConfig<z.ZodNumber> => ({
|
|
28
|
+
type: z.number(),
|
|
29
|
+
desc,
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
/** boolean creates a boolean field. */
|
|
33
|
+
boolean: (desc?: string): FieldConfig<z.ZodBoolean> => ({
|
|
34
|
+
type: z.boolean(),
|
|
35
|
+
desc,
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
/** array creates an array field with the specified item type. */
|
|
39
|
+
array: <T extends z.ZodType>(
|
|
40
|
+
itemType: T,
|
|
41
|
+
desc?: string,
|
|
42
|
+
): FieldConfig<z.ZodArray<T>> => ({
|
|
43
|
+
type: z.array(itemType),
|
|
44
|
+
desc,
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
/** object creates an object field with the specified shape. */
|
|
48
|
+
object: <T extends z.ZodRawShape>(
|
|
49
|
+
shape: T,
|
|
50
|
+
desc?: string,
|
|
51
|
+
): FieldConfig<z.ZodObject<T>> => ({
|
|
52
|
+
type: z.object(shape),
|
|
53
|
+
desc,
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
/** enum creates an enum field with the specified values. */
|
|
57
|
+
enum: <const T extends readonly [string, ...string[]]>(
|
|
58
|
+
values: T,
|
|
59
|
+
desc?: string,
|
|
60
|
+
) => ({
|
|
61
|
+
type: z.enum(values),
|
|
62
|
+
desc,
|
|
63
|
+
}),
|
|
64
|
+
|
|
65
|
+
/** optional wraps a field type to make it optional. */
|
|
66
|
+
optional: <T extends z.ZodType>(
|
|
67
|
+
fieldConfig: FieldConfig<T>,
|
|
68
|
+
): FieldConfig<z.ZodOptional<T>> => ({
|
|
69
|
+
type: fieldConfig.type.optional(),
|
|
70
|
+
desc: fieldConfig.desc,
|
|
71
|
+
}),
|
|
72
|
+
|
|
73
|
+
/** nullable wraps a field type to make it nullable. */
|
|
74
|
+
nullable: <T extends z.ZodType>(
|
|
75
|
+
fieldConfig: FieldConfig<T>,
|
|
76
|
+
): FieldConfig<z.ZodNullable<T>> => ({
|
|
77
|
+
type: fieldConfig.type.nullable(),
|
|
78
|
+
desc: fieldConfig.desc,
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
/** custom creates a field with a custom Zod type. */
|
|
82
|
+
custom: <T extends z.ZodType>(type: T, desc?: string): FieldConfig<T> => ({
|
|
83
|
+
type,
|
|
84
|
+
desc,
|
|
85
|
+
}),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** buildOutputSchema creates a Zod object schema from output field definitions. */
|
|
89
|
+
export function buildOutputSchema<O extends Record<string, FieldConfig>>(
|
|
90
|
+
outputs: O,
|
|
91
|
+
): z.ZodObject<{ [K in keyof O]: O[K]['type'] }> {
|
|
92
|
+
const shape = {} as { [K in keyof O]: O[K]['type'] }
|
|
93
|
+
for (const [key, config] of Object.entries(outputs)) {
|
|
94
|
+
;(shape as Record<string, z.ZodType>)[key] = config.type
|
|
95
|
+
}
|
|
96
|
+
return z.object(shape)
|
|
97
|
+
}
|
package/state.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSTS SDK state management.
|
|
3
|
+
*
|
|
4
|
+
* Provides base state types and helpers for checkpointable workflow state.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { BaseState } from './types.js'
|
|
8
|
+
|
|
9
|
+
/** createSessionId generates a unique session ID based on the current timestamp. */
|
|
10
|
+
export function createSessionId(): string {
|
|
11
|
+
const now = new Date()
|
|
12
|
+
const id = now
|
|
13
|
+
.toISOString()
|
|
14
|
+
.replace(/[-:]/g, '')
|
|
15
|
+
.replace('T', '_')
|
|
16
|
+
.split('.')[0]
|
|
17
|
+
return id ?? ''
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** createBaseState creates a new base state with default values. */
|
|
21
|
+
export function createBaseState(): BaseState {
|
|
22
|
+
return {
|
|
23
|
+
sessionId: createSessionId(),
|
|
24
|
+
startedAt: new Date().toISOString(),
|
|
25
|
+
phase: 'init',
|
|
26
|
+
steps: [],
|
|
27
|
+
subPipelines: [],
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** extendBaseState creates a state factory that extends BaseState with additional fields. */
|
|
32
|
+
export function extendBaseState<T extends Record<string, unknown>>(
|
|
33
|
+
extension: T,
|
|
34
|
+
): BaseState & T {
|
|
35
|
+
return {
|
|
36
|
+
...createBaseState(),
|
|
37
|
+
...extension,
|
|
38
|
+
}
|
|
39
|
+
}
|