@herbertgao/pi-subagents 0.17.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +427 -120
- package/docs/rpc.md +184 -0
- package/docs/workflows.md +466 -0
- package/examples/agent-tool-description.md +6 -6
- package/examples/workflows/compose.js +52 -0
- package/examples/workflows/fan-out-audit.js +56 -0
- package/examples/workflows/gated-fix.js +60 -0
- package/examples/workflows/lib/count-child.js +30 -0
- package/examples/workflows/review-panel.js +68 -0
- package/examples/workflows/structured-findings.js +81 -0
- package/package.json +11 -9
- package/src/agent-file-toggle.ts +52 -12
- package/src/agent-manager.ts +837 -146
- package/src/agent-runner.ts +213 -39
- package/src/cross-extension-rpc.ts +73 -14
- package/src/custom-agents.ts +101 -47
- package/src/index.ts +2249 -914
- package/src/invocation-config.ts +13 -0
- package/src/mention-clone.ts +215 -0
- package/src/mention.ts +147 -0
- package/src/model-resolver.ts +9 -1
- package/src/nested-tools.ts +40 -26
- package/src/output-file.ts +18 -8
- package/src/prompts.ts +46 -9
- package/src/schedule.ts +21 -16
- package/src/settings.ts +137 -7
- package/src/structured-output.ts +136 -0
- package/src/types.ts +126 -8
- package/src/ui/agent-mention.ts +274 -0
- package/src/ui/agent-widget.ts +20 -5
- package/src/ui/conversation-viewer.ts +10 -4
- package/src/ui/fleet-list.ts +167 -22
- package/src/ui/workflow-card.ts +555 -0
- package/src/ui/workflow-dialog.ts +1304 -0
- package/src/ui/workflow-menu.ts +226 -0
- package/src/workflow/collisions.ts +122 -0
- package/src/workflow/entry.ts +47 -0
- package/src/workflow/host.ts +463 -0
- package/src/workflow/journal.ts +164 -0
- package/src/workflow/json-schema.ts +142 -0
- package/src/workflow/meta.ts +401 -0
- package/src/workflow/progress.ts +622 -0
- package/src/workflow/runtime.ts +1399 -0
- package/src/workflow/saved.ts +230 -0
- package/src/workflow/task.ts +333 -0
- package/src/workflow/tool-description.ts +200 -0
- package/src/workflow/worker-source.ts +781 -0
- package/src/worktree.ts +97 -95
- package/src/xml.ts +13 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow-menu.ts — `/agents → Workflows`, and the run inspector behind it.
|
|
3
|
+
*
|
|
4
|
+
* The same shape `schedule-menu.ts` has for `/agents → Scheduled jobs`: the
|
|
5
|
+
* submenu and the overlay it opens live here, and everything they need arrives
|
|
6
|
+
* as {@link WorkflowMenuDeps} rather than through a closure. The inspector is
|
|
7
|
+
* reached from two places — this menu and a `workflow` row in the fleet list —
|
|
8
|
+
* and both go through `showWorkflowDialog`, so the two entry points cannot
|
|
9
|
+
* drift apart on what the keys do.
|
|
10
|
+
*
|
|
11
|
+
* Lives in the agents menu rather than as a top-level `/workflows` command: it
|
|
12
|
+
* is one more view of the same fleet, and a second command name would only add
|
|
13
|
+
* a collision surface (pi renames duplicate commands to `/workflows:1` and
|
|
14
|
+
* `/workflows:2`, which breaks the bare name for both).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent"
|
|
18
|
+
import type { AgentRecord } from "../types.js"
|
|
19
|
+
import {
|
|
20
|
+
pauseWorkflowTask,
|
|
21
|
+
resumeWorkflowTask,
|
|
22
|
+
type WorkflowTask,
|
|
23
|
+
} from "../workflow/task.js"
|
|
24
|
+
import { WorkflowDialog } from "./workflow-dialog.js"
|
|
25
|
+
|
|
26
|
+
/** Everything the menu and the inspector need from the extension around them. */
|
|
27
|
+
export interface WorkflowMenuDeps {
|
|
28
|
+
/**
|
|
29
|
+
* Live runs by id, read on every use rather than snapshotted: a run that
|
|
30
|
+
* settled and was swept between render and keypress must be a no-op, not a
|
|
31
|
+
* crash.
|
|
32
|
+
*/
|
|
33
|
+
tasks: ReadonlyMap<string, WorkflowTask>
|
|
34
|
+
/** The record behind an agent id, or undefined once it has been swept. */
|
|
35
|
+
getRecord(id: string): AgentRecord | undefined
|
|
36
|
+
/** The conversation overlay `c` opens on an agent row. */
|
|
37
|
+
viewAgentConversation(
|
|
38
|
+
ctx: ExtensionCommandContext,
|
|
39
|
+
record: AgentRecord,
|
|
40
|
+
): Promise<void>
|
|
41
|
+
/**
|
|
42
|
+
* The session context, for the fleet-list entry point — that one is a
|
|
43
|
+
* keypress in a list that holds no `ctx` of its own. Undefined between
|
|
44
|
+
* sessions, which is a no-op rather than an error.
|
|
45
|
+
*/
|
|
46
|
+
getCtx(): ExtensionCommandContext | undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Open the inspector for a workflow run.
|
|
51
|
+
*
|
|
52
|
+
* All six controls are wired: `onKill` aborts the run's controller, while
|
|
53
|
+
* pause/resume and per-agent skip/retry go through `task.control`, the handle
|
|
54
|
+
* `runWorkflow` hands back. `onOpenAgent` is the odd one out — it opens the
|
|
55
|
+
* child's conversation rather than changing the run. The dialog derives its key
|
|
56
|
+
* hints from the actions it is handed, so the footer advertises exactly what
|
|
57
|
+
* works — see `WorkflowDialogActions`.
|
|
58
|
+
*/
|
|
59
|
+
export async function showWorkflowDialog(
|
|
60
|
+
ctx: ExtensionCommandContext,
|
|
61
|
+
task: WorkflowTask,
|
|
62
|
+
deps: WorkflowMenuDeps,
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
// Overlaid on the same terms as the conversation viewer, because they are
|
|
65
|
+
// reached the same way: both are rows of the fleet list, and opening one
|
|
66
|
+
// must not behave unlike opening the other. Inline, the frame would render
|
|
67
|
+
// into the conversation and stay in the scrollback after it closed.
|
|
68
|
+
const { VIEWPORT_HEIGHT_PCT } = await import("./conversation-viewer.js")
|
|
69
|
+
/**
|
|
70
|
+
* This dialog's own overlay, so `c` can hide it while the conversation is
|
|
71
|
+
* up. Overlays stack, so the viewer would render *over* it either way —
|
|
72
|
+
* but the two frames size themselves to different content, and the taller
|
|
73
|
+
* one's edges show around the shorter. Hidden, there is nothing to peek
|
|
74
|
+
* out, and un-hiding puts the focus back on the dialog when the viewer
|
|
75
|
+
* closes.
|
|
76
|
+
*/
|
|
77
|
+
let overlay: { setHidden(hidden: boolean): void } | undefined
|
|
78
|
+
await ctx.ui.custom<undefined>(
|
|
79
|
+
(tui, theme, _keybindings, done) =>
|
|
80
|
+
new WorkflowDialog(
|
|
81
|
+
tui,
|
|
82
|
+
// Re-read on every render: the run is in the background, so the
|
|
83
|
+
// dialog has to follow it rather than snapshot it at open time.
|
|
84
|
+
() => ({
|
|
85
|
+
progress: task.workflowProgress,
|
|
86
|
+
task: {
|
|
87
|
+
status: task.status,
|
|
88
|
+
workflowName: task.workflowName,
|
|
89
|
+
startTime: task.startTime,
|
|
90
|
+
endTime: task.endTime,
|
|
91
|
+
totalPausedMs: task.totalPausedMs,
|
|
92
|
+
},
|
|
93
|
+
meta: task.meta,
|
|
94
|
+
agentCount: task.agentCount,
|
|
95
|
+
}),
|
|
96
|
+
theme,
|
|
97
|
+
done,
|
|
98
|
+
{
|
|
99
|
+
onKill: () => {
|
|
100
|
+
if (task.abortController.signal.aborted) return
|
|
101
|
+
task.abortController.abort()
|
|
102
|
+
ctx.ui.notify(
|
|
103
|
+
`Stopped workflow "${task.meta?.name ?? task.id}".`,
|
|
104
|
+
"info",
|
|
105
|
+
)
|
|
106
|
+
},
|
|
107
|
+
onPause: () => {
|
|
108
|
+
if (pauseWorkflowTask(task)) {
|
|
109
|
+
// Named rather than implied: "paused" on a run whose agents are
|
|
110
|
+
// still finishing reads as a stronger promise than it is.
|
|
111
|
+
ctx.ui.notify(
|
|
112
|
+
"Paused — running agents finish, no new ones start.",
|
|
113
|
+
"info",
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
onResume: () => {
|
|
118
|
+
if (resumeWorkflowTask(task)) ctx.ui.notify("Resumed.", "info")
|
|
119
|
+
},
|
|
120
|
+
onSkipAgent: (index) => {
|
|
121
|
+
if (task.control?.skip(index) !== true) {
|
|
122
|
+
ctx.ui.notify(
|
|
123
|
+
"Nothing to skip — that agent has already finished.",
|
|
124
|
+
"info",
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
onRetryAgent: (index) => {
|
|
129
|
+
if (task.control?.retry(index) !== true) {
|
|
130
|
+
// The window is exactly "while it is running": before that
|
|
131
|
+
// there is nothing to stop, after it the script has its answer.
|
|
132
|
+
ctx.ui.notify("Only a running agent can be retried.", "info")
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
onOpenAgent: (recordId) => {
|
|
136
|
+
const record = deps.getRecord(recordId)
|
|
137
|
+
// A run's children are records like any other, so they are swept
|
|
138
|
+
// ten minutes after they finish — the row outlives the
|
|
139
|
+
// conversation it points at, and saying why beats an overlay that
|
|
140
|
+
// opens empty.
|
|
141
|
+
if (record === undefined) {
|
|
142
|
+
ctx.ui.notify(
|
|
143
|
+
"No conversation left — agent records are dropped ten minutes after they finish.",
|
|
144
|
+
"info",
|
|
145
|
+
)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
overlay?.setHidden(true)
|
|
149
|
+
// Caught before the `finally`, so a viewer that fails to open
|
|
150
|
+
// still un-hides the dialog and cannot surface as an unhandled
|
|
151
|
+
// rejection out of a detached promise.
|
|
152
|
+
void deps
|
|
153
|
+
.viewAgentConversation(ctx, record)
|
|
154
|
+
.catch((err) =>
|
|
155
|
+
ctx.ui.notify(
|
|
156
|
+
`Could not open the conversation: ${err instanceof Error ? err.message : String(err)}`,
|
|
157
|
+
"warning",
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
.finally(() => overlay?.setHidden(false))
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
),
|
|
164
|
+
{
|
|
165
|
+
overlay: true,
|
|
166
|
+
overlayOptions: {
|
|
167
|
+
anchor: "center",
|
|
168
|
+
width: "90%",
|
|
169
|
+
maxHeight: `${VIEWPORT_HEIGHT_PCT}%`,
|
|
170
|
+
},
|
|
171
|
+
onHandle: (handle) => {
|
|
172
|
+
overlay = handle
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Open a run from the fleet list.
|
|
180
|
+
*
|
|
181
|
+
* The list hands back an id rather than a task, so a run that settled and was
|
|
182
|
+
* swept between render and keypress is a no-op instead of a crash. `esc` in the
|
|
183
|
+
* dialog closes it and control returns to the list — which is why the promise
|
|
184
|
+
* is handed back: the list puts the cursor back on the run rather than dropping
|
|
185
|
+
* the reader at `main`.
|
|
186
|
+
*/
|
|
187
|
+
export function openWorkflowFromFleet(
|
|
188
|
+
id: string,
|
|
189
|
+
deps: WorkflowMenuDeps,
|
|
190
|
+
): Promise<void> | void {
|
|
191
|
+
const task = deps.tasks.get(id)
|
|
192
|
+
const ctx = deps.getCtx()
|
|
193
|
+
if (task === undefined || ctx === undefined) return
|
|
194
|
+
return showWorkflowDialog(ctx, task, deps)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** `/agents → Workflows` — list this session's runs, open one. */
|
|
198
|
+
export async function showWorkflowsMenu(
|
|
199
|
+
ctx: ExtensionCommandContext,
|
|
200
|
+
deps: WorkflowMenuDeps,
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
const tasks = [...deps.tasks.values()].sort(
|
|
203
|
+
(a, b) => b.startTime - a.startTime,
|
|
204
|
+
)
|
|
205
|
+
if (tasks.length === 0) {
|
|
206
|
+
ctx.ui.notify("No workflows in this session.", "info")
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
if (tasks.length === 1) {
|
|
210
|
+
await showWorkflowDialog(ctx, tasks[0], deps)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
// More than one: pick first. Newest at the top, since that is almost
|
|
214
|
+
// always the one being asked about. `select` deals in plain strings and
|
|
215
|
+
// hands back the string, so the label has to be unique or `indexOf` maps
|
|
216
|
+
// the second run of a workflow onto the first — the run id makes it so.
|
|
217
|
+
const labels = tasks.map(
|
|
218
|
+
(task) =>
|
|
219
|
+
`${task.meta?.name ?? task.id} — ${task.status}, ${task.agentCount} agent${
|
|
220
|
+
task.agentCount === 1 ? "" : "s"
|
|
221
|
+
} · ${task.id}`,
|
|
222
|
+
)
|
|
223
|
+
const picked = await ctx.ui.select("Workflows", labels)
|
|
224
|
+
const index = picked !== undefined ? labels.indexOf(picked) : -1
|
|
225
|
+
if (index >= 0) await showWorkflowDialog(ctx, tasks[index], deps)
|
|
226
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* collisions.ts — deciding what to do when another extension already offers a
|
|
3
|
+
* workflow tool.
|
|
4
|
+
*
|
|
5
|
+
* Workflows are on by default, so this extension can be the *second*
|
|
6
|
+
* orchestrator in a session rather than the only one. Two workflow tools in one
|
|
7
|
+
* spec is worse than either alone: the model has to guess which to call, and
|
|
8
|
+
* pays for both descriptions to find out. The other extension was installed
|
|
9
|
+
* deliberately; a default of ours should not compete with it.
|
|
10
|
+
*
|
|
11
|
+
* ## What counts as a conflict
|
|
12
|
+
*
|
|
13
|
+
* An exact name match against {@link FOREIGN_WORKFLOW_TOOL_NAMES}, from a tool
|
|
14
|
+
* that is not ours. Exact and not a substring on purpose: `Workflow` is a
|
|
15
|
+
* common word in tool names that have nothing to do with orchestration
|
|
16
|
+
* (`github_workflow_run`, `list_workflows`), and silently disabling the feature
|
|
17
|
+
* against one of those would be a bug nobody could see.
|
|
18
|
+
*
|
|
19
|
+
* Two shapes, both decided here:
|
|
20
|
+
*
|
|
21
|
+
* 1. **A foreign tool took our name.** Registration is first-wins across
|
|
22
|
+
* extensions (`getAllRegisteredTools` skips a name it already has), and the
|
|
23
|
+
* winner also overwrites a built-in of the same name. Nothing throws, and
|
|
24
|
+
* there is no tool-conflict diagnostic the way there is for shortcuts. Ours
|
|
25
|
+
* never reached the registry, so there is nothing to withdraw — but the rest
|
|
26
|
+
* of the feature (the menu, the CLI flag) is still live and would drive a
|
|
27
|
+
* tool the model cannot call. It comes down with it.
|
|
28
|
+
* 2. **A foreign tool sits beside ours** under a different name, typically
|
|
29
|
+
* Claude Code's bare `Workflow`. Both are registered and both are offered.
|
|
30
|
+
* That one the caller can actually withdraw — `withdraw` says so.
|
|
31
|
+
*
|
|
32
|
+
* Only one direction of case 1 is detectable. If ours registered first, the
|
|
33
|
+
* other extension's tool is the one dropped, and pi exposes no way to see a
|
|
34
|
+
* tool that lost — the registry keeps winners only.
|
|
35
|
+
*
|
|
36
|
+
* Split from the acting half deliberately: everything here is a pure function
|
|
37
|
+
* of the tool list, so the policy can be tested without a host that can be made
|
|
38
|
+
* to register a competing extension. The caller owns `getAllTools`, the notify
|
|
39
|
+
* and the `setActiveTools` — see `resolveWorkflowCollisions` in index.ts.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { SUBAGENT_TOOL_NAMES } from "../agent-runner.js"
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Tool names that mean "another extension already orchestrates subagents".
|
|
46
|
+
*
|
|
47
|
+
* Our own name, because pi resolves a duplicate registration silently, and
|
|
48
|
+
* Claude Code's bare `Workflow`, because a port of that tool is what a second
|
|
49
|
+
* workflow extension most likely calls itself.
|
|
50
|
+
*/
|
|
51
|
+
export const FOREIGN_WORKFLOW_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
52
|
+
SUBAGENT_TOOL_NAMES.WORKFLOW,
|
|
53
|
+
"Workflow",
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
/** The fields of a registered tool this decision reads. */
|
|
57
|
+
export interface RegisteredToolInfo {
|
|
58
|
+
name: string
|
|
59
|
+
description?: string
|
|
60
|
+
sourceInfo?: { source?: string }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type WorkflowCollision =
|
|
64
|
+
/** Nobody else is offering one. Carry on. */
|
|
65
|
+
| { kind: "none" }
|
|
66
|
+
/**
|
|
67
|
+
* A foreign tool took our name, but the user pinned `workflowsEnabled: true`.
|
|
68
|
+
* Nothing changes — pi has already dropped our registration — but it is worth
|
|
69
|
+
* reporting, because pi resolved it silently.
|
|
70
|
+
*/
|
|
71
|
+
| { kind: "report"; message: string }
|
|
72
|
+
/**
|
|
73
|
+
* Stand down for this session. `withdraw` is false in case 1, where ours
|
|
74
|
+
* never reached the registry and there is nothing to take out of the active
|
|
75
|
+
* set.
|
|
76
|
+
*/
|
|
77
|
+
| { kind: "standDown"; message: string; withdraw: boolean }
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decide, from the registered tools alone.
|
|
81
|
+
*
|
|
82
|
+
* `ownDescription` identifies our own registration: this extension does not
|
|
83
|
+
* know its install path, and the description is the one field that is certainly
|
|
84
|
+
* ours. `pinned` is an explicit `workflowsEnabled` — a default yields to
|
|
85
|
+
* evidence, a choice does not.
|
|
86
|
+
*/
|
|
87
|
+
export function decideWorkflowCollision(input: {
|
|
88
|
+
tools: readonly RegisteredToolInfo[]
|
|
89
|
+
ownDescription: string
|
|
90
|
+
pinned: boolean
|
|
91
|
+
}): WorkflowCollision {
|
|
92
|
+
const foreign = input.tools.find(
|
|
93
|
+
(tool) =>
|
|
94
|
+
FOREIGN_WORKFLOW_TOOL_NAMES.has(tool.name) &&
|
|
95
|
+
tool.description !== input.ownDescription,
|
|
96
|
+
)
|
|
97
|
+
if (foreign === undefined) return { kind: "none" }
|
|
98
|
+
|
|
99
|
+
const source = foreign.sourceInfo?.source ?? "unknown source"
|
|
100
|
+
const tookOurName = foreign.name === SUBAGENT_TOOL_NAMES.WORKFLOW
|
|
101
|
+
|
|
102
|
+
if (input.pinned) {
|
|
103
|
+
if (!tookOurName) return { kind: "none" }
|
|
104
|
+
return {
|
|
105
|
+
kind: "report",
|
|
106
|
+
message:
|
|
107
|
+
`Another extension (${source}) already registers a "${SUBAGENT_TOOL_NAMES.WORKFLOW}" tool. ` +
|
|
108
|
+
"Pi keeps the first registration, so this extension's workflow tool is not offered to the " +
|
|
109
|
+
"model. Disable one of the two.",
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
kind: "standDown",
|
|
115
|
+
message:
|
|
116
|
+
`Another extension (${source}) already provides a "${foreign.name}" tool, so this extension's ` +
|
|
117
|
+
"workflows are disabled for this session to avoid offering the model two orchestrators. " +
|
|
118
|
+
'Set `"workflowsEnabled": true` in .pi/subagents.json to keep both.',
|
|
119
|
+
// Case 1: ours never reached the registry, so there is nothing to withdraw.
|
|
120
|
+
withdraw: !tookOurName,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entry.ts — what a finished workflow leaves behind in the session transcript.
|
|
3
|
+
*
|
|
4
|
+
* A workflow started from `--subagents-workflow-file` has no tool call to hang
|
|
5
|
+
* its result card on, so it appends a custom session entry instead. That entry
|
|
6
|
+
* has to survive a reload, which is why this is a plain-JSON snapshot rather
|
|
7
|
+
* than the live {@link WorkflowTask}: the task holds an `AbortController`, the
|
|
8
|
+
* script source and the run's control handle, none of which belongs in a
|
|
9
|
+
* session file.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately free of any renderer import. The card this data renders through
|
|
12
|
+
* lives in `ui/workflow-card.ts` (`renderWorkflowEntryCard`), so the shape a
|
|
13
|
+
* session file stores does not depend on the code that draws it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { WorkflowMeta } from "./meta.js"
|
|
17
|
+
import type { WorkflowEntry, WorkflowRunStatus } from "./progress.js"
|
|
18
|
+
import type { WorkflowTask } from "./task.js"
|
|
19
|
+
|
|
20
|
+
/** `customType` of the session entry a flag-launched workflow renders through. */
|
|
21
|
+
export const WORKFLOW_ENTRY_TYPE = "subagents:workflow"
|
|
22
|
+
|
|
23
|
+
/** The persisted snapshot of a settled run. */
|
|
24
|
+
export interface WorkflowEntryData {
|
|
25
|
+
name: string
|
|
26
|
+
status: WorkflowRunStatus
|
|
27
|
+
startTime: number
|
|
28
|
+
endTime?: number
|
|
29
|
+
progress: WorkflowEntry[]
|
|
30
|
+
agentCount: number
|
|
31
|
+
totalTokens: number
|
|
32
|
+
meta?: WorkflowMeta
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Snapshot a settled task for {@link WORKFLOW_ENTRY_TYPE}. */
|
|
36
|
+
export function workflowEntryData(task: WorkflowTask): WorkflowEntryData {
|
|
37
|
+
return {
|
|
38
|
+
name: task.workflowName ?? task.id,
|
|
39
|
+
status: task.status,
|
|
40
|
+
startTime: task.startTime,
|
|
41
|
+
endTime: task.endTime,
|
|
42
|
+
progress: task.workflowProgress,
|
|
43
|
+
agentCount: task.agentCount,
|
|
44
|
+
totalTokens: task.totalTokens,
|
|
45
|
+
...(task.meta !== undefined ? { meta: task.meta } : {}),
|
|
46
|
+
}
|
|
47
|
+
}
|