-
Notifications
You must be signed in to change notification settings - Fork 164
Expand file tree
/
Copy pathworkflows.ts
More file actions
147 lines (138 loc) · 6.18 KB
/
Copy pathworkflows.ts
File metadata and controls
147 lines (138 loc) · 6.18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import { tool } from "@opencode-ai/plugin/tool"
import { createCodeNomadRequester, type CodeNomadConfig } from "./request.js"
type WorkflowStatus = "running" | "waiting_for_review" | "completed" | "failed" | "cancelled" | "interrupted"
type WorkflowRun = {
id: string
objective: string
status: WorkflowStatus
error?: string
pendingReviewStepId?: string
steps: Array<{
id: string
title: string
status: string
sessionId?: string
output?: unknown
outputTruncated?: boolean
}>
}
function modelConfig(providerID?: string, modelID?: string) {
if (Boolean(providerID) !== Boolean(modelID)) {
throw new Error("Provider ID and model ID must be supplied together.")
}
return providerID && modelID ? { providerID, modelID } : undefined
}
function summarize(run: WorkflowRun) {
const steps = run.steps
.map((step) => `${step.title}: ${step.status}${step.sessionId ? ` (${step.sessionId})` : ""}`)
.join("\n")
const review = run.status === "waiting_for_review"
? "\nHuman review is required in CodeNomad before the workflow can continue or complete."
: ""
return `Workflow ${run.id}\nStatus: ${run.status}\n${steps}${review}${run.error ? `\nError: ${run.error}` : ""}`
}
function details(run: WorkflowRun) {
const reviewed = run.steps.find((step) => step.id === run.pendingReviewStepId)
const output = reviewed?.output === undefined ? "" : `\nPending review:\n${JSON.stringify(reviewed.output, null, 2)}`
const truncated = reviewed?.outputTruncated
? `\nThis output is truncated. Review the full generated session${reviewed.sessionId ? ` ${reviewed.sessionId}` : ""} before approval.`
: ""
return `${summarize(run)}${output}${truncated}`
}
export const describeWorkflowDetails = details
export function describeWorkflowStart(run: WorkflowRun, hasApprovalGate: boolean) {
const gateMessage = hasApprovalGate
? "The workflow will pause at its configured human approval gates."
: "This workflow has no human approval gate."
return `${summarize(run)}\n${gateMessage}`
}
export function createWorkflowTools(config: CodeNomadConfig) {
const requester = createCodeNomadRequester(config)
const request = <T>(path: string, init?: RequestInit) => requester.requestJson<T>(`/workflow-runs${path}`, init)
return {
start_codenomad_workflow: tool({
description:
"Start a host-managed sequential workflow. Stages may require explicit human review before the next stage runs.",
args: {
objective: tool.schema.string().describe("The objective for the workflow"),
stages: tool.schema.array(tool.schema.object({
id: tool.schema.string().describe("Stable stage ID using letters, numbers, underscore, or dash"),
title: tool.schema.string().describe("Human-readable stage title"),
instructions: tool.schema.string().describe("Instructions for this stage"),
agent: tool.schema.string().optional().describe("Optional OpenCode agent"),
provider_id: tool.schema.string().optional().describe("Optional model provider ID"),
model_id: tool.schema.string().optional().describe("Optional model ID"),
requires_approval: tool.schema.boolean().optional().describe("Pause for human review after this stage"),
})).min(1).max(12).optional().describe("Ordered workflow stages; defaults to Planner then Implementer"),
},
async execute(args, context) {
const stages = args.stages?.map((stage) => {
const model = modelConfig(stage.provider_id, stage.model_id)
return {
id: stage.id,
title: stage.title,
instructions: stage.instructions,
...(stage.agent ? { agent: stage.agent } : {}),
...(model ? { model } : {}),
requiresApproval: Boolean(stage.requires_approval),
}
}) ?? [
{
id: "planner",
title: "Planner",
instructions: "Create a concise implementation plan with ordered, verifiable steps.",
requiresApproval: true,
},
{
id: "implementer",
title: "Implementer",
instructions: "Implement the approved plan and run focused validation.",
requiresApproval: false,
},
]
const run = await request<WorkflowRun>("", {
method: "POST",
body: JSON.stringify({
objective: args.objective,
initiatorSessionId: context.sessionID,
stages,
}),
})
return describeWorkflowStart(run, stages.some((stage) => stage.requiresApproval))
},
}),
list_codenomad_workflows: tool({
description: "List workflow runs managed by CodeNomad for this workspace.",
args: {},
async execute() {
const response = await request<{ runs: WorkflowRun[] }>("")
if (response.runs.length === 0) return "No CodeNomad workflow runs found."
return response.runs.map((run) => `${run.id} | ${run.status} | ${run.objective}`).join("\n")
},
}),
get_codenomad_workflow: tool({
description: "Inspect one CodeNomad workflow run and its role sessions.",
args: { run_id: tool.schema.string().describe("Workflow run ID") },
async execute(args) {
return details(await request<WorkflowRun>(`/${encodeURIComponent(args.run_id)}`))
},
}),
approve_codenomad_workflow: tool({
description:
"Show the pending approval details. Only a user in the CodeNomad Workflows panel can approve and continue the workflow.",
args: { run_id: tool.schema.string().describe("Workflow run ID") },
async execute(args) {
const run = await request<WorkflowRun>(`/${encodeURIComponent(args.run_id)}`)
return `${details(run)}\nApproval was not applied. Ask the user to approve this run in the CodeNomad Workflows panel.`
},
}),
cancel_codenomad_workflow: tool({
description: "Cancel a running or review-pending CodeNomad workflow.",
args: { run_id: tool.schema.string().describe("Workflow run ID") },
async execute(args) {
const run = await request<WorkflowRun>(`/${encodeURIComponent(args.run_id)}/cancel`, { method: "POST" })
return summarize(run)
},
}),
}
}