task-while 0.0.1 → 0.0.3
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 +32 -34
- package/package.json +2 -2
- package/src/adapters/fs/harness-store.ts +84 -0
- package/src/agents/claude.ts +159 -9
- package/src/agents/codex.ts +68 -4
- package/src/agents/event-log.ts +195 -0
- package/src/batch/discovery.ts +1 -1
- package/src/batch/provider.ts +9 -0
- package/src/commands/batch.ts +69 -165
- package/src/commands/run-branch-helpers.ts +81 -0
- package/src/commands/run-providers.ts +77 -0
- package/src/commands/run.ts +117 -225
- package/src/core/create-runtime-ports.ts +118 -0
- package/src/core/runtime.ts +15 -36
- package/src/harness/in-memory-store.ts +45 -0
- package/src/harness/kernel.ts +226 -0
- package/src/harness/state.ts +47 -0
- package/src/harness/store.ts +26 -0
- package/src/harness/workflow-builders.ts +87 -0
- package/src/harness/workflow-program.ts +86 -0
- package/src/ports/agent.ts +17 -0
- package/src/ports/code-host.ts +23 -0
- package/src/programs/batch.ts +139 -0
- package/src/programs/run-direct.ts +209 -0
- package/src/programs/run-pr-transitions.ts +81 -0
- package/src/programs/run-pr.ts +290 -0
- package/src/programs/shared-steps.ts +252 -0
- package/src/schedulers/scheduler.ts +208 -0
- package/src/session/session.ts +127 -0
- package/src/workflow/config.ts +15 -0
- package/src/core/engine-helpers.ts +0 -114
- package/src/core/engine-outcomes.ts +0 -166
- package/src/core/engine.ts +0 -223
- package/src/core/orchestrator-helpers.ts +0 -52
- package/src/core/orchestrator-integrate-resume.ts +0 -149
- package/src/core/orchestrator-review-resume.ts +0 -228
- package/src/core/orchestrator-task-attempt.ts +0 -257
- package/src/core/orchestrator.ts +0 -99
- package/src/runtime/fs-runtime.ts +0 -209
- package/src/workflow/direct-preset.ts +0 -44
- package/src/workflow/preset.ts +0 -86
- package/src/workflow/pull-request-preset.ts +0 -312
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createInitialState,
|
|
3
|
+
TaskStatus,
|
|
4
|
+
type Artifact,
|
|
5
|
+
type TaskState,
|
|
6
|
+
} from './state'
|
|
7
|
+
import {
|
|
8
|
+
WorkflowNodeType,
|
|
9
|
+
type DomainResult,
|
|
10
|
+
type Transition,
|
|
11
|
+
type TransitionRule,
|
|
12
|
+
type TypedArtifactMap,
|
|
13
|
+
type WorkflowContext,
|
|
14
|
+
type WorkflowNode,
|
|
15
|
+
type WorkflowProgram,
|
|
16
|
+
} from './workflow-program'
|
|
17
|
+
|
|
18
|
+
import type { HarnessStore } from './store'
|
|
19
|
+
|
|
20
|
+
export enum KernelResultKind {
|
|
21
|
+
Error = 'error',
|
|
22
|
+
GateFail = 'gate.fail',
|
|
23
|
+
GatePass = 'gate.pass',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function retryBudgetReached(state: TaskState, maxIterations: number) {
|
|
27
|
+
return Math.max(0, ...Object.values(state.phaseIterations)) >= maxIterations
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function errorRetry(maxIterations: number) {
|
|
31
|
+
return (retryPhase: string): TransitionRule =>
|
|
32
|
+
(input) =>
|
|
33
|
+
retryBudgetReached(input.state, maxIterations)
|
|
34
|
+
? { nextPhase: null, status: TaskStatus.Blocked }
|
|
35
|
+
: { nextPhase: retryPhase, status: TaskStatus.Running }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface KernelResult {
|
|
39
|
+
status: TaskStatus
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function runKernel(input: {
|
|
43
|
+
config: Record<string, unknown>
|
|
44
|
+
program: WorkflowProgram
|
|
45
|
+
protocol: string
|
|
46
|
+
store: HarnessStore
|
|
47
|
+
subjectId: string
|
|
48
|
+
}): Promise<KernelResult> {
|
|
49
|
+
const { config, program, protocol, store, subjectId } = input
|
|
50
|
+
|
|
51
|
+
let state =
|
|
52
|
+
(await store.loadState(protocol, subjectId)) ??
|
|
53
|
+
createInitialState(subjectId)
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
state.status === TaskStatus.Done ||
|
|
57
|
+
state.status === TaskStatus.Blocked ||
|
|
58
|
+
state.status === TaskStatus.Replan
|
|
59
|
+
) {
|
|
60
|
+
return { status: state.status }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const artifactCache = new Map<string, Artifact>()
|
|
64
|
+
for (const [kind, artifactId] of Object.entries(state.artifacts)) {
|
|
65
|
+
const artifact = await store.loadArtifact(protocol, subjectId, artifactId)
|
|
66
|
+
if (artifact) {
|
|
67
|
+
artifactCache.set(kind, artifact)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const artifacts: TypedArtifactMap = {
|
|
72
|
+
get<T>(kind: string) {
|
|
73
|
+
return artifactCache.get(kind) as Artifact<T> | undefined
|
|
74
|
+
},
|
|
75
|
+
has(kind: string) {
|
|
76
|
+
return artifactCache.has(kind)
|
|
77
|
+
},
|
|
78
|
+
set(artifact: Artifact) {
|
|
79
|
+
artifactCache.set(artifact.kind, artifact)
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (state.status === TaskStatus.Suspended) {
|
|
84
|
+
state = { ...state, status: TaskStatus.Running }
|
|
85
|
+
await store.saveState(protocol, subjectId, state)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let current: null | string = state.currentPhase ?? program.entry
|
|
89
|
+
|
|
90
|
+
while (current) {
|
|
91
|
+
const node: undefined | WorkflowNode = program.nodes[current]
|
|
92
|
+
if (!node) {
|
|
93
|
+
throw new Error(`unknown node: ${current}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (node.type === WorkflowNodeType.Gate) {
|
|
97
|
+
const ctx: WorkflowContext = { artifacts, config, state, subjectId }
|
|
98
|
+
const passed: boolean = await node.test(ctx)
|
|
99
|
+
const nextPhase: string = passed ? node.then : node.otherwise
|
|
100
|
+
|
|
101
|
+
await store.appendTransition(protocol, subjectId, {
|
|
102
|
+
nextPhase,
|
|
103
|
+
phase: current,
|
|
104
|
+
status: state.status,
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
resultKind: passed
|
|
107
|
+
? KernelResultKind.GatePass
|
|
108
|
+
: KernelResultKind.GateFail,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
current = nextPhase
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (node.type === WorkflowNodeType.Branch) {
|
|
116
|
+
const ctx: WorkflowContext = { artifacts, config, state, subjectId }
|
|
117
|
+
const decision = await node.decide(ctx)
|
|
118
|
+
const nextPhase: string | undefined = node.paths[decision]
|
|
119
|
+
if (!nextPhase) {
|
|
120
|
+
throw new Error(`branch "${current}" has no path for "${decision}"`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await store.appendTransition(protocol, subjectId, {
|
|
124
|
+
nextPhase,
|
|
125
|
+
phase: current,
|
|
126
|
+
resultKind: `branch.${decision}`,
|
|
127
|
+
status: state.status,
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
current = nextPhase
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const phaseCount = (state.phaseIterations[current] ?? 0) + 1
|
|
136
|
+
state = {
|
|
137
|
+
...state,
|
|
138
|
+
currentPhase: current,
|
|
139
|
+
iteration: phaseCount,
|
|
140
|
+
phaseIterations: { ...state.phaseIterations, [current]: phaseCount },
|
|
141
|
+
status: TaskStatus.Running,
|
|
142
|
+
}
|
|
143
|
+
await store.saveState(protocol, subjectId, state)
|
|
144
|
+
|
|
145
|
+
let actionResult: { artifact?: Artifact; result: DomainResult }
|
|
146
|
+
try {
|
|
147
|
+
const ctx: WorkflowContext = { artifacts, config, state, subjectId }
|
|
148
|
+
actionResult = await node.run(ctx)
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
151
|
+
state = { ...state, failureReason: reason }
|
|
152
|
+
|
|
153
|
+
if (program.transitions[current]?.[KernelResultKind.Error]) {
|
|
154
|
+
actionResult = { result: { kind: KernelResultKind.Error } }
|
|
155
|
+
} else {
|
|
156
|
+
state = { ...state, status: TaskStatus.Blocked }
|
|
157
|
+
await store.saveState(protocol, subjectId, state)
|
|
158
|
+
await store.appendTransition(protocol, subjectId, {
|
|
159
|
+
nextPhase: null,
|
|
160
|
+
phase: current,
|
|
161
|
+
resultKind: KernelResultKind.Error,
|
|
162
|
+
status: TaskStatus.Blocked,
|
|
163
|
+
timestamp: new Date().toISOString(),
|
|
164
|
+
})
|
|
165
|
+
return { status: TaskStatus.Blocked }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (actionResult.artifact) {
|
|
170
|
+
await store.saveArtifact(protocol, subjectId, actionResult.artifact)
|
|
171
|
+
artifacts.set(actionResult.artifact)
|
|
172
|
+
state = {
|
|
173
|
+
...state,
|
|
174
|
+
artifacts: {
|
|
175
|
+
...state.artifacts,
|
|
176
|
+
[actionResult.artifact.kind]: actionResult.artifact.id,
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const transitionTable = program.transitions[current]
|
|
182
|
+
if (!transitionTable) {
|
|
183
|
+
throw new Error(`no transition table for "${current}"`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const rule: TransitionRule | undefined =
|
|
187
|
+
transitionTable[actionResult.result.kind]
|
|
188
|
+
if (!rule) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`no transition rule for "${current}" -> "${actionResult.result.kind}"`,
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const transition: Transition =
|
|
195
|
+
typeof rule === 'function'
|
|
196
|
+
? rule({ result: actionResult.result, state })
|
|
197
|
+
: rule
|
|
198
|
+
|
|
199
|
+
state = {
|
|
200
|
+
...state,
|
|
201
|
+
currentPhase: transition.nextPhase,
|
|
202
|
+
status: transition.status,
|
|
203
|
+
completedAt:
|
|
204
|
+
transition.status === TaskStatus.Done
|
|
205
|
+
? new Date().toISOString()
|
|
206
|
+
: state.completedAt,
|
|
207
|
+
}
|
|
208
|
+
await store.saveState(protocol, subjectId, state)
|
|
209
|
+
|
|
210
|
+
await store.appendTransition(protocol, subjectId, {
|
|
211
|
+
nextPhase: transition.nextPhase,
|
|
212
|
+
phase: current,
|
|
213
|
+
resultKind: actionResult.result.kind,
|
|
214
|
+
status: transition.status,
|
|
215
|
+
timestamp: new Date().toISOString(),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
if (state.status !== TaskStatus.Running) {
|
|
219
|
+
return { status: state.status }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
current = transition.nextPhase
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return { status: state.status }
|
|
226
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export enum TaskStatus {
|
|
2
|
+
Blocked = 'blocked',
|
|
3
|
+
Done = 'done',
|
|
4
|
+
Queued = 'queued',
|
|
5
|
+
Replan = 'replan',
|
|
6
|
+
Running = 'running',
|
|
7
|
+
Suspended = 'suspended',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TaskState {
|
|
11
|
+
artifacts: Record<string, string>
|
|
12
|
+
completedAt: null | string
|
|
13
|
+
currentPhase: null | string
|
|
14
|
+
failureReason: null | string
|
|
15
|
+
iteration: number
|
|
16
|
+
phaseIterations: Record<string, number>
|
|
17
|
+
status: TaskStatus
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TransitionRecord {
|
|
21
|
+
nextPhase: null | string
|
|
22
|
+
phase: string
|
|
23
|
+
resultKind: string
|
|
24
|
+
status: TaskStatus
|
|
25
|
+
timestamp: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Artifact<TPayload = unknown> {
|
|
29
|
+
id: string
|
|
30
|
+
kind: string
|
|
31
|
+
payload: TPayload
|
|
32
|
+
subjectId: string
|
|
33
|
+
timestamp: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createInitialState(subjectId: string): TaskState {
|
|
37
|
+
void subjectId
|
|
38
|
+
return {
|
|
39
|
+
artifacts: {},
|
|
40
|
+
completedAt: null,
|
|
41
|
+
currentPhase: null,
|
|
42
|
+
failureReason: null,
|
|
43
|
+
iteration: 0,
|
|
44
|
+
phaseIterations: {},
|
|
45
|
+
status: TaskStatus.Queued,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Artifact, TaskState, TransitionRecord } from './state'
|
|
2
|
+
|
|
3
|
+
export interface HarnessStore {
|
|
4
|
+
appendTransition: (
|
|
5
|
+
protocol: string,
|
|
6
|
+
subjectId: string,
|
|
7
|
+
record: TransitionRecord,
|
|
8
|
+
) => Promise<void>
|
|
9
|
+
listArtifacts: (protocol: string, subjectId: string) => Promise<Artifact[]>
|
|
10
|
+
loadArtifact: (
|
|
11
|
+
protocol: string,
|
|
12
|
+
subjectId: string,
|
|
13
|
+
artifactId: string,
|
|
14
|
+
) => Promise<Artifact | null>
|
|
15
|
+
loadState: (protocol: string, subjectId: string) => Promise<null | TaskState>
|
|
16
|
+
saveArtifact: (
|
|
17
|
+
protocol: string,
|
|
18
|
+
subjectId: string,
|
|
19
|
+
artifact: Artifact,
|
|
20
|
+
) => Promise<void>
|
|
21
|
+
saveState: (
|
|
22
|
+
protocol: string,
|
|
23
|
+
subjectId: string,
|
|
24
|
+
state: TaskState,
|
|
25
|
+
) => Promise<void>
|
|
26
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WorkflowNodeType,
|
|
3
|
+
type ActionNode,
|
|
4
|
+
type ActionRunFn,
|
|
5
|
+
type BranchDecideFn,
|
|
6
|
+
type BranchNode,
|
|
7
|
+
type GateNode,
|
|
8
|
+
type GateTestFn,
|
|
9
|
+
type TransitionRule,
|
|
10
|
+
type WorkflowNode,
|
|
11
|
+
type WorkflowProgram,
|
|
12
|
+
} from './workflow-program'
|
|
13
|
+
|
|
14
|
+
export function action<TConfig = unknown>(
|
|
15
|
+
name: string,
|
|
16
|
+
overrides?: { run?: ActionRunFn<TConfig> },
|
|
17
|
+
): ActionNode<TConfig> {
|
|
18
|
+
return {
|
|
19
|
+
name,
|
|
20
|
+
type: WorkflowNodeType.Action,
|
|
21
|
+
run:
|
|
22
|
+
overrides?.run ??
|
|
23
|
+
(async () => ({ result: { kind: `${name}.completed` } })),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function gate<TConfig = unknown>(
|
|
28
|
+
name: string,
|
|
29
|
+
input: { otherwise: string; test: GateTestFn<TConfig>; then: string },
|
|
30
|
+
): GateNode<TConfig> {
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
otherwise: input.otherwise,
|
|
34
|
+
test: input.test,
|
|
35
|
+
then: input.then,
|
|
36
|
+
type: WorkflowNodeType.Gate,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function branch<TConfig = unknown>(
|
|
41
|
+
name: string,
|
|
42
|
+
input: { decide: BranchDecideFn<TConfig>; paths: Record<string, string> },
|
|
43
|
+
): BranchNode<TConfig> {
|
|
44
|
+
return {
|
|
45
|
+
name,
|
|
46
|
+
decide: input.decide,
|
|
47
|
+
paths: input.paths,
|
|
48
|
+
type: WorkflowNodeType.Branch,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function sequence<TConfig = unknown>(
|
|
53
|
+
nodes: WorkflowNode<TConfig>[],
|
|
54
|
+
transitions: Record<string, Record<string, TransitionRule>>,
|
|
55
|
+
): WorkflowProgram<TConfig> {
|
|
56
|
+
const nodeMap: Record<string, WorkflowNode<TConfig>> = {}
|
|
57
|
+
for (const node of nodes) {
|
|
58
|
+
nodeMap[node.name] = node
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const nodeNames = new Set(Object.keys(nodeMap))
|
|
62
|
+
|
|
63
|
+
for (const node of nodes) {
|
|
64
|
+
if (node.type === WorkflowNodeType.Action && !(node.name in transitions)) {
|
|
65
|
+
throw new Error(`missing transition table for action node "${node.name}"`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const rules of Object.values(transitions)) {
|
|
70
|
+
for (const rule of Object.values(rules)) {
|
|
71
|
+
if (typeof rule === 'function') {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (rule.nextPhase !== null && !nodeNames.has(rule.nextPhase)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`transition references unknown node "${rule.nextPhase}"`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
entry: nodes[0]!.name,
|
|
84
|
+
nodes: nodeMap,
|
|
85
|
+
transitions,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Artifact, TaskState, TaskStatus } from './state'
|
|
2
|
+
|
|
3
|
+
export enum WorkflowNodeType {
|
|
4
|
+
Action = 'action',
|
|
5
|
+
Branch = 'branch',
|
|
6
|
+
Gate = 'gate',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TypedArtifactMap {
|
|
10
|
+
get: <T = unknown>(kind: string) => Artifact<T> | undefined
|
|
11
|
+
has: (kind: string) => boolean
|
|
12
|
+
set: (artifact: Artifact) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WorkflowContext<TConfig = unknown> {
|
|
16
|
+
artifacts: TypedArtifactMap
|
|
17
|
+
config: TConfig
|
|
18
|
+
state: TaskState
|
|
19
|
+
subjectId: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DomainResult<
|
|
23
|
+
TKind extends string = string,
|
|
24
|
+
TPayload = unknown,
|
|
25
|
+
> {
|
|
26
|
+
kind: TKind
|
|
27
|
+
payload?: TPayload
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ActionResult {
|
|
31
|
+
artifact?: Artifact
|
|
32
|
+
result: DomainResult
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ActionRunFn<TConfig = unknown> = (
|
|
36
|
+
ctx: WorkflowContext<TConfig>,
|
|
37
|
+
) => Promise<ActionResult>
|
|
38
|
+
|
|
39
|
+
export type GateTestFn<TConfig = unknown> = (
|
|
40
|
+
ctx: WorkflowContext<TConfig>,
|
|
41
|
+
) => Promise<boolean>
|
|
42
|
+
|
|
43
|
+
export type BranchDecideFn<TConfig = unknown> = (
|
|
44
|
+
ctx: WorkflowContext<TConfig>,
|
|
45
|
+
) => Promise<string>
|
|
46
|
+
|
|
47
|
+
export interface ActionNode<TConfig = unknown> {
|
|
48
|
+
name: string
|
|
49
|
+
run: ActionRunFn<TConfig>
|
|
50
|
+
type: WorkflowNodeType.Action
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface GateNode<TConfig = unknown> {
|
|
54
|
+
name: string
|
|
55
|
+
otherwise: string
|
|
56
|
+
test: GateTestFn<TConfig>
|
|
57
|
+
then: string
|
|
58
|
+
type: WorkflowNodeType.Gate
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BranchNode<TConfig = unknown> {
|
|
62
|
+
decide: BranchDecideFn<TConfig>
|
|
63
|
+
name: string
|
|
64
|
+
paths: Record<string, string>
|
|
65
|
+
type: WorkflowNodeType.Branch
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type WorkflowNode<TConfig = unknown> =
|
|
69
|
+
| ActionNode<TConfig>
|
|
70
|
+
| BranchNode<TConfig>
|
|
71
|
+
| GateNode<TConfig>
|
|
72
|
+
|
|
73
|
+
export interface Transition {
|
|
74
|
+
nextPhase: null | string
|
|
75
|
+
status: TaskStatus
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type TransitionRule =
|
|
79
|
+
| ((input: { result: DomainResult; state: TaskState }) => Transition)
|
|
80
|
+
| Transition
|
|
81
|
+
|
|
82
|
+
export interface WorkflowProgram<TConfig = unknown> {
|
|
83
|
+
entry: string
|
|
84
|
+
nodes: Record<string, WorkflowNode<TConfig>>
|
|
85
|
+
transitions: Record<string, Record<string, TransitionRule>>
|
|
86
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface AgentInvocation {
|
|
2
|
+
outputSchema: Record<string, unknown>
|
|
3
|
+
prompt: string
|
|
4
|
+
role: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface AgentPort {
|
|
8
|
+
execute: (invocation: AgentInvocation) => Promise<unknown>
|
|
9
|
+
name: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createRoleInvocation(
|
|
13
|
+
role: string,
|
|
14
|
+
input: Omit<AgentInvocation, 'role'>,
|
|
15
|
+
): AgentInvocation {
|
|
16
|
+
return { ...input, role }
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PullRequestSnapshot } from '../core/runtime'
|
|
2
|
+
|
|
3
|
+
export interface CodeHostPort {
|
|
4
|
+
createPullRequest: (input: {
|
|
5
|
+
baseBranch: string
|
|
6
|
+
body: string
|
|
7
|
+
headBranch: string
|
|
8
|
+
title: string
|
|
9
|
+
}) => Promise<{ number: number; url: string }>
|
|
10
|
+
findMergedPullRequestByHeadBranch: (input: {
|
|
11
|
+
headBranch: string
|
|
12
|
+
}) => Promise<null | { mergeCommitSha: string; number: number }>
|
|
13
|
+
findOpenPullRequestByHeadBranch: (input: {
|
|
14
|
+
headBranch: string
|
|
15
|
+
}) => Promise<null | { number: number }>
|
|
16
|
+
getPullRequestSnapshot: (input: {
|
|
17
|
+
pullRequestNumber: number
|
|
18
|
+
}) => Promise<PullRequestSnapshot>
|
|
19
|
+
squashMergePullRequest: (input: {
|
|
20
|
+
pullRequestNumber: number
|
|
21
|
+
subject: string
|
|
22
|
+
}) => Promise<{ commitSha: string }>
|
|
23
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { TaskStatus, type Artifact } from '../harness/state'
|
|
5
|
+
import { action, sequence } from '../harness/workflow-builders'
|
|
6
|
+
import { writeJsonAtomic } from '../utils/fs'
|
|
7
|
+
|
|
8
|
+
import type { BatchStructuredOutputProvider } from '../batch/provider'
|
|
9
|
+
import type { WorkflowProgram } from '../harness/workflow-program'
|
|
10
|
+
|
|
11
|
+
export enum BatchPhase {
|
|
12
|
+
Persist = 'persist',
|
|
13
|
+
Prepare = 'prepare',
|
|
14
|
+
Process = 'process',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export enum BatchResult {
|
|
18
|
+
PersistCompleted = 'persist.completed',
|
|
19
|
+
PrepareCompleted = 'prepare.completed',
|
|
20
|
+
ProcessCompleted = 'process.completed',
|
|
21
|
+
ProcessRetryRequested = 'process.retry_requested',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export enum BatchArtifactKind {
|
|
25
|
+
PersistResult = 'persist_result',
|
|
26
|
+
PrepareResult = 'prepare_result',
|
|
27
|
+
ProcessResult = 'process_result',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createBatchProgram(deps: {
|
|
31
|
+
configDir: string
|
|
32
|
+
maxRetries: number
|
|
33
|
+
outputSchema: Record<string, unknown>
|
|
34
|
+
prompt: string
|
|
35
|
+
provider: BatchStructuredOutputProvider
|
|
36
|
+
results: Record<string, unknown>
|
|
37
|
+
resultsPath: string
|
|
38
|
+
validateOutput: (value: unknown) => void
|
|
39
|
+
}): WorkflowProgram {
|
|
40
|
+
return sequence(
|
|
41
|
+
[
|
|
42
|
+
action(BatchPhase.Prepare, {
|
|
43
|
+
async run(ctx) {
|
|
44
|
+
const content = await readFile(
|
|
45
|
+
path.join(deps.configDir, ctx.subjectId),
|
|
46
|
+
'utf8',
|
|
47
|
+
)
|
|
48
|
+
const artifact: Artifact<{ content: string; filePath: string }> = {
|
|
49
|
+
id: `${encodeURIComponent(ctx.subjectId)}:${BatchArtifactKind.PrepareResult}`,
|
|
50
|
+
kind: BatchArtifactKind.PrepareResult,
|
|
51
|
+
payload: { content, filePath: ctx.subjectId },
|
|
52
|
+
subjectId: ctx.subjectId,
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
artifact,
|
|
57
|
+
result: { kind: BatchResult.PrepareCompleted },
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
action(BatchPhase.Process, {
|
|
62
|
+
async run(ctx) {
|
|
63
|
+
const prepareArtifact = ctx.artifacts.get<{
|
|
64
|
+
content: string
|
|
65
|
+
filePath: string
|
|
66
|
+
}>(BatchArtifactKind.PrepareResult)
|
|
67
|
+
try {
|
|
68
|
+
const output = await deps.provider.runFile({
|
|
69
|
+
content: prepareArtifact!.payload.content,
|
|
70
|
+
filePath: prepareArtifact!.payload.filePath,
|
|
71
|
+
outputSchema: deps.outputSchema,
|
|
72
|
+
prompt: deps.prompt,
|
|
73
|
+
})
|
|
74
|
+
deps.validateOutput(output)
|
|
75
|
+
const artifact: Artifact<{ output: unknown }> = {
|
|
76
|
+
id: `${encodeURIComponent(ctx.subjectId)}:${BatchArtifactKind.ProcessResult}`,
|
|
77
|
+
kind: BatchArtifactKind.ProcessResult,
|
|
78
|
+
payload: { output },
|
|
79
|
+
subjectId: ctx.subjectId,
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
artifact,
|
|
84
|
+
result: { kind: BatchResult.ProcessCompleted },
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
return {
|
|
88
|
+
result: { kind: BatchResult.ProcessRetryRequested },
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
action(BatchPhase.Persist, {
|
|
94
|
+
async run(ctx) {
|
|
95
|
+
const processArtifact = ctx.artifacts.get<{ output: unknown }>(
|
|
96
|
+
BatchArtifactKind.ProcessResult,
|
|
97
|
+
)
|
|
98
|
+
deps.results[ctx.subjectId] = processArtifact!.payload.output
|
|
99
|
+
await writeJsonAtomic(deps.resultsPath, deps.results)
|
|
100
|
+
const artifact: Artifact<{ filePath: string; persisted: boolean }> = {
|
|
101
|
+
id: `${encodeURIComponent(ctx.subjectId)}:${BatchArtifactKind.PersistResult}`,
|
|
102
|
+
kind: BatchArtifactKind.PersistResult,
|
|
103
|
+
payload: { filePath: ctx.subjectId, persisted: true },
|
|
104
|
+
subjectId: ctx.subjectId,
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
artifact,
|
|
109
|
+
result: { kind: BatchResult.PersistCompleted },
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
],
|
|
114
|
+
{
|
|
115
|
+
[BatchPhase.Persist]: {
|
|
116
|
+
[BatchResult.PersistCompleted]: {
|
|
117
|
+
nextPhase: null,
|
|
118
|
+
status: TaskStatus.Done,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
[BatchPhase.Prepare]: {
|
|
122
|
+
[BatchResult.PrepareCompleted]: {
|
|
123
|
+
nextPhase: BatchPhase.Process,
|
|
124
|
+
status: TaskStatus.Running,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
[BatchPhase.Process]: {
|
|
128
|
+
[BatchResult.ProcessCompleted]: {
|
|
129
|
+
nextPhase: BatchPhase.Persist,
|
|
130
|
+
status: TaskStatus.Running,
|
|
131
|
+
},
|
|
132
|
+
[BatchResult.ProcessRetryRequested]: (input) =>
|
|
133
|
+
input.state.iteration >= deps.maxRetries
|
|
134
|
+
? { nextPhase: null, status: TaskStatus.Blocked }
|
|
135
|
+
: { nextPhase: BatchPhase.Process, status: TaskStatus.Suspended },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
}
|