Add server-side workflows with YAML files, task freeze and lead tools
This commit is contained in:
@@ -10,8 +10,6 @@
|
||||
|
||||
# M5 — Workflows
|
||||
|
||||
- Workflow definitions and storage
|
||||
A workflow is a sequence of stages, each with a role prompt template, a vendor and model, typed artifact inputs and outputs, an optional gate, and optional review profiles. Definitions, prompt templates and review profiles live on the server, not in repos, and export and import as files. A task freezes its workflow definition at intake, so later edits to the workflow don't change running tasks. No general DAG engine until a real workflow needs one.
|
||||
- Stages, gates and bounded loops
|
||||
Gates are human approval through the inbox, a review pass or an eval pass. Loops are bounded, for example `on_fail: { goto: build, max: 3, then: escalate }`.
|
||||
- Plan artifact
|
||||
|
||||
@@ -236,6 +236,22 @@ paths:
|
||||
schema: { $ref: "#/components/schemas/Task" }
|
||||
"404": { $ref: "#/components/responses/Error" }
|
||||
|
||||
/api/tasks/{taskId}/workflow:
|
||||
parameters:
|
||||
- { name: taskId, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
operationId: getTaskWorkflow
|
||||
description: >
|
||||
The workflow the task froze when it was created, as it was then. Later edits and deletes of the workflow
|
||||
don't change it.
|
||||
responses:
|
||||
"200":
|
||||
description: The frozen workflow.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/TaskWorkflow" }
|
||||
"404": { $ref: "#/components/responses/Error" }
|
||||
|
||||
/api/tasks/{taskId}/unmerged:
|
||||
parameters:
|
||||
- { name: taskId, in: path, required: true, schema: { type: string } }
|
||||
@@ -781,6 +797,54 @@ paths:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Status" }
|
||||
|
||||
/api/workflows:
|
||||
get:
|
||||
operationId: listWorkflows
|
||||
responses:
|
||||
"200":
|
||||
description: Every workflow's name and description, by name.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/WorkflowSummary" }
|
||||
|
||||
/api/workflows/{name}:
|
||||
parameters:
|
||||
- { name: name, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
operationId: getWorkflow
|
||||
responses:
|
||||
"200":
|
||||
description: "The workflow's file: its WorkflowDefinition as YAML, as an attachment named after it."
|
||||
content:
|
||||
application/yaml:
|
||||
schema: { type: string }
|
||||
"404": { $ref: "#/components/responses/Error" }
|
||||
put:
|
||||
operationId: putWorkflow
|
||||
description: >
|
||||
Creates or replaces the workflow from its file, a WorkflowDefinition as YAML (or JSON). The name is one line
|
||||
of 1 to 100 characters. Tasks that already froze the workflow keep their copy.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/yaml:
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200":
|
||||
description: The stored workflow.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/WorkflowSummary" }
|
||||
"400": { $ref: "#/components/responses/Error" }
|
||||
delete:
|
||||
operationId: deleteWorkflow
|
||||
responses:
|
||||
"204":
|
||||
description: Deleted. Tasks that froze it keep their copy.
|
||||
"404": { $ref: "#/components/responses/Error" }
|
||||
|
||||
components:
|
||||
responses:
|
||||
Error:
|
||||
@@ -868,6 +932,7 @@ components:
|
||||
forked_from: { type: string, description: "The task this one was forked from; absent for other tasks." }
|
||||
forked_at: { type: integer, description: "With forked_from: the n of the snapshot it was forked at." }
|
||||
merge_queued_at: { type: string, format: date-time, description: "When Merge was requested while another task in the project was merging; absent once the merge starts or when none is held." }
|
||||
workflow: { type: string, description: "The name of the workflow the task froze at creation (getTaskWorkflow has its copy); absent for a task without one." }
|
||||
|
||||
Todo:
|
||||
type: object
|
||||
@@ -917,6 +982,48 @@ components:
|
||||
required: [title]
|
||||
properties:
|
||||
title: { type: string }
|
||||
workflow: { type: string, description: "The name of a workflow for the task to freeze: it keeps a copy of the workflow's definition as it is now." }
|
||||
|
||||
WorkflowSummary:
|
||||
type: object
|
||||
required: [name, description]
|
||||
properties:
|
||||
name: { type: string }
|
||||
description: { type: string }
|
||||
|
||||
WorkflowDefinition:
|
||||
type: object
|
||||
description: >
|
||||
A workflow's file, which holds everything but its name: a sequence of stages run in order. Sent and returned
|
||||
as YAML.
|
||||
required: [stages]
|
||||
properties:
|
||||
description: { type: string, x-go-type-skip-optional-pointer: true }
|
||||
stages:
|
||||
type: array
|
||||
minItems: 1
|
||||
items: { $ref: "#/components/schemas/Stage" }
|
||||
|
||||
Stage:
|
||||
type: object
|
||||
required: [name, vendor, model, prompt]
|
||||
properties:
|
||||
name: { type: string, description: "One line of 1 to 100 characters, unique within the workflow." }
|
||||
vendor:
|
||||
type: string
|
||||
enum: [claude, codex]
|
||||
x-enum-varnames: [StageVendorClaude, StageVendorCodex]
|
||||
description: Must be the vendor that runs model (gpt-* models run on Codex, every other model on Claude Code).
|
||||
model: { type: string }
|
||||
prompt: { type: string, description: "The stage agent's instructions." }
|
||||
|
||||
TaskWorkflow:
|
||||
type: object
|
||||
description: The workflow a task froze when it was created, as it was then.
|
||||
required: [name, definition]
|
||||
properties:
|
||||
name: { type: string }
|
||||
definition: { type: string, description: The WorkflowDefinition as YAML. }
|
||||
|
||||
TaskChanges:
|
||||
type: object
|
||||
|
||||
+15
-1
@@ -11,6 +11,7 @@
|
||||
- `internal/claude`: the Claude Code stream-json client (see `docs/claude-protocol.md`).
|
||||
- `internal/codex`: the `codex app-server` JSON-RPC client, with types generated from the CLI's schema by `go generate ./internal/codex` (see `docs/codex-protocol.md`).
|
||||
- `internal/tailscale`: `tailscale serve` for task previews (see "Previews").
|
||||
- `internal/workflow`: validating workflow definitions and reading and writing their YAML files (see "Workflows").
|
||||
- `web/`: the SPA, frameworkless TypeScript compiled file-by-file to ES modules (no bundler). `web/src/*.ts` is the source; `web/src/api.gen.ts` holds types generated from `api/openapi.yaml`, and `web/src/api.ts` is the typed client over them. `go generate ./web` runs `npm ci`, regenerates the types and compiles `web/dist/`, which is committed so `go install` needs no Node. The binary embeds `index.html`, `style.css`, `dist/`, `src/` (for source maps), `fonts/` and `vendor/` (Mermaid, see "Inbox"; xterm.js, see "Terminals"). `byre serve --dev` serves `./web` from disk instead (run from the repo root); pair it with `npm --prefix web exec tsc -- --watch`.
|
||||
|
||||
## Data
|
||||
@@ -145,7 +146,7 @@ Each user has one long-running lead agent, shown in the left pane. It is not tie
|
||||
- **Sessions.** A lead session is an `agent_sessions` row with `owner` set to the user's login, `role` `lead` and no `task_id`. Its events carry `owner` and no project or task. It runs in `leads/<login>` with `leadGuidance` (passed and re-attached the same way as a task agent's guidance) and the lead tools instead of the task tools. It has no Stop-hook bounce, no snapshots, no end-of-turn commit and no turn summary; a turn that ends in error records the error on its `session.state` event, which the pane shows as a note.
|
||||
- **Messages.** `POST /api/me/lead/messages` sends the caller's message to their lead, starting a lead session when there is none. While the lead is mid-turn the message is held in `lead_inputs`; the lead's PostToolUse hook (Codex: `turn/steer` after each completed tool item) hands held messages over after its next tool call, like a task's queued messages (see "Agent sessions"), and any still held become its next turn. Signals are never handed over mid-turn. The composer stays enabled. `GET /api/me/lead/sessions` lists the caller's lead sessions; interrupt and compact use the usual session endpoints.
|
||||
- **Signals.** Every published event goes through `Manager.Observe`. These become signals in `lead_inputs` for every user who has a lead session: `inbox.created` (a question with its choices and recommendation, an error or rate limit), `inbox.updated` when a human answered a question or error, `task.created` (naming the TODO item it was started from, if any), `task.merged`, `task.discarded`, and `turn.ended` with `summary: true` ("<task> finished a turn:" with its `result` cut to 1500 characters). The human switching the right pane is not a signal. A signal caused by the lead itself (actor `<login> (lead)`) is skipped, as is the update from an answer it gave. A task's `task.created` event carries the actor that created it, separately from its `created_by` login: tasks the lead creates with `create_task` or `start_todo` keep the human's login as `created_by` but have actor `<login> (lead)`. Whenever the lead is idle, byre sends the oldest held message from the human as its own turn, or else every held signal as one `byre` message headed "Signals since your last turn (n)". Its guidance tells it not to respond to the human's own actions, and to make its final message exactly `NO_REPLY` when a batch needs nothing from it. A transcript (`quietReply` in `web/src/task.ts`) never draws an `agent.message` whose last line is `NO_REPLY` (the model sometimes writes a sentence before it), and hides the live partial message while its text is still a prefix of it; the partial message starts hidden and is first shown by its 100ms render, so an empty one never takes up space. A lead turn that byre starts without a handoff (a signal batch or a handoff continuation) is held (`ctx.held`): its `byre` message isn't drawn and doesn't open the turn, so the spinner stays off, and its partial message stays hidden, until the lead draws something else, such as a tool call, thinking or a real message, which draws the `byre` message ahead of it. When such a turn ends in `NO_REPLY`, everything it drew is removed with it (its `byre` message, tool calls, thinking), so the transcript keeps nothing of it; a turn that ends in a real message keeps all of it.
|
||||
- **Tools.** `list_tasks` (each project's active tasks, then a line listing the titles of its `TODO.md` items on the default branch that no active task was started from), `read_transcript` (a task's events in brief, the last `limit` or those after `after_seq`), `read_inbox`, `answer_question` (an `option`, `other` or `dismiss` response recorded as the user, through the same checks as `POST /api/inbox/responses`), `send_message` (starts the task's agent if it has none; refused while it is mid-turn), `create_task`, `start_todo`, `interrupt`, `compact_task`, `merge_task`, `discard_task`, `show` and `compact_self`. The lead also has Claude Code's own tools and reads worktrees by path. Messages it sends task agents have actor `<login> (lead)`, so they don't make the user a participant; tasks, merges, discards and answers are done as the user. Its guidance says to answer a question only when the answer is clear or the human gave it, and to merge or discard only when the human asks.
|
||||
- **Tools.** `list_tasks` (each project's active tasks, then a line listing the titles of its `TODO.md` items on the default branch that no active task was started from), `read_transcript` (a task's events in brief, the last `limit` or those after `after_seq`), `read_inbox`, `answer_question` (an `option`, `other` or `dismiss` response recorded as the user, through the same checks as `POST /api/inbox/responses`), `send_message` (starts the task's agent if it has none; refused while it is mid-turn), `create_task` and `start_todo` (each with an optional `workflow` to freeze), `interrupt`, `compact_task`, `merge_task`, `discard_task`, `show`, `compact_self`, and the workflow tools (see "Workflows"). The lead also has Claude Code's own tools and reads worktrees by path. Messages it sends task agents have actor `<login> (lead)`, so they don't make the user a participant; tasks, merges, discards and answers are done as the user. Its guidance says to answer a question only when the answer is clear or the human gave it, and to merge or discard only when the human asks.
|
||||
- **Focus.** `users.focus` holds what the user's right pane shows: `project_id`, `task_id`, `tabs` (the tabs put first, in order) and `tab` (the selected one). With a `task_id`, `project_id` is always that task's project, filled in by the server; with only a `project_id`, the pane shows that project with no task, which only the Project tab has anything for. `GET /api/me/focus` reads it; `PUT /api/me/focus` and the lead's `show` set it, refusing an unknown tab, a task that isn't active or an unknown project, and record a `focus.updated` event owned by the user. The page applies the focus on load and on every `focus.updated` it didn't cause itself; the human's own changes (⌘K, the right pane's task picker, clicking a tab, a task link, a notification) are sent with `PUT /api/me/focus`. The URL follows the focus: `#/task/<id>`, `#/project/<id>`, or `#/` for neither.
|
||||
- **Pane lock.** A `show` of what the pane already shows (the same task, or the same project with no task, on the same selected tab; `sameView`) tells the lead so and applies only its `tabs` order, whether or not the human is in the pane, so the human's selected tab stays selected. Otherwise, while the human is working in the right pane, the lead's `show` is refused with an error telling it to say in chat what it would have shown, unless its selected tab is `project` (the fallback the lead shows when nothing needs the human, so the human being there doesn't hold it) or the pane is finished (`Manager.paneFinished`): its task is merged or discarded, or its selected tab is `inbox` and the task has no open items. When an `inbox.updated`, `task.merged` or `task.discarded` event leaves a user's focused task finished that way, that user's signal for it gets a line "Your right pane is free: <task> has nothing left in its inbox." (or "is merged."/"is discarded."), even when the event itself makes no signal, such as a dismissed error; `inbox.updated` on a task that is no longer active adds none. The lead's guidance tells it to show the next thing that needs the human on that line, or, when nothing does, the project's Project tab with a short status in chat: one line per active task, then up to three TODO items it would start next, each with a reason, starting one only when the human picks it. It writes each item as a link to `#/todo/<project_id>/<URL-encoded title>`; Markdown views draw that (`web/src/todolink.ts`) as the title with a **Start** button that calls `POST /api/projects/{id}/todo/tasks` with the first model, as the Project tab's does, or, once an active task was started from that item, the task's colour, dot and state, linking to it. A chat view refills them whenever its transcript grows or tasks or sessions change. When that status says nothing new since its last one, it replies `NO_REPLY` instead. The page sends `PUT /api/me/pane-focus` with `focused` whenever that changes: true while the browser window has focus and the focused element is inside the right pane. The pane itself is focusable (`tabIndex` -1), so a click anywhere in it counts, not only on inputs and buttons; a re-render that removes the focused element hands focus back to the pane rather than counting as leaving. The page sends false on `pagehide` (with `keepalive`) and resends true when its event stream reconnects. byre keeps the flag in memory per user (`Manager.paneFocused`), so it starts false after a restart until the page resends it.
|
||||
- **Start hold.** Start on a TODO item in the Project tab leaves the pane where it is. When the user starts one themselves (`StartTodo` with the user as actor, not the lead's `start_todo`), byre sets `Manager.startedTodo` for them, and while it is set and the pane's selected tab is `project`, every `show` that would change the view is refused, focused or not, with an error telling the lead the human is likely to start more. The user's own `PUT /api/me/focus` (switching task or tab) or a message to the lead clears it. It is in memory, so a restart clears it.
|
||||
@@ -236,6 +237,8 @@ The transcript is the timeline. The UI learns a task's snapshots from the `snaps
|
||||
|
||||
The cut point comes from `vendor_uuid`: every top-level `agent.message`, `agent.thinking`, `agent.tool_call` and `agent.tool_result` event (not a subagent's) carries the `uuid` of the Claude Code stream-json line it came from, which is the id of that entry in the CLI's own session transcript. `forkPoint` takes the last such event of the snapshot's session at or before the snapshot's `event_seq`, then extends the cut through the following tool calls and results until every tool call it has kept has its result, so the fork never resumes on an unanswered tool call. For a `turn_end` snapshot the cut is the turn's last message. A Codex session's events carry no `vendor_uuid` except its `turn.ended`, which carries the turn id, so for Codex `forkPoint` picks the last turn that ended at or before the snapshot. Snapshots whose session recorded no `vendor_uuid` by then, such as those from before byre recorded it, can't be forked.
|
||||
|
||||
A fork of a task that froze a workflow gets a copy of the source's frozen workflow, not the library's current one (see "Workflows").
|
||||
|
||||
The fork's session guidance gets one more line (`forkGuidance` in `internal/agents/guidance.go`) naming the source task, the snapshot, the fork's branch and worktree and the source's worktree, telling the agent that everything before the fork happened in the other worktree and to work only in its own. Claude Code can reuse the system prompt recorded for the source conversation on a fork, the source's cwd included, so the `UserPromptSubmit` hook's copy is what reliably tells it. The fork's transcript starts with a note from its `task.created` event, "forked this task from <source> at snapshot #n", linking the source while it is active; the lead's signal for it reads "<task> was forked by <user> from <source> at snapshot n", and `list_tasks` notes the source. The fork's transcript shows only its own events, not the source's history.
|
||||
|
||||
## Reviews
|
||||
@@ -249,6 +252,17 @@ Every new snapshot of a task gets a review: a separate agent session on the vend
|
||||
- **Events.** `review.started` and `review.finished` (state `done`, `failed` or `skipped`; a skipped review emits only `review.finished`) carry the whole review, with the task's ids and no `agent_session_id`. A reviewer's own events carry its session id and the task's ids: the transcript skips every event of a session whose `session.created` has `role` `reviewer`, and draws each `review.finished` as a note with its outcome; the lead's `read_transcript` leaves the reviewer's events out and shows each `review.finished` with its summary and findings (`reviewLine`). `GET /api/tasks/{taskId}/sessions` and everything that looks for the task's agent (`ListSessions`) see only implementer sessions.
|
||||
- **UI.** The **Reviews** tab (`web/src/reviews.ts`) shows one review at a time, picked by snapshot (see "Snapshots" for the picker): its snapshot, state or severity counts, error, `change`, summary, screenshots and findings. A finding with `path` and `line` shows, under its title, the lines `line` to `end_line` plus 3 either side from that file's diff for the snapshot's step (`changes?snapshot=<n>&step=true`, the diff the reviewer was given), with new-file line numbers and the finding's own lines marked; it shows nothing when those lines aren't in the diff, and "diff not available" with the error once the task is closed. Screenshots link to the full image. Each finding has a **fix** button (also on review cards in the inbox) that sends the finding's severity, title, location and detail to the task's agent as a message from the user: through `POST /api/sessions/{id}/messages` when its latest live session is idle, starting a session when it has none, and refused with an alert while the agent is mid-turn. Once sent, the right pane switches to that task's Transcript tab. It loads when it opens, when the task changes, and on each `review.*` event in the task's stream. When the finished review of the highest-numbered snapshot has findings, the tab (or the More menu holding it) is highlighted with a badge of the finding count. Each snapshot row's **review** button carries a mark over its top-right corner from that snapshot's latest `review.*` event: a spinner while the review runs, a grey ● when it was skipped, a green ✓ when it finished with no findings, and, when it has findings, the button highlighted with the count in a badge. A failed review leaves the button unmarked. `GET /api/tasks/{taskId}/reviews` lists a task's reviews oldest first. `sqlite3 byre.db "SELECT id, task_id, snapshot_n, vendor, state, error FROM reviews ORDER BY created_at DESC LIMIT 20"` shows recent ones.
|
||||
|
||||
## Workflows
|
||||
|
||||
A workflow is a named sequence of stages, each a stage name, a vendor, a model and the stage agent's prompt written out in full. Workflows live in byre's database, in the `workflows` table (`name`, and the definition's JSON as `definition`), shared by every project and never in a repo. Nothing runs a workflow yet: byre stores, validates, exports, imports and freezes them.
|
||||
|
||||
- **Names.** The name is the workflow's key and is shown to the human on buttons and pickers, so it is free text: one line of 1 to 100 characters, trimmed (`workflow.CleanName`). It isn't part of the definition, so the file doesn't carry it. In URLs it is a path-escaped segment.
|
||||
- **File.** One workflow per file: the `WorkflowDefinition` (`api/openapi.yaml`) as YAML, an optional `description` and its `stages`. `workflow.Format` writes it with multi-line prompts as literal blocks; `workflow.Parse` reads YAML or JSON and refuses unknown fields. The download is named after the workflow with `/\:*?"<>|` replaced by `_` (`workflow.Filename`).
|
||||
- **Validation.** `workflow.Validate` refuses a definition (400, everything wrong listed) with no stages, a stage name that isn't a valid one-line name or repeats another stage's, an empty prompt, or a `vendor` that isn't the one that runs its `model` (`workflow.VendorOf`, the same rule sessions use: `gpt-*` is Codex, everything else Claude Code).
|
||||
- **API.** `GET /api/workflows` lists every workflow's name and description. `GET /api/workflows/{name}` downloads its file; `PUT` takes a file as the body and creates or replaces the workflow; `DELETE` removes it. Each save or delete records a `workflow.updated` or `workflow.deleted` event carrying the name. `sqlite3 byre.db 'SELECT name FROM workflows'` lists them. Saves and reads go through the Manager (`internal/agents/workflows.go`) so the HTTP API and the lead's tools share them.
|
||||
- **Lead tools.** `list_workflows`, `read_workflow` (`name`, or `task_id` for a task's frozen copy), `save_workflow` (`name` and `definition`, the file as YAML) and `delete_workflow`, with the same checks and errors as the API. Task agents don't get them.
|
||||
- **Freezing.** `POST /api/projects/{id}/tasks`, and the lead's `create_task` and `start_todo`, take an optional `workflow` (404 when there is none of that name). The task copies the workflow's name and definition as they are at that moment into `tasks.workflow_name` and `tasks.workflow_definition`, so later saves or deletes don't change it. The Task's `workflow` is the name; `GET /api/tasks/{id}/workflow` returns the copy as `{name, definition}` with the definition as YAML (404 for a task without one). A task agent's `create_task` and the Project tab's Start create tasks without one; a fork copies its source's.
|
||||
|
||||
## Events
|
||||
|
||||
`GET /api/events` is server-sent events: `id` is the event's `seq`, `data` the Event JSON. Reconnecting with `Last-Event-ID` (EventSource does this itself) or `?after=` replays everything after that seq before going live. `?task_id=` limits the stream, replay and live, to one task's events (404 for an unknown task); `?lead=true` to the events of the caller's lead agent sessions. An event with an `owner` (a lead agent's events, a user's `focus.updated`) goes only to that user's streams; the others never see it. Unstored events (`seq` 0, currently only `agent.delta`) go out live only, without an `id` line, so they don't move `Last-Event-ID`. A client that falls further behind than its subscriber buffer (`subscribe` in `internal/server/hub.go`) is disconnected and catches up by reconnecting. Every 15s the server sends an `event: ping` so clients can tell a live stream from a dead one. The web UI (`subscribe` in `web/src/api.ts`) tracks the last seq it saw and opens a new stream from it when nothing (event or ping) has arrived for 40s, 2s after the browser gives up on one (as it does when `tailscale serve` answers 502 while byre restarts), when the browser comes back online, and on becoming visible if the stream is stale. While disconnected the navbar shows a red dot and "reconnecting…"; partial output (`agent.delta`) sent while disconnected is lost, and the finished `agent.message` replaces it.
|
||||
|
||||
@@ -7,6 +7,7 @@ require (
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/getkin/kin-openapi v0.142.0
|
||||
github.com/oapi-codegen/runtime v1.7.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
modernc.org/sqlite v1.60.1
|
||||
)
|
||||
|
||||
@@ -27,7 +28,6 @@ require (
|
||||
github.com/speakeasy-api/jsonpath v0.6.3 // indirect
|
||||
github.com/speakeasy-api/openapi v1.24.0 // indirect
|
||||
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
|
||||
+11
-1
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
)
|
||||
|
||||
func (m *Manager) Fork(ctx context.Context, src api.Task, project api.Project, n int, creator api.User) (api.Task, error) {
|
||||
@@ -33,6 +34,15 @@ func (m *Manager) Fork(ctx context.Context, src api.Task, project api.Project, n
|
||||
return api.Task{}, fmt.Errorf("%w: snapshot %d of task %s has no recorded agent message at or before it to fork the conversation from", ErrConflict, n, src.Id)
|
||||
}
|
||||
|
||||
var frozen *store.FrozenWorkflow
|
||||
if src.Workflow != nil {
|
||||
f, err := m.store.TaskWorkflow(ctx, src.Id)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
frozen = &f
|
||||
}
|
||||
|
||||
t, err := m.createTask(ctx, project, api.Task{
|
||||
Title: fmt.Sprintf("%s (fork of #%d)", src.Title, n),
|
||||
MergeTitle: src.MergeTitle,
|
||||
@@ -40,7 +50,7 @@ func (m *Manager) Fork(ctx context.Context, src api.Task, project api.Project, n
|
||||
TodoItem: src.TodoItem,
|
||||
ForkedFrom: &src.Id,
|
||||
ForkedAt: &n,
|
||||
}, snap.CommitSha, creator.Login)
|
||||
}, frozen, snap.CommitSha, creator.Login)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
"g.fc.run/misc/byre/internal/workflow"
|
||||
)
|
||||
|
||||
const leadGuidance = `You are the human's lead agent in byre, shown in the left pane of byre's web UI. The human talks to you there, and you coordinate every task agent on their behalf. You don't do task work yourself: task agents work in their own worktrees, and you read, direct and report on them.
|
||||
@@ -67,7 +68,7 @@ func (m *Manager) createLead(ctx context.Context, owner api.User, prompt, model,
|
||||
}
|
||||
|
||||
id := newUUID()
|
||||
vendor := vendorOf(model)
|
||||
vendor := workflow.VendorOf(model)
|
||||
sess := api.AgentSession{
|
||||
Id: id,
|
||||
Owner: owner.Login,
|
||||
|
||||
@@ -32,7 +32,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
||||
}
|
||||
taskOnly := `{"type": "object", "properties": {"task_id": {"type": "string"}}, "required": ["task_id"]}`
|
||||
|
||||
return []mcp.Tool{
|
||||
return append([]mcp.Tool{
|
||||
tool("list_tasks",
|
||||
"List every project and its active tasks (id, title, worktree path, the agent's state and model, and open inbox items), "+
|
||||
"and the titles of its TODO.md items on the default branch that no active task was started from.",
|
||||
@@ -162,7 +162,8 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
||||
"title": {"type": "string"},
|
||||
"prompt": {"type": "string"},
|
||||
"from_task_id": {"type": "string"},
|
||||
"model": {"type": "string"}
|
||||
"model": {"type": "string"},
|
||||
"workflow": {"type": "string", "description": "The name of a workflow for the task to freeze a copy of."}
|
||||
}, "required": ["project_id", "title", "prompt"]}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
args := struct {
|
||||
@@ -171,6 +172,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
||||
Prompt string `json:"prompt"`
|
||||
FromTaskID string `json:"from_task_id"`
|
||||
Model string `json:"model"`
|
||||
Workflow string `json:"workflow"`
|
||||
}{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
@@ -197,7 +199,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
task, err := m.CreateTask(ctx, project, args.Title, user.Login, base, leadActor(user.Login))
|
||||
task, err := m.CreateTask(ctx, project, args.Title, user.Login, base, leadActor(user.Login), args.Workflow)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -213,13 +215,15 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
||||
`{"type": "object", "properties": {
|
||||
"project_id": {"type": "string"},
|
||||
"title": {"type": "string"},
|
||||
"model": {"type": "string"}
|
||||
"model": {"type": "string"},
|
||||
"workflow": {"type": "string", "description": "The name of a workflow for the task to freeze a copy of."}
|
||||
}, "required": ["project_id", "title"]}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
args := struct {
|
||||
ProjectID string `json:"project_id"`
|
||||
Title string `json:"title"`
|
||||
Model string `json:"model"`
|
||||
Workflow string `json:"workflow"`
|
||||
}{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
@@ -229,7 +233,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
task, err := m.StartTodo(ctx, project, user, args.Title, m.modelOr(args.Model, l), leadActor(user.Login))
|
||||
task, err := m.StartTodo(ctx, project, user, args.Title, m.modelOr(args.Model, l), leadActor(user.Login), args.Workflow)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -362,7 +366,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool {
|
||||
m.mu.Unlock()
|
||||
return "byre will ask for your handoff once this turn ends.", nil
|
||||
}),
|
||||
}
|
||||
}, m.workflowTools(tool)...)
|
||||
}
|
||||
|
||||
func (m *Manager) SetFocus(ctx context.Context, login string, f api.Focus, actor string) error {
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"g.fc.run/misc/byre/internal/git"
|
||||
"g.fc.run/misc/byre/internal/mcp"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
"g.fc.run/misc/byre/internal/workflow"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -144,7 +145,7 @@ func (m *Manager) create(ctx context.Context, task api.Task, creator api.User, p
|
||||
}
|
||||
|
||||
id := newUUID()
|
||||
vendor := vendorOf(model)
|
||||
vendor := workflow.VendorOf(model)
|
||||
sess := api.AgentSession{
|
||||
Id: id,
|
||||
TaskId: task.Id,
|
||||
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/claude"
|
||||
"g.fc.run/misc/byre/internal/codex"
|
||||
"g.fc.run/misc/byre/internal/mcp"
|
||||
"g.fc.run/misc/byre/internal/workflow"
|
||||
)
|
||||
|
||||
type image struct {
|
||||
@@ -30,13 +30,6 @@ type agentProcess interface {
|
||||
drive(m *Manager, l *live)
|
||||
}
|
||||
|
||||
func vendorOf(model string) api.AgentSessionVendor {
|
||||
if strings.HasPrefix(model, "gpt-") {
|
||||
return api.Codex
|
||||
}
|
||||
return api.Claude
|
||||
}
|
||||
|
||||
func initialVendorSessionID(vendor api.AgentSessionVendor, id string) string {
|
||||
if vendor == api.Codex {
|
||||
return ""
|
||||
@@ -45,8 +38,8 @@ func initialVendorSessionID(vendor api.AgentSessionVendor, id string) string {
|
||||
}
|
||||
|
||||
func checkModel(sess api.AgentSession, model string) error {
|
||||
if vendorOf(model) != sess.Vendor {
|
||||
return fmt.Errorf("%w: session %s is %s; %s is a %s model", ErrInvalid, sess.Id, sess.Vendor, model, vendorOf(model))
|
||||
if workflow.VendorOf(model) != sess.Vendor {
|
||||
return fmt.Errorf("%w: session %s is %s; %s is a %s model", ErrInvalid, sess.Id, sess.Vendor, model, workflow.VendorOf(model))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,14 +16,22 @@ import (
|
||||
"g.fc.run/misc/byre/internal/todo"
|
||||
)
|
||||
|
||||
func (m *Manager) CreateTask(ctx context.Context, project api.Project, title, createdBy, base, actor string) (api.Task, error) {
|
||||
return m.createTask(ctx, project, api.Task{Title: title, CreatedBy: createdBy}, base, actor)
|
||||
func (m *Manager) CreateTask(ctx context.Context, project api.Project, title, createdBy, base, actor, workflowName string) (api.Task, error) {
|
||||
frozen, err := m.freeze(ctx, workflowName)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
return m.createTask(ctx, project, api.Task{Title: title, CreatedBy: createdBy}, frozen, base, actor)
|
||||
}
|
||||
|
||||
func (m *Manager) StartTodo(ctx context.Context, project api.Project, creator api.User, title, model, actor string) (api.Task, error) {
|
||||
func (m *Manager) StartTodo(ctx context.Context, project api.Project, creator api.User, title, model, actor, workflowName string) (api.Task, error) {
|
||||
if creator.GitName == "" || creator.GitEmail == "" {
|
||||
return api.Task{}, ErrNoGitIdentity
|
||||
}
|
||||
frozen, err := m.freeze(ctx, workflowName)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
|
||||
src, found, err := git.ReadFile(project.RepoPath, project.DefaultBranch, todo.File)
|
||||
if err != nil {
|
||||
@@ -47,7 +55,7 @@ func (m *Manager) StartTodo(ctx context.Context, project api.Project, creator ap
|
||||
}
|
||||
}
|
||||
|
||||
task, err := m.createTask(ctx, project, api.Task{Title: item.Title, CreatedBy: creator.Login, TodoItem: &item.Title}, project.DefaultBranch, actor)
|
||||
task, err := m.createTask(ctx, project, api.Task{Title: item.Title, CreatedBy: creator.Login, TodoItem: &item.Title}, frozen, project.DefaultBranch, actor)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
@@ -67,7 +75,7 @@ func todoPrompt(item api.TodoItem) string {
|
||||
todo.File, todo.Markdown(item))
|
||||
}
|
||||
|
||||
func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Task, base, actor string) (api.Task, error) {
|
||||
func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Task, frozen *store.FrozenWorkflow, base, actor string) (api.Task, error) {
|
||||
t.Id = store.NewID()
|
||||
t.ProjectId = project.Id
|
||||
if t.MergeTitle == "" {
|
||||
@@ -84,7 +92,7 @@ func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Tas
|
||||
return api.Task{}, err
|
||||
}
|
||||
|
||||
err = m.store.CreateTask(ctx, t, actor)
|
||||
err = m.store.CreateTask(ctx, t, frozen, actor)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
@@ -164,7 +172,7 @@ func (m *Manager) createTaskFromAgent(ctx context.Context, l *live, raw json.Raw
|
||||
model = sess.Model
|
||||
}
|
||||
|
||||
task, err := m.CreateTask(ctx, project, args.Title, creator.Login, base, creator.Login)
|
||||
task, err := m.CreateTask(ctx, project, args.Title, creator.Login, base, creator.Login, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
"g.fc.run/misc/byre/internal/mcp"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
"g.fc.run/misc/byre/internal/workflow"
|
||||
)
|
||||
|
||||
func (m *Manager) SaveWorkflow(ctx context.Context, name string, file []byte, actor string) (api.WorkflowSummary, error) {
|
||||
name, err := workflow.CleanName(name)
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
}
|
||||
def, err := workflow.Parse(file)
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
}
|
||||
err = m.store.PutWorkflow(ctx, name, def, actor)
|
||||
if err != nil {
|
||||
return api.WorkflowSummary{}, err
|
||||
}
|
||||
return api.WorkflowSummary{Name: name, Description: def.Description}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) WorkflowFile(ctx context.Context, name string) ([]byte, error) {
|
||||
def, err := m.store.GetWorkflow(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workflow.Format(def)
|
||||
}
|
||||
|
||||
func (m *Manager) TaskWorkflow(ctx context.Context, taskID string) (api.TaskWorkflow, error) {
|
||||
frozen, err := m.store.TaskWorkflow(ctx, taskID)
|
||||
if err != nil {
|
||||
return api.TaskWorkflow{}, err
|
||||
}
|
||||
file, err := workflow.Format(frozen.Definition)
|
||||
if err != nil {
|
||||
return api.TaskWorkflow{}, err
|
||||
}
|
||||
return api.TaskWorkflow{Name: frozen.Name, Definition: string(file)}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) freeze(ctx context.Context, name string) (*store.FrozenWorkflow, error) {
|
||||
if name == "" {
|
||||
return nil, nil
|
||||
}
|
||||
frozen, err := m.store.FreezeWorkflow(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &frozen, nil
|
||||
}
|
||||
|
||||
const workflowFileHelp = "A workflow's file is YAML: an optional description, then stages, each with name (one line, unique in the workflow), " +
|
||||
"vendor (claude or codex: gpt-* models run on codex, every other model on claude), model and prompt (the stage agent's instructions). " +
|
||||
"The workflow's name is not in the file."
|
||||
|
||||
func (m *Manager) workflowTools(tool func(name, description, schema string, run func(ctx context.Context, user api.User, raw json.RawMessage) (string, error)) mcp.Tool) []mcp.Tool {
|
||||
return []mcp.Tool{
|
||||
tool("list_workflows",
|
||||
"List the workflows on the server: each one's name and description.",
|
||||
`{"type": "object", "properties": {}}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
workflows, err := m.store.ListWorkflows(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(workflows) == 0 {
|
||||
return "No workflows.", nil
|
||||
}
|
||||
b := &strings.Builder{}
|
||||
for _, w := range workflows {
|
||||
fmt.Fprintf(b, "- %q", w.Name)
|
||||
if w.Description != "" {
|
||||
fmt.Fprintf(b, ": %s", w.Description)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return b.String(), nil
|
||||
}),
|
||||
|
||||
tool("read_workflow",
|
||||
"Read a workflow's file: with name, the workflow on the server; with task_id, the copy that task froze when it was created. "+workflowFileHelp,
|
||||
`{"type": "object", "properties": {
|
||||
"name": {"type": "string"},
|
||||
"task_id": {"type": "string"}
|
||||
}}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
args := struct {
|
||||
Name string `json:"name"`
|
||||
TaskID string `json:"task_id"`
|
||||
}{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if (args.Name == "") == (args.TaskID == "") {
|
||||
return "", fmt.Errorf("%w: give exactly one of name and task_id", ErrInvalid)
|
||||
}
|
||||
if args.TaskID != "" {
|
||||
tw, err := m.TaskWorkflow(ctx, args.TaskID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Task %s froze workflow %q:\n\n%s", args.TaskID, tw.Name, tw.Definition), nil
|
||||
}
|
||||
file, err := m.WorkflowFile(ctx, args.Name)
|
||||
return string(file), err
|
||||
}),
|
||||
|
||||
tool("save_workflow",
|
||||
"Create or replace a workflow on the server from its file. Tasks that already froze it keep their copy. "+
|
||||
"The name is one line of 1 to 100 characters, shown to the human on buttons and pickers. "+workflowFileHelp,
|
||||
`{"type": "object", "properties": {
|
||||
"name": {"type": "string"},
|
||||
"definition": {"type": "string", "description": "The workflow's file, as YAML."}
|
||||
}, "required": ["name", "definition"]}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
args := struct {
|
||||
Name string `json:"name"`
|
||||
Definition string `json:"definition"`
|
||||
}{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
w, err := m.SaveWorkflow(ctx, args.Name, []byte(args.Definition), leadActor(user.Login))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Saved workflow %q.", w.Name), nil
|
||||
}),
|
||||
|
||||
tool("delete_workflow",
|
||||
"Delete a workflow from the server. Tasks that froze it keep their copy. Only when the human asks.",
|
||||
`{"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}`,
|
||||
func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) {
|
||||
args := struct {
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = m.store.DeleteWorkflow(ctx, args.Name, leadActor(user.Login))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Deleted workflow %q.", args.Name), nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
+367
-158
@@ -324,6 +324,24 @@ func (e SnapshotTrigger) Valid() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for StageVendor.
|
||||
const (
|
||||
StageVendorClaude StageVendor = "claude"
|
||||
StageVendorCodex StageVendor = "codex"
|
||||
)
|
||||
|
||||
// Valid indicates whether the value is a known member of the StageVendor enum.
|
||||
func (e StageVendor) Valid() bool {
|
||||
switch e {
|
||||
case StageVendorClaude:
|
||||
return true
|
||||
case StageVendorCodex:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Defines values for TaskStatus.
|
||||
const (
|
||||
TaskStatusActive TaskStatus = "active"
|
||||
@@ -810,6 +828,23 @@ type Snapshot struct {
|
||||
// SnapshotTrigger turn_end: after byre's end-of-turn commit. message (the agent wrote text after one or more tool calls), tool_call, ask_human and timer appear only on snapshots taken by earlier builds.
|
||||
type SnapshotTrigger string
|
||||
|
||||
// Stage defines model for Stage.
|
||||
type Stage struct {
|
||||
Model string `json:"model"`
|
||||
|
||||
// Name One line of 1 to 100 characters, unique within the workflow.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Prompt The stage agent's instructions.
|
||||
Prompt string `json:"prompt"`
|
||||
|
||||
// Vendor Must be the vendor that runs model (gpt-* models run on Codex, every other model on Claude Code).
|
||||
Vendor StageVendor `json:"vendor"`
|
||||
}
|
||||
|
||||
// StageVendor Must be the vendor that runs model (gpt-* models run on Codex, every other model on Claude Code).
|
||||
type StageVendor string
|
||||
|
||||
// Status defines model for Status.
|
||||
type Status struct {
|
||||
ActiveTasks int `json:"active_tasks"`
|
||||
@@ -876,6 +911,9 @@ type Task struct {
|
||||
|
||||
// TodoItem The title of the TODO.md item the task was started from; absent for other tasks.
|
||||
TodoItem *string `json:"todo_item,omitempty"`
|
||||
|
||||
// Workflow The name of the workflow the task froze at creation (getTaskWorkflow has its copy); absent for a task without one.
|
||||
Workflow *string `json:"workflow,omitempty"`
|
||||
WorktreePath string `json:"worktree_path"`
|
||||
}
|
||||
|
||||
@@ -895,11 +933,21 @@ type TaskChanges struct {
|
||||
// TaskCreate defines model for TaskCreate.
|
||||
type TaskCreate struct {
|
||||
Title string `json:"title"`
|
||||
|
||||
// Workflow The name of a workflow for the task to freeze: it keeps a copy of the workflow's definition as it is now.
|
||||
Workflow *string `json:"workflow,omitempty"`
|
||||
}
|
||||
|
||||
// TaskTab defines model for TaskTab.
|
||||
type TaskTab string
|
||||
|
||||
// TaskWorkflow The workflow a task froze when it was created, as it was then.
|
||||
type TaskWorkflow struct {
|
||||
// Definition The WorkflowDefinition as YAML.
|
||||
Definition string `json:"definition"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// TerminalSize defines model for TerminalSize.
|
||||
type TerminalSize struct {
|
||||
Cols int `json:"cols"`
|
||||
@@ -971,6 +1019,18 @@ type UserUpdate struct {
|
||||
GitName string `json:"git_name"`
|
||||
}
|
||||
|
||||
// WorkflowDefinition A workflow's file, which holds everything but its name: a sequence of stages run in order. Sent and returned as YAML.
|
||||
type WorkflowDefinition struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Stages []Stage `json:"stages"`
|
||||
}
|
||||
|
||||
// WorkflowSummary defines model for WorkflowSummary.
|
||||
type WorkflowSummary struct {
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Zombie defines model for Zombie.
|
||||
type Zombie struct {
|
||||
Detail string `json:"detail"`
|
||||
@@ -1226,6 +1286,21 @@ type ServerInterface interface {
|
||||
|
||||
// (GET /api/tasks/{taskId}/unmerged)
|
||||
GetUnmerged(w http.ResponseWriter, r *http.Request, taskId string)
|
||||
|
||||
// (GET /api/tasks/{taskId}/workflow)
|
||||
GetTaskWorkflow(w http.ResponseWriter, r *http.Request, taskId string)
|
||||
|
||||
// (GET /api/workflows)
|
||||
ListWorkflows(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// (DELETE /api/workflows/{name})
|
||||
DeleteWorkflow(w http.ResponseWriter, r *http.Request, name string)
|
||||
|
||||
// (GET /api/workflows/{name})
|
||||
GetWorkflow(w http.ResponseWriter, r *http.Request, name string)
|
||||
|
||||
// (PUT /api/workflows/{name})
|
||||
PutWorkflow(w http.ResponseWriter, r *http.Request, name string)
|
||||
}
|
||||
|
||||
// ServerInterfaceWrapper converts contexts to parameters.
|
||||
@@ -2550,6 +2625,124 @@ func (siw *ServerInterfaceWrapper) GetUnmerged(w http.ResponseWriter, r *http.Re
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// GetTaskWorkflow operation middleware
|
||||
func (siw *ServerInterfaceWrapper) GetTaskWorkflow(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var err error
|
||||
_ = err
|
||||
|
||||
// ------------- Path parameter "taskId" -------------
|
||||
var taskId string
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "taskId", r.PathValue("taskId"), &taskId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "taskId", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.GetTaskWorkflow(w, r, taskId)
|
||||
}))
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// ListWorkflows operation middleware
|
||||
func (siw *ServerInterfaceWrapper) ListWorkflows(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.ListWorkflows(w, r)
|
||||
}))
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// DeleteWorkflow operation middleware
|
||||
func (siw *ServerInterfaceWrapper) DeleteWorkflow(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var err error
|
||||
_ = err
|
||||
|
||||
// ------------- Path parameter "name" -------------
|
||||
var name string
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "name", r.PathValue("name"), &name, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.DeleteWorkflow(w, r, name)
|
||||
}))
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// GetWorkflow operation middleware
|
||||
func (siw *ServerInterfaceWrapper) GetWorkflow(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var err error
|
||||
_ = err
|
||||
|
||||
// ------------- Path parameter "name" -------------
|
||||
var name string
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "name", r.PathValue("name"), &name, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.GetWorkflow(w, r, name)
|
||||
}))
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// PutWorkflow operation middleware
|
||||
func (siw *ServerInterfaceWrapper) PutWorkflow(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var err error
|
||||
_ = err
|
||||
|
||||
// ------------- Path parameter "name" -------------
|
||||
var name string
|
||||
|
||||
err = runtime.BindStyledParameterWithOptions("simple", "name", r.PathValue("name"), &name, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true})
|
||||
if err != nil {
|
||||
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "name", Err: err})
|
||||
return
|
||||
}
|
||||
|
||||
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
siw.Handler.PutWorkflow(w, r, name)
|
||||
}))
|
||||
|
||||
for _, middleware := range siw.HandlerMiddlewares {
|
||||
handler = middleware(handler)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
type UnescapedCookieParamError struct {
|
||||
ParamName string
|
||||
Err error
|
||||
@@ -2686,6 +2879,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/projects/{projectId}/todo", wrapper.GetTodo)
|
||||
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/projects/{projectId}/todo/tasks", wrapper.StartTodoItem)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}", wrapper.GetTask)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/workflow", wrapper.GetTaskWorkflow)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/unmerged", wrapper.GetUnmerged)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/co-authors", wrapper.GetCoAuthors)
|
||||
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/merge", wrapper.MergeTask)
|
||||
@@ -2715,6 +2909,10 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
||||
m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/inbox/responses", wrapper.RespondInbox)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/events", wrapper.StreamEvents)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/admin/status", wrapper.GetStatus)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/workflows", wrapper.ListWorkflows)
|
||||
m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/workflows/{name}", wrapper.DeleteWorkflow)
|
||||
m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/workflows/{name}", wrapper.GetWorkflow)
|
||||
m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/workflows/{name}", wrapper.PutWorkflow)
|
||||
|
||||
return m
|
||||
}
|
||||
@@ -2724,164 +2922,175 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H
|
||||
// const string: with thousands of chunks the chained `+` fold is several
|
||||
// times slower for the Go compiler than parsing a slice literal.
|
||||
var swaggerSpec = []string{
|
||||
"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",
|
||||
"jUKUxys/usMz9laP7jHkOxU3/PTp/wUAAP//",
|
||||
}
|
||||
|
||||
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"g.fc.run/misc/byre/internal/git"
|
||||
"g.fc.run/misc/byre/internal/store"
|
||||
"g.fc.run/misc/byre/internal/todo"
|
||||
"g.fc.run/misc/byre/internal/workflow"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
@@ -275,9 +276,14 @@ func (s *Server) CreateTask(w http.ResponseWriter, r *http.Request, projectID st
|
||||
return
|
||||
}
|
||||
|
||||
t, err := s.agents.CreateTask(r.Context(), p, req.Title, actor(r), p.DefaultBranch, actor(r))
|
||||
workflowName := ""
|
||||
if req.Workflow != nil {
|
||||
workflowName = *req.Workflow
|
||||
}
|
||||
|
||||
t, err := s.agents.CreateTask(r.Context(), p, req.Title, actor(r), p.DefaultBranch, actor(r), workflowName)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, t)
|
||||
@@ -326,7 +332,7 @@ func (s *Server) StartTodoItem(w http.ResponseWriter, r *http.Request, projectID
|
||||
model = *req.Model
|
||||
}
|
||||
|
||||
t, err := s.agents.StartTodo(r.Context(), p, u, req.Title, model, u.Login)
|
||||
t, err := s.agents.StartTodo(r.Context(), p, u, req.Title, model, u.Login, "")
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
@@ -694,7 +700,7 @@ func writeAgentError(w http.ResponseWriter, err error) {
|
||||
writeError(w, http.StatusNotFound, err)
|
||||
case errors.Is(err, agents.ErrConflict):
|
||||
writeError(w, http.StatusConflict, err)
|
||||
case errors.Is(err, agents.ErrInvalid), errors.Is(err, agents.ErrNoGitIdentity):
|
||||
case errors.Is(err, agents.ErrInvalid), errors.Is(err, agents.ErrNoGitIdentity), errors.Is(err, workflow.ErrInvalid):
|
||||
writeError(w, http.StatusBadRequest, err)
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, err)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"g.fc.run/misc/byre/internal/workflow"
|
||||
)
|
||||
|
||||
const maxWorkflowFileBytes = 1 << 20
|
||||
|
||||
func (s *Server) ListWorkflows(w http.ResponseWriter, r *http.Request) {
|
||||
workflows, err := s.store.ListWorkflows(r.Context())
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, workflows)
|
||||
}
|
||||
|
||||
func (s *Server) GetWorkflow(w http.ResponseWriter, r *http.Request, name string) {
|
||||
file, err := s.agents.WorkflowFile(r.Context(), name)
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/yaml")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape(workflow.Filename(name)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(file)
|
||||
}
|
||||
|
||||
func (s *Server) PutWorkflow(w http.ResponseWriter, r *http.Request, name string) {
|
||||
file, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxWorkflowFileBytes))
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, fmt.Errorf("read workflow file: %w", err))
|
||||
return
|
||||
}
|
||||
summary, err := s.agents.SaveWorkflow(r.Context(), name, file, actor(r))
|
||||
if err != nil {
|
||||
writeAgentError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, summary)
|
||||
}
|
||||
|
||||
func (s *Server) DeleteWorkflow(w http.ResponseWriter, r *http.Request, name string) {
|
||||
err := s.store.DeleteWorkflow(r.Context(), name, actor(r))
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) GetTaskWorkflow(w http.ResponseWriter, r *http.Request, taskID string) {
|
||||
tw, err := s.agents.TaskWorkflow(r.Context(), taskID)
|
||||
if err != nil {
|
||||
writeStoreError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tw)
|
||||
}
|
||||
+28
-6
@@ -340,6 +340,13 @@ INSERT INTO reviews_new SELECT * FROM reviews;
|
||||
DROP TABLE reviews;
|
||||
ALTER TABLE reviews_new RENAME TO reviews;
|
||||
CREATE UNIQUE INDEX reviews_session ON reviews(agent_session_id);
|
||||
`}, {"add-workflows", `
|
||||
CREATE TABLE workflows (
|
||||
name TEXT PRIMARY KEY,
|
||||
definition TEXT NOT NULL
|
||||
);
|
||||
ALTER TABLE tasks ADD COLUMN workflow_name TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN workflow_definition TEXT NOT NULL DEFAULT '';
|
||||
`}}
|
||||
|
||||
const (
|
||||
@@ -605,7 +612,18 @@ func (s *Store) GetProject(ctx context.Context, id string) (api.Project, error)
|
||||
return p, err
|
||||
}
|
||||
|
||||
func (s *Store) CreateTask(ctx context.Context, t api.Task, actor string) error {
|
||||
func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkflow, actor string) error {
|
||||
workflowName := ""
|
||||
workflowDefinition := ""
|
||||
if frozen != nil {
|
||||
b, err := json.Marshal(frozen.Definition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workflowName = frozen.Name
|
||||
workflowDefinition = string(b)
|
||||
t.Workflow = &workflowName
|
||||
}
|
||||
todoItem := ""
|
||||
if t.TodoItem != nil {
|
||||
todoItem = *t.TodoItem
|
||||
@@ -642,9 +660,9 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, actor string) error
|
||||
return err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, todo_item, forked_from, forked_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, t.Color, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt)
|
||||
INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, todo_item, forked_from, forked_at, workflow_name, workflow_definition)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, t.Color, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt, workflowName, workflowDefinition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1230,7 +1248,7 @@ func (s *Store) write(ctx context.Context, fn func(tx *sql.Tx, emit emitFunc) er
|
||||
|
||||
const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at`
|
||||
|
||||
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at`
|
||||
const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name`
|
||||
|
||||
const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, created_by, created_at, forked_from, forked_at`
|
||||
|
||||
@@ -1259,10 +1277,14 @@ func scanTask(row scanner) (api.Task, error) {
|
||||
forkedFrom := ""
|
||||
forkedAt := 0
|
||||
mergeQueuedAt := ""
|
||||
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt)
|
||||
workflowName := ""
|
||||
err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
if workflowName != "" {
|
||||
t.Workflow = &workflowName
|
||||
}
|
||||
if t.MergeTitle == "" {
|
||||
t.MergeTitle = t.Title
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
)
|
||||
|
||||
type FrozenWorkflow struct {
|
||||
Name string
|
||||
Definition api.WorkflowDefinition
|
||||
}
|
||||
|
||||
func (s *Store) ListWorkflows(ctx context.Context) ([]api.WorkflowSummary, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT name, coalesce(json_extract(definition, '$.description'), '') FROM workflows ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
workflows := []api.WorkflowSummary{}
|
||||
for rows.Next() {
|
||||
w := api.WorkflowSummary{}
|
||||
err = rows.Scan(&w.Name, &w.Description)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
workflows = append(workflows, w)
|
||||
}
|
||||
return workflows, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) GetWorkflow(ctx context.Context, name string) (api.WorkflowDefinition, error) {
|
||||
def := ""
|
||||
err := s.db.QueryRowContext(ctx, `SELECT definition FROM workflows WHERE name = ?`, name).Scan(&def)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("%w: no workflow %q", ErrNotFound, name)
|
||||
}
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, err
|
||||
}
|
||||
w := api.WorkflowDefinition{}
|
||||
err = json.Unmarshal([]byte(def), &w)
|
||||
return w, err
|
||||
}
|
||||
|
||||
func (s *Store) PutWorkflow(ctx context.Context, name string, def api.WorkflowDefinition, actor string) error {
|
||||
b, err := json.Marshal(def)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
_, err := tx.ExecContext(ctx, `INSERT INTO workflows (name, definition) VALUES (?, ?)
|
||||
ON CONFLICT (name) DO UPDATE SET definition = excluded.definition`, name, string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return emit(api.Event{Kind: "workflow.updated", Actor: actor, Payload: map[string]any{"name": name}})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) DeleteWorkflow(ctx context.Context, name, actor string) error {
|
||||
return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error {
|
||||
res, err := tx.ExecContext(ctx, `DELETE FROM workflows WHERE name = ?`, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("%w: no workflow %q", ErrNotFound, name)
|
||||
}
|
||||
return emit(api.Event{Kind: "workflow.deleted", Actor: actor, Payload: map[string]any{"name": name}})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Store) FreezeWorkflow(ctx context.Context, name string) (FrozenWorkflow, error) {
|
||||
def, err := s.GetWorkflow(ctx, name)
|
||||
if err != nil {
|
||||
return FrozenWorkflow{}, err
|
||||
}
|
||||
return FrozenWorkflow{Name: name, Definition: def}, nil
|
||||
}
|
||||
|
||||
func (s *Store) TaskWorkflow(ctx context.Context, taskID string) (FrozenWorkflow, error) {
|
||||
name := ""
|
||||
def := ""
|
||||
err := s.db.QueryRowContext(ctx, `SELECT workflow_name, workflow_definition FROM tasks WHERE id = ?`, taskID).Scan(&name, &def)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return FrozenWorkflow{}, fmt.Errorf("%w: no task %s", ErrNotFound, taskID)
|
||||
}
|
||||
if err != nil {
|
||||
return FrozenWorkflow{}, err
|
||||
}
|
||||
if name == "" {
|
||||
return FrozenWorkflow{}, fmt.Errorf("%w: task %s has no workflow", ErrNotFound, taskID)
|
||||
}
|
||||
f := FrozenWorkflow{Name: name}
|
||||
err = json.Unmarshal([]byte(def), &f.Definition)
|
||||
return f, err
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
)
|
||||
|
||||
var keyOrder = []string{"description", "stages", "name", "vendor", "model", "prompt"}
|
||||
|
||||
func Parse(data []byte) (api.WorkflowDefinition, error) {
|
||||
raw := any(nil)
|
||||
err := yaml.Unmarshal(data, &raw)
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||
}
|
||||
j, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||
}
|
||||
|
||||
def := api.WorkflowDefinition{}
|
||||
dec := json.NewDecoder(bytes.NewReader(j))
|
||||
dec.DisallowUnknownFields()
|
||||
err = dec.Decode(&def)
|
||||
if err != nil {
|
||||
return api.WorkflowDefinition{}, fmt.Errorf("%w: %v", ErrInvalid, err)
|
||||
}
|
||||
return def, Validate(def)
|
||||
}
|
||||
|
||||
func Format(def api.WorkflowDefinition) ([]byte, error) {
|
||||
j, err := json.Marshal(def)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc := yaml.Node{}
|
||||
err = yaml.Unmarshal(j, &doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
restyle(&doc)
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
enc := yaml.NewEncoder(out)
|
||||
enc.SetIndent(2)
|
||||
err = enc.Encode(&doc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = enc.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
func Filename(name string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if strings.ContainsRune(`/\:*?"<>|`, r) {
|
||||
return '_'
|
||||
}
|
||||
return r
|
||||
}, name) + ".yaml"
|
||||
}
|
||||
|
||||
func restyle(n *yaml.Node) {
|
||||
switch n.Kind {
|
||||
case yaml.ScalarNode:
|
||||
n.Style = 0
|
||||
if n.Tag == "!!str" && strings.Contains(n.Value, "\n") {
|
||||
n.Style = yaml.LiteralStyle
|
||||
}
|
||||
case yaml.MappingNode:
|
||||
n.Style = 0
|
||||
pairs := [][2]*yaml.Node{}
|
||||
for i := 0; i+1 < len(n.Content); i += 2 {
|
||||
pairs = append(pairs, [2]*yaml.Node{n.Content[i], n.Content[i+1]})
|
||||
}
|
||||
slices.SortStableFunc(pairs, func(a, b [2]*yaml.Node) int {
|
||||
return keyRank(a[0].Value) - keyRank(b[0].Value)
|
||||
})
|
||||
n.Content = n.Content[:0]
|
||||
for _, p := range pairs {
|
||||
n.Content = append(n.Content, p[0], p[1])
|
||||
}
|
||||
case yaml.SequenceNode:
|
||||
n.Style = 0
|
||||
}
|
||||
for _, c := range n.Content {
|
||||
restyle(c)
|
||||
}
|
||||
}
|
||||
|
||||
func keyRank(key string) int {
|
||||
i := slices.Index(keyOrder, key)
|
||||
if i < 0 {
|
||||
return len(keyOrder)
|
||||
}
|
||||
return i
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"g.fc.run/misc/byre/internal/api"
|
||||
)
|
||||
|
||||
var ErrInvalid = errors.New("invalid workflow")
|
||||
|
||||
const maxNameRunes = 100
|
||||
|
||||
func VendorOf(model string) api.AgentSessionVendor {
|
||||
if strings.HasPrefix(model, "gpt-") {
|
||||
return api.Codex
|
||||
}
|
||||
return api.Claude
|
||||
}
|
||||
|
||||
func CleanName(name string) (string, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
problem := nameProblem(name)
|
||||
if problem != "" {
|
||||
return "", fmt.Errorf("%w: workflow name %s", ErrInvalid, problem)
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func nameProblem(name string) string {
|
||||
switch {
|
||||
case strings.TrimSpace(name) == "":
|
||||
return "is empty"
|
||||
case strings.TrimSpace(name) != name:
|
||||
return fmt.Sprintf("%q has leading or trailing spaces", name)
|
||||
case utf8.RuneCountInString(name) > maxNameRunes:
|
||||
return fmt.Sprintf("%q is longer than %d characters", name, maxNameRunes)
|
||||
case strings.ContainsFunc(name, unicode.IsControl):
|
||||
return fmt.Sprintf("%q isn't a single line", name)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func Validate(def api.WorkflowDefinition) error {
|
||||
problems := []string{}
|
||||
if len(def.Stages) == 0 {
|
||||
problems = append(problems, "it has no stages")
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
for i, s := range def.Stages {
|
||||
where := fmt.Sprintf("stage %d (%q)", i+1, s.Name)
|
||||
p := nameProblem(s.Name)
|
||||
if p != "" {
|
||||
problems = append(problems, fmt.Sprintf("%s: name %s", where, p))
|
||||
}
|
||||
if seen[s.Name] {
|
||||
problems = append(problems, fmt.Sprintf("%s: another stage has the same name", where))
|
||||
}
|
||||
seen[s.Name] = true
|
||||
|
||||
switch {
|
||||
case !s.Vendor.Valid():
|
||||
problems = append(problems, fmt.Sprintf("%s: vendor must be claude or codex, not %q", where, s.Vendor))
|
||||
case strings.TrimSpace(s.Model) == "":
|
||||
problems = append(problems, fmt.Sprintf("%s: model is required", where))
|
||||
case string(VendorOf(s.Model)) != string(s.Vendor):
|
||||
problems = append(problems, fmt.Sprintf("%s: %s is a %s model, not %s", where, s.Model, VendorOf(s.Model), s.Vendor))
|
||||
}
|
||||
|
||||
if strings.TrimSpace(s.Prompt) == "" {
|
||||
problems = append(problems, fmt.Sprintf("%s: prompt is empty", where))
|
||||
}
|
||||
}
|
||||
|
||||
if len(problems) > 0 {
|
||||
return fmt.Errorf("%w: %s", ErrInvalid, strings.Join(problems, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -214,6 +214,25 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/tasks/{taskId}/workflow": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
taskId: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
/** @description The workflow the task froze when it was created, as it was then. Later edits and deletes of the workflow don't change it. */
|
||||
get: operations["getTaskWorkflow"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/tasks/{taskId}/unmerged": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -733,6 +752,41 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/workflows": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listWorkflows"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/workflows/{name}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
name: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["getWorkflow"];
|
||||
/** @description Creates or replaces the workflow from its file, a WorkflowDefinition as YAML (or JSON). The name is one line of 1 to 100 characters. Tasks that already froze the workflow keep their copy. */
|
||||
put: operations["putWorkflow"];
|
||||
post?: never;
|
||||
delete: operations["deleteWorkflow"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
@@ -814,6 +868,8 @@ export interface components {
|
||||
* @description When Merge was requested while another task in the project was merging; absent once the merge starts or when none is held.
|
||||
*/
|
||||
merge_queued_at?: string;
|
||||
/** @description The name of the workflow the task froze at creation (getTaskWorkflow has its copy); absent for a task without one. */
|
||||
workflow?: string;
|
||||
};
|
||||
Todo: {
|
||||
/** @description Whether the default branch has a TODO.md at its root. */
|
||||
@@ -852,6 +908,35 @@ export interface components {
|
||||
};
|
||||
TaskCreate: {
|
||||
title: string;
|
||||
/** @description The name of a workflow for the task to freeze: it keeps a copy of the workflow's definition as it is now. */
|
||||
workflow?: string;
|
||||
};
|
||||
WorkflowSummary: {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
/** @description A workflow's file, which holds everything but its name: a sequence of stages run in order. Sent and returned as YAML. */
|
||||
WorkflowDefinition: {
|
||||
description?: string;
|
||||
stages: components["schemas"]["Stage"][];
|
||||
};
|
||||
Stage: {
|
||||
/** @description One line of 1 to 100 characters, unique within the workflow. */
|
||||
name: string;
|
||||
/**
|
||||
* @description Must be the vendor that runs model (gpt-* models run on Codex, every other model on Claude Code).
|
||||
* @enum {string}
|
||||
*/
|
||||
vendor: "claude" | "codex";
|
||||
model: string;
|
||||
/** @description The stage agent's instructions. */
|
||||
prompt: string;
|
||||
};
|
||||
/** @description The workflow a task froze when it was created, as it was then. */
|
||||
TaskWorkflow: {
|
||||
name: string;
|
||||
/** @description The WorkflowDefinition as YAML. */
|
||||
definition: string;
|
||||
};
|
||||
TaskChanges: {
|
||||
/** @description The commit or tree compared with: the merge-base of the task branch and the default branch, since, or the previous snapshot's tree. */
|
||||
@@ -1589,6 +1674,29 @@ export interface operations {
|
||||
404: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
getTaskWorkflow: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
taskId: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description The frozen workflow. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["TaskWorkflow"];
|
||||
};
|
||||
};
|
||||
404: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
getUnmerged: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -2342,4 +2450,95 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
listWorkflows: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Every workflow's name and description, by name. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["WorkflowSummary"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
getWorkflow: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
name: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description The workflow's file: its WorkflowDefinition as YAML, as an attachment named after it. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/yaml": string;
|
||||
};
|
||||
};
|
||||
404: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
putWorkflow: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
name: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/yaml": string;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description The stored workflow. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["WorkflowSummary"];
|
||||
};
|
||||
};
|
||||
400: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
deleteWorkflow: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
name: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Deleted. Tasks that froze it keep their copy. */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
404: components["responses"]["Error"];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user