bosun 0.41.0 → 0.41.2
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/.env.example +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* issue-continuation.mjs — Issue-State Continuation Loop Template
|
|
3
|
+
*
|
|
4
|
+
* Workflow-owned continuation loop:
|
|
5
|
+
* - Polls external task state each turn
|
|
6
|
+
* - Continues agent execution while not terminal
|
|
7
|
+
* - Detects stuck sessions from unchanged commit/file progress
|
|
8
|
+
* - Emits session-stuck event and routes by configured onStuck action
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { edge, node, resetLayout } from "./_helpers.mjs";
|
|
12
|
+
|
|
13
|
+
resetLayout();
|
|
14
|
+
|
|
15
|
+
export const CONTINUATION_LOOP_TEMPLATE = {
|
|
16
|
+
id: "template-continuation-loop",
|
|
17
|
+
name: "Continuation Loop",
|
|
18
|
+
description:
|
|
19
|
+
"Issue-state continuation loop. Polls externalStatus, keeps driving the " +
|
|
20
|
+
"agent until terminal state or max turns, and handles stuck sessions " +
|
|
21
|
+
"with retry/escalate/pause.",
|
|
22
|
+
category: "reliability",
|
|
23
|
+
enabled: true,
|
|
24
|
+
recommended: false,
|
|
25
|
+
trigger: "trigger.manual",
|
|
26
|
+
variables: {
|
|
27
|
+
taskId: "",
|
|
28
|
+
worktreePath: "",
|
|
29
|
+
maxTurns: 8,
|
|
30
|
+
pollIntervalMs: 30000,
|
|
31
|
+
terminalStates: ["done", "cancelled"],
|
|
32
|
+
stuckThresholdMs: 300000,
|
|
33
|
+
maxStuckAutoRetries: 1,
|
|
34
|
+
onStuck: "escalate", // retry | escalate | pause
|
|
35
|
+
continuePrompt:
|
|
36
|
+
"Continue this task from the current state. Focus on the next missing step and push toward completion.",
|
|
37
|
+
retryPrompt:
|
|
38
|
+
"No progress was detected recently. Try a different approach and make concrete progress (commit or file updates).",
|
|
39
|
+
sdk: "auto",
|
|
40
|
+
model: "",
|
|
41
|
+
timeoutMs: 1800000,
|
|
42
|
+
},
|
|
43
|
+
nodes: [
|
|
44
|
+
node("trigger", "trigger.manual", "Start Continuation Loop", {}, { x: 420, y: 60 }),
|
|
45
|
+
|
|
46
|
+
node("init-turn", "action.set_variable", "Initialize Turn Counter", {
|
|
47
|
+
key: "continuationTurn",
|
|
48
|
+
value: "0",
|
|
49
|
+
isExpression: true,
|
|
50
|
+
}, { x: 420, y: 170 }),
|
|
51
|
+
|
|
52
|
+
node("init-progress-at", "action.set_variable", "Initialize Progress Clock", {
|
|
53
|
+
key: "lastProgressAt",
|
|
54
|
+
value: "Date.now()",
|
|
55
|
+
isExpression: true,
|
|
56
|
+
}, { x: 420, y: 280 }),
|
|
57
|
+
|
|
58
|
+
node("init-signature", "action.set_variable", "Initialize Progress Signature", {
|
|
59
|
+
key: "lastProgressSignature",
|
|
60
|
+
value: "''",
|
|
61
|
+
isExpression: true,
|
|
62
|
+
}, { x: 420, y: 390 }),
|
|
63
|
+
|
|
64
|
+
node("init-stuck-retry-count", "action.set_variable", "Initialize Stuck Retry Count", {
|
|
65
|
+
key: "stuckRetryCount",
|
|
66
|
+
value: "0",
|
|
67
|
+
isExpression: true,
|
|
68
|
+
}, { x: 420, y: 450 }),
|
|
69
|
+
|
|
70
|
+
node("poll-task", "action.bosun_function", "Poll External Task State", {
|
|
71
|
+
function: "tasks.get",
|
|
72
|
+
args: {
|
|
73
|
+
taskId: "{{taskId}}",
|
|
74
|
+
},
|
|
75
|
+
outputVariable: "continuationTask",
|
|
76
|
+
}, { x: 420, y: 500 }),
|
|
77
|
+
|
|
78
|
+
node("derive-status", "action.set_variable", "Derive External Status", {
|
|
79
|
+
key: "currentExternalStatus",
|
|
80
|
+
value:
|
|
81
|
+
"String(($data?.continuationTask?.externalStatus ?? $data?.continuationTask?.status ?? '') || '').trim().toLowerCase()",
|
|
82
|
+
isExpression: true,
|
|
83
|
+
}, { x: 420, y: 610 }),
|
|
84
|
+
|
|
85
|
+
node("terminal-check", "condition.expression", "Terminal State Reached?", {
|
|
86
|
+
expression:
|
|
87
|
+
"(() => { const s = String($data?.currentExternalStatus || '').trim().toLowerCase(); const t = Array.isArray($data?.terminalStates) ? $data.terminalStates.map(v => String(v || '').trim().toLowerCase()).filter(Boolean) : []; return Boolean(s) && t.includes(s); })()",
|
|
88
|
+
}, { x: 420, y: 720, outputs: ["yes", "no"] }),
|
|
89
|
+
|
|
90
|
+
node("end-terminal", "flow.end", "End: Terminal State", {
|
|
91
|
+
status: "completed",
|
|
92
|
+
message: "Continuation loop completed: terminal external state '{{currentExternalStatus}}' reached for task {{taskId}}.",
|
|
93
|
+
output: {
|
|
94
|
+
reason: "terminal_state",
|
|
95
|
+
taskId: "{{taskId}}",
|
|
96
|
+
externalStatus: "{{currentExternalStatus}}",
|
|
97
|
+
},
|
|
98
|
+
}, { x: 160, y: 860 }),
|
|
99
|
+
|
|
100
|
+
node("max-turns-check", "condition.expression", "Max Turns Reached?", {
|
|
101
|
+
expression: "Number($data?.continuationTurn || 0) >= Number($data?.maxTurns || 0)",
|
|
102
|
+
}, { x: 620, y: 860, outputs: ["yes", "no"] }),
|
|
103
|
+
|
|
104
|
+
node("end-max-turns", "flow.end", "End: Max Turns", {
|
|
105
|
+
status: "failed",
|
|
106
|
+
message: "Continuation loop stopped after reaching maxTurns={{maxTurns}} for task {{taskId}}.",
|
|
107
|
+
output: {
|
|
108
|
+
reason: "max_turns",
|
|
109
|
+
taskId: "{{taskId}}",
|
|
110
|
+
turns: "{{continuationTurn}}",
|
|
111
|
+
},
|
|
112
|
+
}, { x: 460, y: 1000 }),
|
|
113
|
+
|
|
114
|
+
node("run-agent", "action.run_agent", "Drive Agent", {
|
|
115
|
+
prompt: "{{continuePrompt}}",
|
|
116
|
+
taskId: "{{taskId}}",
|
|
117
|
+
cwd: "{{worktreePath}}",
|
|
118
|
+
sdk: "{{sdk}}",
|
|
119
|
+
model: "{{model}}",
|
|
120
|
+
timeoutMs: "{{timeoutMs}}",
|
|
121
|
+
failOnError: false,
|
|
122
|
+
}, { x: 800, y: 1000 }),
|
|
123
|
+
|
|
124
|
+
node("capture-progress", "action.run_command", "Capture Progress Signature", {
|
|
125
|
+
command:
|
|
126
|
+
"node -e \"const cp=require('node:child_process');const crypto=require('node:crypto');const head=(cp.execSync('git rev-parse HEAD',{encoding:'utf8'}).trim()||'');const dirtyRaw=cp.execSync('git status --porcelain=v1',{encoding:'utf8'});const dirtyCount=dirtyRaw.split(/\\r?\\n/).filter(Boolean).length;const statusDigest=crypto.createHash('sha1').update(dirtyRaw).digest('hex').slice(0,16);process.stdout.write(JSON.stringify({head,dirtyCount,statusDigest}));\"",
|
|
127
|
+
cwd: "{{worktreePath}}",
|
|
128
|
+
failOnError: false,
|
|
129
|
+
}, { x: 800, y: 1120 }),
|
|
130
|
+
|
|
131
|
+
node("derive-signature", "action.set_variable", "Derive Signature", {
|
|
132
|
+
key: "currentProgressSignature",
|
|
133
|
+
value:
|
|
134
|
+
"(() => { const raw = String($ctx.getNodeOutput('capture-progress')?.output || '').trim(); try { const parsed = JSON.parse(raw); const head = String(parsed?.head || ''); const dirty = Number(parsed?.dirtyCount || 0); const statusDigest = String(parsed?.statusDigest || ''); return `${head}:${dirty}:${statusDigest}`; } catch { return ''; } })()",
|
|
135
|
+
isExpression: true,
|
|
136
|
+
}, { x: 800, y: 1240 }),
|
|
137
|
+
|
|
138
|
+
node("progress-changed", "condition.expression", "Progress Changed?", {
|
|
139
|
+
expression: "String($data?.currentProgressSignature || '') !== String($data?.lastProgressSignature || '')",
|
|
140
|
+
}, { x: 800, y: 1360, outputs: ["yes", "no"] }),
|
|
141
|
+
|
|
142
|
+
node("mark-progress-at", "action.set_variable", "Update Progress Clock", {
|
|
143
|
+
key: "lastProgressAt",
|
|
144
|
+
value:
|
|
145
|
+
"(() => { const changed = String($data?.currentProgressSignature || '') !== String($data?.lastProgressSignature || ''); return changed ? Date.now() : Number($data?.lastProgressAt || 0); })()",
|
|
146
|
+
isExpression: true,
|
|
147
|
+
}, { x: 680, y: 1490 }),
|
|
148
|
+
|
|
149
|
+
node("mark-progress-sig", "action.set_variable", "Update Progress Signature", {
|
|
150
|
+
key: "lastProgressSignature",
|
|
151
|
+
value: "$data?.currentProgressSignature || ''",
|
|
152
|
+
isExpression: true,
|
|
153
|
+
}, { x: 680, y: 1600 }),
|
|
154
|
+
|
|
155
|
+
node("reset-stuck-retry-count", "action.set_variable", "Reset Stuck Retry Count On Progress", {
|
|
156
|
+
key: "stuckRetryCount",
|
|
157
|
+
value:
|
|
158
|
+
"(() => { const changed = String($data?.currentProgressSignature || '') !== String($data?.lastProgressSignature || ''); return changed ? 0 : Number($data?.stuckRetryCount || 0); })()",
|
|
159
|
+
isExpression: true,
|
|
160
|
+
}, { x: 680, y: 1710 }),
|
|
161
|
+
|
|
162
|
+
node("stuck-check", "condition.expression", "Session Stuck?", {
|
|
163
|
+
expression:
|
|
164
|
+
"(Date.now() - Number($data?.lastProgressAt || 0)) >= Number($data?.stuckThresholdMs || 0)",
|
|
165
|
+
}, { x: 980, y: 1820, outputs: ["yes", "no"] }),
|
|
166
|
+
|
|
167
|
+
node("emit-stuck", "action.emit_event", "Emit session-stuck", {
|
|
168
|
+
eventType: "session-stuck",
|
|
169
|
+
payload: {
|
|
170
|
+
taskId: "{{taskId}}",
|
|
171
|
+
turn: "{{continuationTurn}}",
|
|
172
|
+
externalStatus: "{{currentExternalStatus}}",
|
|
173
|
+
stuckThresholdMs: "{{stuckThresholdMs}}",
|
|
174
|
+
stuckForMs: "{{Math.max(0, Date.now() - Number($data?.lastProgressAt || 0))}}",
|
|
175
|
+
onStuck: "{{onStuck}}",
|
|
176
|
+
stuckRetryCount: "{{stuckRetryCount}}",
|
|
177
|
+
maxStuckAutoRetries: "{{maxStuckAutoRetries}}",
|
|
178
|
+
lastProgressAt: "{{lastProgressAt}}",
|
|
179
|
+
lastProgressSignature: "{{lastProgressSignature}}",
|
|
180
|
+
currentProgressSignature: "{{currentProgressSignature}}",
|
|
181
|
+
progressSnapshot: "{{$ctx.getNodeOutput('capture-progress')?.output || ''}}",
|
|
182
|
+
lastAgentSuccess: "{{$ctx.getNodeOutput('run-agent')?.success === true}}",
|
|
183
|
+
lastAgentOutput: "{{$ctx.getNodeOutput('run-agent')?.output || ''}}",
|
|
184
|
+
},
|
|
185
|
+
outputVariable: "sessionStuckEvent",
|
|
186
|
+
}, { x: 980, y: 1600 }),
|
|
187
|
+
|
|
188
|
+
node("stuck-route", "condition.switch", "Route onStuck Action", {
|
|
189
|
+
value: "$data?.onStuck || 'escalate'",
|
|
190
|
+
cases: {
|
|
191
|
+
retry: "retry",
|
|
192
|
+
escalate: "escalate",
|
|
193
|
+
pause: "pause",
|
|
194
|
+
},
|
|
195
|
+
}, { x: 980, y: 1710, outputs: ["retry", "escalate", "pause", "default"] }),
|
|
196
|
+
|
|
197
|
+
node("stuck-retry-budget", "condition.expression", "Stuck Retry Budget Remaining?", {
|
|
198
|
+
expression: "Number($data?.stuckRetryCount || 0) < Number($data?.maxStuckAutoRetries || 0)",
|
|
199
|
+
}, { x: 760, y: 1820, outputs: ["yes", "no"] }),
|
|
200
|
+
|
|
201
|
+
node("stuck-retry", "action.run_agent", "Retry After Stuck", {
|
|
202
|
+
prompt:
|
|
203
|
+
"{{retryPrompt}}\n\n" +
|
|
204
|
+
"Stuck context:\n" +
|
|
205
|
+
"- taskId: {{taskId}}\n" +
|
|
206
|
+
"- externalStatus: {{currentExternalStatus}}\n" +
|
|
207
|
+
"- turn: {{continuationTurn}}\n" +
|
|
208
|
+
"- stuckRetryCount: {{stuckRetryCount}}/{{maxStuckAutoRetries}}\n" +
|
|
209
|
+
"- stuckForMs: {{Math.max(0, Date.now() - Number($data?.lastProgressAt || 0))}}\n" +
|
|
210
|
+
"- lastProgressSignature: {{lastProgressSignature}}\n" +
|
|
211
|
+
"- currentProgressSignature: {{currentProgressSignature}}\n" +
|
|
212
|
+
"- progressSnapshot: {{$ctx.getNodeOutput('capture-progress')?.output || ''}}\n" +
|
|
213
|
+
"- lastAgentOutput: {{$ctx.getNodeOutput('run-agent')?.output || ''}}\n\n" +
|
|
214
|
+
"Try a materially different approach. If you cannot create progress, explain the specific blocker.",
|
|
215
|
+
taskId: "{{taskId}}",
|
|
216
|
+
cwd: "{{worktreePath}}",
|
|
217
|
+
sdk: "{{sdk}}",
|
|
218
|
+
model: "{{model}}",
|
|
219
|
+
timeoutMs: "{{timeoutMs}}",
|
|
220
|
+
failOnError: false,
|
|
221
|
+
}, { x: 760, y: 1830 }),
|
|
222
|
+
|
|
223
|
+
node("increment-stuck-retry-count", "action.set_variable", "Increment Stuck Retry Count", {
|
|
224
|
+
key: "stuckRetryCount",
|
|
225
|
+
value: "Number($data?.stuckRetryCount || 0) + 1",
|
|
226
|
+
isExpression: true,
|
|
227
|
+
}, { x: 760, y: 1940 }),
|
|
228
|
+
|
|
229
|
+
node("stuck-escalate", "notify.log", "Escalate Stuck Session", {
|
|
230
|
+
level: "warn",
|
|
231
|
+
message:
|
|
232
|
+
"session-stuck: escalation requested for task {{taskId}} at turn {{continuationTurn}} (externalStatus={{currentExternalStatus}}, stuckForMs={{Math.max(0, Date.now() - Number($data?.lastProgressAt || 0))}}, stuckRetryCount={{stuckRetryCount}}/{{maxStuckAutoRetries}}, lastProgressSignature={{lastProgressSignature}}, currentProgressSignature={{currentProgressSignature}})",
|
|
233
|
+
}, { x: 980, y: 1830 }),
|
|
234
|
+
|
|
235
|
+
node("stuck-escalate-budget", "notify.log", "Escalate Stuck Session (Retry Limit)", {
|
|
236
|
+
level: "warn",
|
|
237
|
+
message:
|
|
238
|
+
"session-stuck: retry budget exhausted for task {{taskId}} at turn {{continuationTurn}} (externalStatus={{currentExternalStatus}}, stuckForMs={{Math.max(0, Date.now() - Number($data?.lastProgressAt || 0))}}, stuckRetryCount={{stuckRetryCount}}/{{maxStuckAutoRetries}}, lastProgressSignature={{lastProgressSignature}}, currentProgressSignature={{currentProgressSignature}})",
|
|
239
|
+
}, { x: 760, y: 2050 }),
|
|
240
|
+
|
|
241
|
+
node("stuck-pause", "notify.log", "Pause Stuck Session", {
|
|
242
|
+
level: "warn",
|
|
243
|
+
message:
|
|
244
|
+
"session-stuck: paused task {{taskId}} at turn {{continuationTurn}} (externalStatus={{currentExternalStatus}})",
|
|
245
|
+
}, { x: 1200, y: 1830 }),
|
|
246
|
+
|
|
247
|
+
node("end-escalated", "flow.end", "End: Escalated", {
|
|
248
|
+
status: "failed",
|
|
249
|
+
message: "Continuation loop escalated due to session-stuck for task {{taskId}}.",
|
|
250
|
+
output: {
|
|
251
|
+
reason: "stuck_escalated",
|
|
252
|
+
taskId: "{{taskId}}",
|
|
253
|
+
event: "{{sessionStuckEvent.eventType}}",
|
|
254
|
+
stuckRetryCount: "{{stuckRetryCount}}",
|
|
255
|
+
maxStuckAutoRetries: "{{maxStuckAutoRetries}}",
|
|
256
|
+
},
|
|
257
|
+
}, { x: 980, y: 1950 }),
|
|
258
|
+
|
|
259
|
+
node("end-paused", "flow.end", "End: Paused", {
|
|
260
|
+
status: "completed",
|
|
261
|
+
message: "Continuation loop paused due to session-stuck for task {{taskId}}.",
|
|
262
|
+
output: {
|
|
263
|
+
reason: "stuck_paused",
|
|
264
|
+
taskId: "{{taskId}}",
|
|
265
|
+
event: "{{sessionStuckEvent.eventType}}",
|
|
266
|
+
},
|
|
267
|
+
}, { x: 1200, y: 1950 }),
|
|
268
|
+
|
|
269
|
+
node("wait-next-turn", "action.delay", "Wait Poll Interval", {
|
|
270
|
+
ms: "{{pollIntervalMs}}",
|
|
271
|
+
reason: "Waiting before next external status poll",
|
|
272
|
+
}, { x: 760, y: 1950 }),
|
|
273
|
+
|
|
274
|
+
node("increment-turn", "action.set_variable", "Increment Turn", {
|
|
275
|
+
key: "continuationTurn",
|
|
276
|
+
value: "Number($data?.continuationTurn || 0) + 1",
|
|
277
|
+
isExpression: true,
|
|
278
|
+
}, { x: 760, y: 2060 }),
|
|
279
|
+
|
|
280
|
+
node("wait-next-turn-no-stuck", "action.delay", "Wait (No Stuck)", {
|
|
281
|
+
ms: "{{pollIntervalMs}}",
|
|
282
|
+
reason: "Waiting before next external status poll",
|
|
283
|
+
}, { x: 1080, y: 1950 }),
|
|
284
|
+
|
|
285
|
+
node("increment-turn-no-stuck", "action.set_variable", "Increment Turn (No Stuck)", {
|
|
286
|
+
key: "continuationTurn",
|
|
287
|
+
value: "Number($data?.continuationTurn || 0) + 1",
|
|
288
|
+
isExpression: true,
|
|
289
|
+
}, { x: 1080, y: 2060 }),
|
|
290
|
+
],
|
|
291
|
+
edges: [
|
|
292
|
+
edge("trigger", "init-turn"),
|
|
293
|
+
edge("init-turn", "init-progress-at"),
|
|
294
|
+
edge("init-progress-at", "init-signature"),
|
|
295
|
+
edge("init-signature", "init-stuck-retry-count"),
|
|
296
|
+
edge("init-stuck-retry-count", "poll-task"),
|
|
297
|
+
edge("poll-task", "derive-status"),
|
|
298
|
+
edge("derive-status", "terminal-check"),
|
|
299
|
+
edge("terminal-check", "end-terminal", { condition: "$output?.result === true", port: "yes" }),
|
|
300
|
+
edge("terminal-check", "max-turns-check", { condition: "$output?.result !== true", port: "no" }),
|
|
301
|
+
edge("max-turns-check", "end-max-turns", { condition: "$output?.result === true", port: "yes" }),
|
|
302
|
+
edge("max-turns-check", "run-agent", { condition: "$output?.result !== true", port: "no" }),
|
|
303
|
+
edge("run-agent", "capture-progress"),
|
|
304
|
+
edge("capture-progress", "derive-signature"),
|
|
305
|
+
edge("derive-signature", "progress-changed"),
|
|
306
|
+
edge("progress-changed", "mark-progress-at"),
|
|
307
|
+
edge("mark-progress-at", "mark-progress-sig"),
|
|
308
|
+
edge("mark-progress-sig", "reset-stuck-retry-count"),
|
|
309
|
+
edge("reset-stuck-retry-count", "stuck-check"),
|
|
310
|
+
edge("stuck-check", "emit-stuck", { condition: "$output?.result === true", port: "yes" }),
|
|
311
|
+
edge("stuck-check", "wait-next-turn-no-stuck", { condition: "$output?.result !== true", port: "no" }),
|
|
312
|
+
edge("emit-stuck", "stuck-route"),
|
|
313
|
+
edge("stuck-route", "stuck-retry-budget", { port: "retry" }),
|
|
314
|
+
edge("stuck-route", "stuck-escalate", { port: "escalate" }),
|
|
315
|
+
edge("stuck-route", "stuck-pause", { port: "pause" }),
|
|
316
|
+
edge("stuck-route", "stuck-escalate", { port: "default" }),
|
|
317
|
+
edge("stuck-retry-budget", "stuck-retry", { condition: "$output?.result === true", port: "yes" }),
|
|
318
|
+
edge("stuck-retry-budget", "stuck-escalate-budget", { condition: "$output?.result !== true", port: "no" }),
|
|
319
|
+
edge("stuck-retry", "increment-stuck-retry-count"),
|
|
320
|
+
edge("increment-stuck-retry-count", "wait-next-turn"),
|
|
321
|
+
edge("stuck-escalate", "end-escalated"),
|
|
322
|
+
edge("stuck-escalate-budget", "end-escalated"),
|
|
323
|
+
edge("stuck-pause", "end-paused"),
|
|
324
|
+
edge("wait-next-turn", "increment-turn"),
|
|
325
|
+
edge("wait-next-turn-no-stuck", "increment-turn-no-stuck"),
|
|
326
|
+
edge("increment-turn", "poll-task", { backEdge: true, maxIterations: 500 }),
|
|
327
|
+
edge("increment-turn-no-stuck", "poll-task", { backEdge: true, maxIterations: 500 }),
|
|
328
|
+
],
|
|
329
|
+
metadata: {
|
|
330
|
+
author: "bosun",
|
|
331
|
+
version: 1,
|
|
332
|
+
createdAt: "2026-03-10T00:00:00Z",
|
|
333
|
+
templateVersion: "1.1.0",
|
|
334
|
+
tags: ["continuation", "loop", "linear", "external-status", "stuck-detection"],
|
|
335
|
+
configType: "continuation-loop",
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export default CONTINUATION_LOOP_TEMPLATE;
|