@harms-haus/pi-workflows 1.0.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/LICENSE +21 -0
- package/README.md +113 -0
- package/docs/architecture.md +318 -0
- package/docs/configuration-reference.md +427 -0
- package/docs/contributing.md +132 -0
- package/docs/examples.md +1242 -0
- package/docs/hook-lifecycle.md +380 -0
- package/docs/state-management.md +534 -0
- package/docs/subworkflows.md +428 -0
- package/docs/template-variables.md +383 -0
- package/docs/testing.md +479 -0
- package/package.json +69 -0
- package/skills/workflow-generation/SKILL.md +272 -0
- package/src/TimerManager.ts +67 -0
- package/src/command.ts +199 -0
- package/src/config/index.ts +11 -0
- package/src/config/loading-parse.ts +205 -0
- package/src/config/loading-phases.ts +78 -0
- package/src/config/loading-resolve.ts +82 -0
- package/src/config/loading.ts +202 -0
- package/src/config/templates.ts +25 -0
- package/src/config/validation.ts +258 -0
- package/src/hooks.ts +265 -0
- package/src/index.ts +98 -0
- package/src/prompts.ts +141 -0
- package/src/renderers.ts +46 -0
- package/src/state.ts +426 -0
- package/src/tool.ts +364 -0
- package/src/types.ts +211 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# Hook Lifecycle
|
|
2
|
+
|
|
3
|
+
The pi-workflows extension registers **4 hook functions** on pi framework lifecycle events. Each hook receives the current [`WorkflowState`](#hookstatemutation-and-workflowstate) and the loaded [`WorkflowDefinition`](configuration-reference.md) map, then either mutates state, injects messages, or returns control signals back to the framework.
|
|
4
|
+
|
|
5
|
+
For the overall extension architecture, see [architecture.md](architecture.md). For state semantics and the `resolveActive` / `isActive` helpers, see [state-management.md](state-management.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Registration
|
|
10
|
+
|
|
11
|
+
All four hooks are wired in [`src/index.ts`](../src/index.ts) via `pi.on()`:
|
|
12
|
+
|
|
13
|
+
| Framework Event | Hook Function | Called When |
|
|
14
|
+
| -------------------- | ------------------------ | ------------------------------------------------ |
|
|
15
|
+
| `session_start` | `updateStatus` | Session loaded or created |
|
|
16
|
+
| `session_tree` | `updateStatus` | Session branch changed |
|
|
17
|
+
| `turn_end` | `updateStatus` | Agent turn completed |
|
|
18
|
+
| `tool_call` | `handleToolCall` | Agent requests a tool invocation |
|
|
19
|
+
| `before_agent_start` | `handleBeforeAgentStart` | Before the agent begins a new turn |
|
|
20
|
+
| `agent_end` | `handleAgentEnd` | Agent stops (completion, error, or interruption) |
|
|
21
|
+
|
|
22
|
+
`session_start` and `session_tree` also handle definition loading and state reconstruction before calling `updateStatus`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## updateStatus
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
function updateStatus(
|
|
30
|
+
ctx: { ui: { setStatus: (key: string, text: string | undefined) => void } },
|
|
31
|
+
state: WorkflowState | null,
|
|
32
|
+
definitions: Record<string, WorkflowDefinition>,
|
|
33
|
+
): void;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Called on:** `session_start`, `session_tree`, `turn_end`
|
|
37
|
+
|
|
38
|
+
### Logic
|
|
39
|
+
|
|
40
|
+
1. **Early-return conditions** → calls `ctx.ui.setStatus("workflow", undefined)` to clear the status bar and returns immediately if any of these conditions are met:
|
|
41
|
+
- `!state` (null state)
|
|
42
|
+
- `!state.active` (inactive state)
|
|
43
|
+
- `state.currentPath.length === 0` (empty path)
|
|
44
|
+
- `!(state.currentPath[0].workflowKey in definitions)` (missing root definition)
|
|
45
|
+
|
|
46
|
+
2. **Build status parts array** → starts with the top-level workflow name.
|
|
47
|
+
|
|
48
|
+
3. **Loop through `currentPath` segments** → for each segment, looks up its definition and phase entry:
|
|
49
|
+
- For `SubworkflowReference` entries: adds `{name} [current/total]` (no emoji)
|
|
50
|
+
- For `PhaseDefinition` entries: adds `{emoji} {name} [current/total]`
|
|
51
|
+
|
|
52
|
+
4. **Bounds check** → if any segment's `phaseIndex` is out of bounds (>= phases.length), clears status bar and returns.
|
|
53
|
+
|
|
54
|
+
5. **Format and set** → joins all parts with `>` and calls `ctx.ui.setStatus("workflow", statusString)`.
|
|
55
|
+
|
|
56
|
+
### Status Format
|
|
57
|
+
|
|
58
|
+
All status strings use `>` as the segment separator. Every level in the path — including subworkflow containers — shows its own `[N/M]` progress counter. Emojis appear only on concrete `PhaseDefinition` entries; subworkflow levels (`SubworkflowReference`) show name + progress without an emoji.
|
|
59
|
+
|
|
60
|
+
**Linear workflow** (`currentPath.length === 1`):
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
{workflowName} > {phaseEmoji} {phaseName} [{current}/{total}]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Example: `CI/CD Pipeline > 📋 Planning [1/3]`
|
|
67
|
+
|
|
68
|
+
**Nested workflow** (`currentPath.length === 2`) — shows the subworkflow container with its own progress, then the inner phase:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
{workflowName} > {subworkflowName} [{subCurrent}/{subTotal}] > {phaseEmoji} {phaseName} [{current}/{total}]
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Example: `Release Pipeline > Code Review [2/3] > 🔍 Static Analysis [1/2]`
|
|
75
|
+
|
|
76
|
+
**Deep nesting** (`currentPath.length > 2`) — each subworkflow level repeats the `{name} [N/M]` pattern, terminated by the leaf phase with its emoji:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
{workflowName} > {sub1} [N/M] > {sub2} [N/M] > {phaseEmoji} {phaseName} [N/M]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Example: `RPIR Development > Implementation [3/5] > Testing [2/2] > 🧪 Unit Tests [1/4]`
|
|
83
|
+
|
|
84
|
+
Progress numbers are 1-indexed (`phaseIndex + 1`). Each level's total comes from the corresponding workflow definition's `phases.length`.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## handleToolCall
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
function handleToolCall(
|
|
92
|
+
event: ToolCallEvent,
|
|
93
|
+
state: WorkflowState | null,
|
|
94
|
+
definitions: Record<string, WorkflowDefinition>,
|
|
95
|
+
): { block: true; reason: string } | void;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Called on:** `tool_call`
|
|
99
|
+
|
|
100
|
+
Returns `{ block: true; reason: string }` to block the tool call, or `void` (undefined) to allow it.
|
|
101
|
+
|
|
102
|
+
### Decision Flow
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
state is null or inactive → allow (return void)
|
|
106
|
+
│
|
|
107
|
+
▼
|
|
108
|
+
resolveActive fails → allow
|
|
109
|
+
│
|
|
110
|
+
▼
|
|
111
|
+
toolName === "workflow_step" → always allow
|
|
112
|
+
│
|
|
113
|
+
▼
|
|
114
|
+
phase.tools is undefined → allow (no restrictions for this phase)
|
|
115
|
+
│
|
|
116
|
+
▼
|
|
117
|
+
phase.tools.blacklist is set AND toolName is in blockedTools → BLOCK
|
|
118
|
+
│
|
|
119
|
+
▼
|
|
120
|
+
phase.tools.whitelist is set AND toolName is NOT in whitelist → BLOCK
|
|
121
|
+
│
|
|
122
|
+
▼
|
|
123
|
+
otherwise → allow
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Key behaviors:
|
|
127
|
+
|
|
128
|
+
- **`workflow_step` is always allowed** regardless of blacklist/whitelist. This ensures the agent can always advance phases.
|
|
129
|
+
- If a phase has no `tools` property, all tools are permitted.
|
|
130
|
+
- Exactly one of `blacklist` or `whitelist` may be set per phase (mutually exclusive).
|
|
131
|
+
|
|
132
|
+
### Block Reason Template
|
|
133
|
+
|
|
134
|
+
When a tool is blocked, the reason string is generated from the definition's `blockReasonTemplate` (or the built-in default). Available template variables:
|
|
135
|
+
|
|
136
|
+
| Variable | Description |
|
|
137
|
+
| ---------------- | -------------------------------------- |
|
|
138
|
+
| `{workflowName}` | Human-readable workflow name |
|
|
139
|
+
| `{phaseName}` | Current phase name |
|
|
140
|
+
| `{toolName}` | The tool that was blocked |
|
|
141
|
+
| `{allowedTools}` | Human-readable list of permitted tools |
|
|
142
|
+
|
|
143
|
+
**Default template:**
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
[workflow] The tool "{toolName}" is blocked during the {phaseName} phase.
|
|
147
|
+
Refer to the current phase instructions for allowed tools and approaches.
|
|
148
|
+
When finished, call workflow_step to advance to the next phase.
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
When the block comes from a blacklist, `{allowedTools}` resolves to `"all except: "` + the joined blacklist. When from a whitelist, it resolves to the joined whitelist.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## handleBeforeAgentStart
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
function handleBeforeAgentStart(
|
|
159
|
+
state: WorkflowState | null,
|
|
160
|
+
definitions: Record<string, WorkflowDefinition>,
|
|
161
|
+
): { message: { customType: string; content: string; display: boolean } } | void;
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Called on:** `before_agent_start`
|
|
165
|
+
|
|
166
|
+
### Logic
|
|
167
|
+
|
|
168
|
+
1. If state is inactive or null, or `resolveActive` fails → return `void` (no injection).
|
|
169
|
+
2. Otherwise, calls [`buildContextPrompt(active)`](#buildcontextprompt-details) to generate the full context string.
|
|
170
|
+
3. Returns a hidden message with `customType: "workflow:context"` and `display: false`.
|
|
171
|
+
|
|
172
|
+
The returned message is injected into the conversation before the agent begins its turn. Because `display: false`, the user does not see it, but the agent reads it as context.
|
|
173
|
+
|
|
174
|
+
### buildContextPrompt Details
|
|
175
|
+
|
|
176
|
+
The prompt is assembled from these sections in order:
|
|
177
|
+
|
|
178
|
+
| Section | Source | Notes |
|
|
179
|
+
| ---------------------- | ------------------------------------------------ | ----------------------------------------------- |
|
|
180
|
+
| **Header line** | `[Workflow path: {breadcrumb} ▸ {emoji} {name}]` | Breadcrumb from `active.breadcrumb` |
|
|
181
|
+
| **Role instruction** | `definition.roleInstruction` or default | Template-resolved with workflow/phase variables |
|
|
182
|
+
| **Task details** | `taskDescription`, `taskId` | From `state` |
|
|
183
|
+
| **Current phase** | Emoji + name | From `active.currentPhase` |
|
|
184
|
+
| **Progress** | `globalStepCount` and `phaseIndex`/total | Format varies for linear vs nested |
|
|
185
|
+
| **Phase instructions** | `currentPhase.instructions` | Template-resolved |
|
|
186
|
+
| **Profiles** | `availableProfiles` + all workflow profiles | Lists per-phase and global profiles |
|
|
187
|
+
| **Advance reminder** | `definition.advanceReminder` or default | Reminds agent to call `workflow_step` |
|
|
188
|
+
|
|
189
|
+
**Default role instruction:**
|
|
190
|
+
|
|
191
|
+
> You are the ORCHESTRATOR for this workflow. You must NOT use the edit or write tools directly. All implementation work must be delegated to subagents via the delegate_to_subagents tool. Follow the phase instructions precisely.
|
|
192
|
+
|
|
193
|
+
**Default advance reminder:**
|
|
194
|
+
|
|
195
|
+
> When you finish this phase, call the workflow_step tool with action='next' to advance to the next phase. If you need to restart the current scope from the beginning, use action='loop'.
|
|
196
|
+
|
|
197
|
+
Template variables available to `roleInstruction`, `instructions`, and `advanceReminder`:
|
|
198
|
+
|
|
199
|
+
`{workflowName}`, `{workflowKey}`, `{description}`, `{taskId}`, `{phaseId}`, `{phaseName}`, `{previousPhaseName}`, `{nextPhaseName}`, `{blockedToolsList}`, `{toolName}`, `{breadcrumbPath}`, `{globalStepCount}`
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## handleAgentEnd
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
function handleAgentEnd(
|
|
207
|
+
pi: ExtensionAPI,
|
|
208
|
+
state: WorkflowState | null,
|
|
209
|
+
definitions: Record<string, WorkflowDefinition>,
|
|
210
|
+
ctx: ExtensionContext,
|
|
211
|
+
event: AgentEndEvent,
|
|
212
|
+
): HookStateMutation;
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Called on:** `agent_end`
|
|
216
|
+
|
|
217
|
+
This hook has three distinct code paths. It returns a [`HookStateMutation`](#hookstatemutation-interface) to tell `index.ts` how to update module state.
|
|
218
|
+
|
|
219
|
+
### Case A — Workflow just completed (DONE)
|
|
220
|
+
|
|
221
|
+
**Condition:** `state` exists, `state.active === false`, `state.completionNotified === false`.
|
|
222
|
+
|
|
223
|
+
There are two sub-cases:
|
|
224
|
+
|
|
225
|
+
#### Normal completion (`state.cancelled === false`)
|
|
226
|
+
|
|
227
|
+
1. Looks up the definition via `state.workflowKey`.
|
|
228
|
+
2. Resolves the `completionMessage` template (or default) with variables: `{workflowName}`, `{taskDescription}`, `{taskId}`, `{phaseCount}`.
|
|
229
|
+
3. Sends a visible message via `pi.sendMessage` with `customType: "workflow:complete"`, `display: true`, `triggerTurn: false`.
|
|
230
|
+
4. Sets `state.completionNotified = true`.
|
|
231
|
+
5. Clears the status bar.
|
|
232
|
+
6. Returns `{ unload: true, persist: true }`.
|
|
233
|
+
|
|
234
|
+
**Default completion message:**
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
✅ **{workflowName} Complete**
|
|
238
|
+
|
|
239
|
+
**Task:** {taskDescription}
|
|
240
|
+
**Task ID:** {taskId}
|
|
241
|
+
**Phases completed:** {phaseCount}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### Cancellation (`state.cancelled === true`)
|
|
245
|
+
|
|
246
|
+
Same flow as normal completion, but resolves `completionMessage` if set, otherwise falls back to `DEFAULT_CANCELLED_MESSAGE` (not `DEFAULT_COMPLETION_MESSAGE`). Returns `{ unload: true, persist: false }` (no persistence of cancelled state).
|
|
247
|
+
|
|
248
|
+
> **Note:** There is no separate `cancelledMessage` field on `WorkflowDefinition`. The cancellation path reuses `completionMessage` with `DEFAULT_CANCELLED_MESSAGE` as fallback. Additionally, the `/cancel-workflow` command (`src/command.ts`) sends a hardcoded message with no template resolution and unloads state immediately, so this hook branch only fires when cancelled state is reached through other paths (e.g., session resume).
|
|
249
|
+
|
|
250
|
+
**Default cancelled message:**
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
❌ **{workflowName} Cancelled**
|
|
254
|
+
|
|
255
|
+
**Task:** {taskDescription}
|
|
256
|
+
**Task ID:** {taskId}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Case B — Workflow still active (agent stopped mid-workflow)
|
|
260
|
+
|
|
261
|
+
**Condition:** `state.active === true`.
|
|
262
|
+
|
|
263
|
+
1. Checks [`wasAborted(event.messages)`](#wasaborted-check) — if the user interrupted the agent, returns `{ unload: false, persist: false }` (no enforcement).
|
|
264
|
+
2. Resolves the active workflow. If resolution fails, returns no-op.
|
|
265
|
+
3. Resolves the `notDoneReminder` template (or default) with variables: `{workflowName}`, `{phaseName}`, `{phaseEmoji}`, `{phaseInstructions}`, `{taskDescription}`, `{taskId}`, `{workflowKey}`.
|
|
266
|
+
4. Delegates to `startCountdown(pi, ctx, reminder)`, which behaves differently based on `ctx.hasUI`:
|
|
267
|
+
- **With UI** — displays a `workflow-countdown` widget above the editor and uses `timerManager.startInterval(1000, ...)` to tick down from 3 seconds, updating the widget each second. When the countdown reaches zero, calls `timerManager.clearAll()`, removes the widget, and injects the reminder via `pi.sendUserMessage(reminder)`.
|
|
268
|
+
- **Without UI** — sends an immediate `pi.sendMessage` with `customType: "workflow:countdown"`, `display: true`, `triggerTurn: false`, then uses `timerManager.startTimeout(3000, ...)` to inject the reminder after 3 seconds.
|
|
269
|
+
In both paths, if the user started typing during the grace period, the `pi.sendUserMessage` call is caught and silently ignored. All timer handles are tracked by the [`TimerManager`](../src/TimerManager.ts) singleton (`timerManager`), which prevents stale callbacks and allows clean cancellation via `timerManager.clearAll()`.
|
|
270
|
+
5. Returns `{ unload: false, persist: false }`.
|
|
271
|
+
|
|
272
|
+
**Default not-done reminder:**
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
⚠️ The {workflowName} is still active. Current phase: {phaseEmoji} {phaseName}.
|
|
276
|
+
|
|
277
|
+
You must NOT stop yet. The workflow requires you to complete the current phase
|
|
278
|
+
and call workflow_step to advance.
|
|
279
|
+
|
|
280
|
+
Current phase instructions:
|
|
281
|
+
{phaseInstructions}
|
|
282
|
+
|
|
283
|
+
Continue working on the current phase and call workflow_step when done.
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Case C — Already notified or no state
|
|
287
|
+
|
|
288
|
+
**Condition:** State is null, or `completionNotified === true`, or any other unhandled case.
|
|
289
|
+
|
|
290
|
+
Returns `{ unload: false, persist: false }` — a no-op.
|
|
291
|
+
|
|
292
|
+
### wasAborted Check
|
|
293
|
+
|
|
294
|
+
The `wasAborted` helper walks `event.messages` in reverse to find the last assistant message. If that message has `stopReason === "aborted"`, the agent was interrupted by the user. This prevents the auto-continue countdown from firing when the user deliberately stopped the agent.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## HookStateMutation Interface
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
interface HookStateMutation {
|
|
302
|
+
/** If true, set module state to null (unload workflow). */
|
|
303
|
+
unload: boolean;
|
|
304
|
+
/** If set, replace module state with this value (mutated copy). */
|
|
305
|
+
state?: WorkflowState;
|
|
306
|
+
/** If true, persist the current state via pi.appendEntry. */
|
|
307
|
+
persist: boolean;
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Returned by `handleAgentEnd` and consumed by `index.ts` in the `agent_end` handler:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const mutation = handleAgentEnd(pi, state, definitions, ctx, event);
|
|
315
|
+
if (mutation.unload) {
|
|
316
|
+
state = null; // Unload: clear module state
|
|
317
|
+
} else if (mutation.state) {
|
|
318
|
+
state = mutation.state; // Replace: use the returned state
|
|
319
|
+
}
|
|
320
|
+
if (mutation.persist && state) {
|
|
321
|
+
persistState(pi, state); // Persist: write to session entry log
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Mutation Semantics by Case
|
|
326
|
+
|
|
327
|
+
| Case | `unload` | `state` | `persist` | Effect |
|
|
328
|
+
| ------------------------ | -------- | ------- | --------- | ------------------------------------ |
|
|
329
|
+
| No-op | `false` | — | `false` | No changes |
|
|
330
|
+
| Normal completion | `true` | — | `true` | State persisted, then unloaded |
|
|
331
|
+
| Cancellation | `true` | — | `false` | State discarded, unloaded |
|
|
332
|
+
| Still active / countdown | `false` | — | `false` | No state change; auto-continue fires |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Message Custom Types
|
|
337
|
+
|
|
338
|
+
The hooks produce three distinct `customType` values, each with a registered renderer in [`src/renderers.ts`](../src/renderers.ts):
|
|
339
|
+
|
|
340
|
+
| Custom Type | Produced By | `display` | `triggerTurn` | Rendered Appearance |
|
|
341
|
+
| -------------------- | ------------------------- | --------- | ------------- | -------------------------------------------- |
|
|
342
|
+
| `workflow:context` | `handleBeforeAgentStart` | `false` | — | `🔄 [Workflow Context injected]` (dim) |
|
|
343
|
+
| `workflow:complete` | `handleAgentEnd` (Case A) | `true` | `false` | Bold success/completion message in green |
|
|
344
|
+
| `workflow:countdown` | `handleAgentEnd` (Case B) | `true` | `false` | `⏳ Auto-continuing workflow in 3s...` (dim) |
|
|
345
|
+
|
|
346
|
+
### Renderer Details
|
|
347
|
+
|
|
348
|
+
Each custom type has a dedicated renderer registered via `pi.registerMessageRenderer`:
|
|
349
|
+
|
|
350
|
+
- **`workflow:context`** — Renders a minimal dim accent line. Because `display: false`, this message is hidden from the user's main conversation view; the renderer produces a subtle indicator only.
|
|
351
|
+
- **`workflow:complete`** — Renders the full completion or cancellation message in bold green (`theme.fg("success", ...)`).
|
|
352
|
+
- **`workflow:countdown`** — Renders the countdown timer text in accent color with dim styling, showing the user they have a grace period to interrupt.
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Hook Interaction Flow
|
|
357
|
+
|
|
358
|
+
The following timeline shows how the hooks interact during a typical workflow run:
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
1. session_start / session_tree
|
|
362
|
+
└─ loadWorkflows() → reconstructState() → updateStatus()
|
|
363
|
+
|
|
364
|
+
2. [Each agent turn]
|
|
365
|
+
├─ before_agent_start
|
|
366
|
+
│ └─ handleBeforeAgentStart() → injects workflow:context message
|
|
367
|
+
├─ [agent runs, may call tools]
|
|
368
|
+
│ └─ tool_call (per tool)
|
|
369
|
+
│ └─ handleToolCall() → may block with reason
|
|
370
|
+
└─ agent_end
|
|
371
|
+
└─ handleAgentEnd() → HookStateMutation
|
|
372
|
+
├─ Case A: DONE → workflow:complete message, unload
|
|
373
|
+
├─ Case B: still active → workflow:countdown, 3s auto-continue
|
|
374
|
+
└─ Case C: no-op
|
|
375
|
+
|
|
376
|
+
3. turn_end
|
|
377
|
+
└─ updateStatus() → refreshes status bar with current phase
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Each hook reads state immutably (except `handleAgentEnd`, which may set `completionNotified`). All state mutations flow back through `index.ts` via the `HookStateMutation` return value — hooks never call `setState` directly.
|