pi-crew 0.9.66 → 0.9.67
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 +61 -0
- package/README.md +2 -3
- package/agents/executor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/dist/index.mjs +4441 -4091
- package/package.json +7 -4
- package/scripts/analyze-run.mjs +1333 -0
- package/scripts/resource-sampler.mjs +482 -0
- package/skills/real-test-pi-crew/SKILL.md +10 -9
- package/src/config/role-tools.ts +2 -2
- package/src/extension/knowledge-injection.ts +17 -0
- package/src/extension/pi-api.ts +0 -16
- package/src/extension/team-tool/api/agent-control.ts +358 -0
- package/src/extension/team-tool/api/handler-context.ts +57 -0
- package/src/extension/team-tool/api/heartbeat.ts +75 -0
- package/src/extension/team-tool/api/mailbox.ts +242 -0
- package/src/extension/team-tool/api/plan-approval.ts +190 -0
- package/src/extension/team-tool/api/read.ts +443 -0
- package/src/extension/team-tool/api/task-claims.ts +207 -0
- package/src/extension/team-tool/api.ts +56 -1301
- package/src/extension/team-tool/cancel.ts +15 -4
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +4 -4
- package/src/extension/team-tool/lifecycle-actions.ts +5 -5
- package/src/extension/team-tool/parallel-dispatch.ts +2 -2
- package/src/extension/team-tool/respond.ts +11 -3
- package/src/extension/team-tool/run-intent.ts +357 -0
- package/src/extension/team-tool/run.ts +8 -285
- package/src/extension/team-tool/status.ts +56 -21
- package/src/extension/team-tool.ts +10 -8
- package/src/prompt/scratchpad-lifecycle.ts +80 -5
- package/src/runtime/child-pi/child-pi-spawn.ts +13 -7
- package/src/runtime/child-pi/child-pi.ts +22 -144
- package/src/runtime/child-pi/mock-fixtures.ts +171 -0
- package/src/runtime/crew-agent-records.ts +18 -1
- package/src/runtime/scratchpad/README.md +6 -0
- package/src/runtime/scratchpad/guest.ts +54 -5
- package/src/runtime/scratchpad/snapshot-hmac.ts +7 -1
- package/src/runtime/supervisor-contact.ts +0 -16
- package/src/runtime/task-runner/child-executor.ts +1 -0
- package/src/runtime/task-runner/state-helpers.ts +9 -1
- package/src/runtime/team-runner.ts +39 -2
- package/src/state/contracts.ts +3 -0
- package/src/state/event-log/event-log.ts +19 -27
- package/src/state/gitignore-manager.ts +61 -7
- package/src/utils/glob-match.ts +29 -0
- package/types/dwf.d.ts +1 -1
- package/src/types/new-api-types.ts +0 -35
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracted `api` operation handlers — live-agent control group (H3 phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `src/extension/team-tool/api.ts` on 2026-08-10. Behaviour is
|
|
5
|
+
* byte-identical to the inline `if` blocks they replace.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readCrewAgents } from "../../../runtime/crew-agent-records.ts";
|
|
9
|
+
import { appendLiveAgentControlRequest } from "../../../runtime/live-session/live-agent-control.ts";
|
|
10
|
+
import {
|
|
11
|
+
followUpLiveAgent,
|
|
12
|
+
getLiveAgent,
|
|
13
|
+
listActiveLiveAgents,
|
|
14
|
+
resumeLiveAgent,
|
|
15
|
+
steerLiveAgent,
|
|
16
|
+
stopLiveAgent,
|
|
17
|
+
} from "../../../runtime/live-session/live-agent-manager.ts";
|
|
18
|
+
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../../runtime/live-session/live-control-realtime.ts";
|
|
19
|
+
import {
|
|
20
|
+
appendFollowUpMessage,
|
|
21
|
+
appendFollowUpMessageAsync,
|
|
22
|
+
appendSteeringMessage,
|
|
23
|
+
appendSteeringMessageAsync,
|
|
24
|
+
} from "../../../state/coordination/mailbox.ts";
|
|
25
|
+
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
26
|
+
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
27
|
+
|
|
28
|
+
export const handleNudgeAgent: ApiOperationHandler = (hctx) => {
|
|
29
|
+
const { cfg, loaded, result, paramRequired, ctx } = hctx;
|
|
30
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
31
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
32
|
+
if (!agent)
|
|
33
|
+
return result(
|
|
34
|
+
paramRequired(
|
|
35
|
+
"API nudge-agent",
|
|
36
|
+
"config.agentId matching an agent id or task id",
|
|
37
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'nudge-agent', agentId: 'agent-1' } }",
|
|
38
|
+
),
|
|
39
|
+
{
|
|
40
|
+
action: "api",
|
|
41
|
+
status: "error",
|
|
42
|
+
runId: loaded.manifest.runId,
|
|
43
|
+
},
|
|
44
|
+
true,
|
|
45
|
+
);
|
|
46
|
+
const messageText =
|
|
47
|
+
typeof cfg.message === "string" && cfg.message.trim()
|
|
48
|
+
? cfg.message.trim()
|
|
49
|
+
: "Please report your current status, blocker, or smallest next step.";
|
|
50
|
+
const message = appendSteeringMessage(loaded.manifest, {
|
|
51
|
+
taskId: agent.taskId,
|
|
52
|
+
to: agent.taskId,
|
|
53
|
+
body: messageText,
|
|
54
|
+
priority: "normal",
|
|
55
|
+
data: { source: "nudge-agent" },
|
|
56
|
+
});
|
|
57
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
58
|
+
type: "agent.nudged",
|
|
59
|
+
runId: loaded.manifest.runId,
|
|
60
|
+
taskId: agent.taskId,
|
|
61
|
+
message: messageText,
|
|
62
|
+
data: { agentId: agent.id, mailboxMessageId: message.id },
|
|
63
|
+
});
|
|
64
|
+
ctx.events?.emit?.("crew.mailbox.message", {
|
|
65
|
+
runId: loaded.manifest.runId,
|
|
66
|
+
id: message.id,
|
|
67
|
+
direction: message.direction,
|
|
68
|
+
from: message.from,
|
|
69
|
+
to: message.to,
|
|
70
|
+
taskId: message.taskId,
|
|
71
|
+
source: "nudge-agent",
|
|
72
|
+
});
|
|
73
|
+
return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), {
|
|
74
|
+
action: "api",
|
|
75
|
+
status: "ok",
|
|
76
|
+
runId: loaded.manifest.runId,
|
|
77
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const handleListLiveAgents: ApiOperationHandler = (hctx) => {
|
|
82
|
+
const { loaded, result } = hctx;
|
|
83
|
+
return result(
|
|
84
|
+
JSON.stringify(
|
|
85
|
+
listActiveLiveAgents().filter((agent) => agent.runId === loaded.manifest.runId),
|
|
86
|
+
null,
|
|
87
|
+
2,
|
|
88
|
+
),
|
|
89
|
+
{
|
|
90
|
+
action: "api",
|
|
91
|
+
status: "ok",
|
|
92
|
+
runId: loaded.manifest.runId,
|
|
93
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handles steer-agent / follow-up-agent / stop-agent / resume-agent /
|
|
100
|
+
* interrupt-agent — the live-agent control cluster (shared body in the
|
|
101
|
+
* original handleApi; interrupt falls through to stop).
|
|
102
|
+
*/
|
|
103
|
+
export const handleLiveAgentControl: ApiOperationHandler = async (hctx) => {
|
|
104
|
+
const { cfg, loaded, result, paramRequired, ctx } = hctx;
|
|
105
|
+
const operation = typeof cfg.operation === "string" ? cfg.operation : "";
|
|
106
|
+
const agentId = typeof cfg.agentId === "string" ? cfg.agentId : undefined;
|
|
107
|
+
if (!agentId)
|
|
108
|
+
return result(
|
|
109
|
+
paramRequired(
|
|
110
|
+
`API ${operation}`,
|
|
111
|
+
"config.agentId",
|
|
112
|
+
`{ action: 'api', runId: 'team_...', config: { operation: '${operation}', agentId: 'agent-1' } }`,
|
|
113
|
+
),
|
|
114
|
+
{
|
|
115
|
+
action: "api",
|
|
116
|
+
status: "error",
|
|
117
|
+
runId: loaded.manifest.runId,
|
|
118
|
+
},
|
|
119
|
+
true,
|
|
120
|
+
);
|
|
121
|
+
const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
|
|
122
|
+
const prompt = typeof cfg.prompt === "string" && cfg.prompt.trim() ? cfg.prompt.trim() : message;
|
|
123
|
+
try {
|
|
124
|
+
const live = getLiveAgent(agentId);
|
|
125
|
+
if (live && live.runId !== loaded.manifest.runId)
|
|
126
|
+
return result(
|
|
127
|
+
`Live agent '${agentId}' does not belong to run ${loaded.manifest.runId}.`,
|
|
128
|
+
{
|
|
129
|
+
action: "api",
|
|
130
|
+
status: "error",
|
|
131
|
+
runId: loaded.manifest.runId,
|
|
132
|
+
},
|
|
133
|
+
true,
|
|
134
|
+
);
|
|
135
|
+
if (live && live.workspaceId !== loaded.manifest.cwd)
|
|
136
|
+
return result(
|
|
137
|
+
`Live agent '${agentId}' does not belong to workspace ${loaded.manifest.cwd}.`,
|
|
138
|
+
{
|
|
139
|
+
action: "api",
|
|
140
|
+
status: "error",
|
|
141
|
+
runId: loaded.manifest.runId,
|
|
142
|
+
},
|
|
143
|
+
true,
|
|
144
|
+
);
|
|
145
|
+
if (!live && (operation === "steer-agent" || operation === "follow-up-agent"))
|
|
146
|
+
throw new Error(`Live agent '${agentId}' not found.`);
|
|
147
|
+
const liveTaskId = live?.taskId;
|
|
148
|
+
if ((operation === "steer-agent" || operation === "follow-up-agent") && !liveTaskId)
|
|
149
|
+
throw new Error(`Live agent '${agentId}' not found.`);
|
|
150
|
+
const targetTaskId = liveTaskId ?? agentId;
|
|
151
|
+
if (operation === "steer-agent") {
|
|
152
|
+
const text = message ?? "Please report current status and wrap up if possible.";
|
|
153
|
+
const realtime = await steerLiveAgent(agentId, text);
|
|
154
|
+
const mailboxMessage = await appendSteeringMessageAsync(loaded.manifest, {
|
|
155
|
+
taskId: targetTaskId,
|
|
156
|
+
body: text,
|
|
157
|
+
status: "delivered",
|
|
158
|
+
data: { source: "steer-agent", realtime: true },
|
|
159
|
+
});
|
|
160
|
+
return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), {
|
|
161
|
+
action: "api",
|
|
162
|
+
status: "ok",
|
|
163
|
+
runId: loaded.manifest.runId,
|
|
164
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (operation === "follow-up-agent") {
|
|
168
|
+
if (!prompt)
|
|
169
|
+
return result(
|
|
170
|
+
paramRequired(
|
|
171
|
+
"API follow-up-agent",
|
|
172
|
+
"config.prompt or config.message",
|
|
173
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'follow-up-agent', agentId: 'agent-1', prompt: '<next step>' } }",
|
|
174
|
+
),
|
|
175
|
+
{
|
|
176
|
+
action: "api",
|
|
177
|
+
status: "error",
|
|
178
|
+
runId: loaded.manifest.runId,
|
|
179
|
+
},
|
|
180
|
+
true,
|
|
181
|
+
);
|
|
182
|
+
const realtime = await followUpLiveAgent(agentId, prompt);
|
|
183
|
+
const mailboxMessage = await appendFollowUpMessageAsync(loaded.manifest, {
|
|
184
|
+
taskId: targetTaskId,
|
|
185
|
+
body: prompt,
|
|
186
|
+
status: "delivered",
|
|
187
|
+
data: { source: "follow-up-agent", realtime: true },
|
|
188
|
+
});
|
|
189
|
+
return result(JSON.stringify({ realtime, mailboxMessage }, null, 2), {
|
|
190
|
+
action: "api",
|
|
191
|
+
status: "ok",
|
|
192
|
+
runId: loaded.manifest.runId,
|
|
193
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (operation === "resume-agent") {
|
|
197
|
+
if (!prompt)
|
|
198
|
+
return result(
|
|
199
|
+
paramRequired(
|
|
200
|
+
"API resume-agent",
|
|
201
|
+
"config.prompt or config.message",
|
|
202
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'resume-agent', agentId: 'agent-1', prompt: '<resume instruction>' } }",
|
|
203
|
+
),
|
|
204
|
+
{
|
|
205
|
+
action: "api",
|
|
206
|
+
status: "error",
|
|
207
|
+
runId: loaded.manifest.runId,
|
|
208
|
+
},
|
|
209
|
+
true,
|
|
210
|
+
);
|
|
211
|
+
return result(JSON.stringify(await resumeLiveAgent(agentId, prompt), null, 2), {
|
|
212
|
+
action: "api",
|
|
213
|
+
status: "ok",
|
|
214
|
+
runId: loaded.manifest.runId,
|
|
215
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return result(JSON.stringify(await stopLiveAgent(agentId), null, 2), {
|
|
219
|
+
action: "api",
|
|
220
|
+
status: "ok",
|
|
221
|
+
runId: loaded.manifest.runId,
|
|
222
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
223
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
226
|
+
if (!agent) {
|
|
227
|
+
const err = error instanceof Error ? error.message : String(error);
|
|
228
|
+
return result(
|
|
229
|
+
err,
|
|
230
|
+
{
|
|
231
|
+
action: "api",
|
|
232
|
+
status: "error",
|
|
233
|
+
runId: loaded.manifest.runId,
|
|
234
|
+
},
|
|
235
|
+
true,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const task = loaded.tasks.find((item) => item.id === agent.taskId);
|
|
239
|
+
if (!task)
|
|
240
|
+
return result(
|
|
241
|
+
`API ${operation} agent '${agentId}' does not match a run task.`,
|
|
242
|
+
{
|
|
243
|
+
action: "api",
|
|
244
|
+
status: "error",
|
|
245
|
+
runId: loaded.manifest.runId,
|
|
246
|
+
},
|
|
247
|
+
true,
|
|
248
|
+
);
|
|
249
|
+
if (operation === "resume-agent" && !prompt)
|
|
250
|
+
return result(
|
|
251
|
+
paramRequired(
|
|
252
|
+
"API resume-agent",
|
|
253
|
+
"config.prompt or config.message",
|
|
254
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'resume-agent', agentId: 'agent-1', prompt: '<resume instruction>' } }",
|
|
255
|
+
),
|
|
256
|
+
{
|
|
257
|
+
action: "api",
|
|
258
|
+
status: "error",
|
|
259
|
+
runId: loaded.manifest.runId,
|
|
260
|
+
},
|
|
261
|
+
true,
|
|
262
|
+
);
|
|
263
|
+
if (operation === "follow-up-agent" && !prompt)
|
|
264
|
+
return result(
|
|
265
|
+
paramRequired(
|
|
266
|
+
"API follow-up-agent",
|
|
267
|
+
"config.prompt or config.message",
|
|
268
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'follow-up-agent', agentId: 'agent-1', prompt: '<next step>' } }",
|
|
269
|
+
),
|
|
270
|
+
{
|
|
271
|
+
action: "api",
|
|
272
|
+
status: "error",
|
|
273
|
+
runId: loaded.manifest.runId,
|
|
274
|
+
},
|
|
275
|
+
true,
|
|
276
|
+
);
|
|
277
|
+
try {
|
|
278
|
+
const request = appendLiveAgentControlRequest(loaded.manifest, {
|
|
279
|
+
taskId: task.id,
|
|
280
|
+
agentId: agent.id,
|
|
281
|
+
operation:
|
|
282
|
+
operation === "resume-agent"
|
|
283
|
+
? "resume"
|
|
284
|
+
: operation === "follow-up-agent"
|
|
285
|
+
? "follow-up"
|
|
286
|
+
: operation === "steer-agent"
|
|
287
|
+
? "steer"
|
|
288
|
+
: "stop",
|
|
289
|
+
message: operation === "resume-agent" || operation === "follow-up-agent" ? prompt : message,
|
|
290
|
+
});
|
|
291
|
+
const mailboxMessage =
|
|
292
|
+
operation === "steer-agent"
|
|
293
|
+
? appendSteeringMessage(loaded.manifest, {
|
|
294
|
+
taskId: task.id,
|
|
295
|
+
to: agent.id,
|
|
296
|
+
body: message ?? "Please report current status and wrap up if possible.",
|
|
297
|
+
status: "delivered",
|
|
298
|
+
data: {
|
|
299
|
+
source: "steer-agent",
|
|
300
|
+
liveControlRequestId: request.id,
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
: operation === "follow-up-agent" && prompt
|
|
304
|
+
? appendFollowUpMessage(loaded.manifest, {
|
|
305
|
+
taskId: task.id,
|
|
306
|
+
to: agent.id,
|
|
307
|
+
body: prompt,
|
|
308
|
+
status: "delivered",
|
|
309
|
+
data: {
|
|
310
|
+
source: "follow-up-agent",
|
|
311
|
+
liveControlRequestId: request.id,
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
: undefined;
|
|
315
|
+
publishLiveControlRealtime(request);
|
|
316
|
+
ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
|
|
317
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
318
|
+
type: "agent.control.queued",
|
|
319
|
+
runId: loaded.manifest.runId,
|
|
320
|
+
taskId: agent.taskId,
|
|
321
|
+
message: `Queued ${request.operation} control request for live agent.`,
|
|
322
|
+
data: {
|
|
323
|
+
request,
|
|
324
|
+
mailboxMessageId: mailboxMessage?.id,
|
|
325
|
+
realtime: true,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
return result(JSON.stringify({ queued: true, request, mailboxMessage }, null, 2), {
|
|
329
|
+
action: "api",
|
|
330
|
+
status: "ok",
|
|
331
|
+
runId: loaded.manifest.runId,
|
|
332
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
333
|
+
});
|
|
334
|
+
} catch (queueError) {
|
|
335
|
+
const message = queueError instanceof Error ? queueError.message : String(queueError);
|
|
336
|
+
return result(
|
|
337
|
+
message,
|
|
338
|
+
{
|
|
339
|
+
action: "api",
|
|
340
|
+
status: "error",
|
|
341
|
+
runId: loaded.manifest.runId,
|
|
342
|
+
},
|
|
343
|
+
true,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/** Dispatcher map for the live-agent control group. Consumed by handleApi. */
|
|
350
|
+
export const AGENT_CONTROL_OPERATIONS: Record<string, ApiOperationHandler> = {
|
|
351
|
+
"nudge-agent": handleNudgeAgent,
|
|
352
|
+
"list-live-agents": handleListLiveAgents,
|
|
353
|
+
"steer-agent": handleLiveAgentControl,
|
|
354
|
+
"follow-up-agent": handleLiveAgentControl,
|
|
355
|
+
"stop-agent": handleLiveAgentControl,
|
|
356
|
+
"resume-agent": handleLiveAgentControl,
|
|
357
|
+
"interrupt-agent": handleLiveAgentControl,
|
|
358
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared context for extracted `api` operation handlers (H3 phase 1+).
|
|
3
|
+
*
|
|
4
|
+
* The `handleApi` function in `src/extension/team-tool/api.ts` was 1222 lines
|
|
5
|
+
* with 35+ `if (operation === "…")` branches. This file defines the shared
|
|
6
|
+
* bundle of closure dependencies that each extracted handler receives, so
|
|
7
|
+
* sibling modules under `src/extension/team-tool/api/<group>.ts` can be
|
|
8
|
+
* pure-function-shaped without re-establishing the closure.
|
|
9
|
+
*
|
|
10
|
+
* Extraction pattern mirrors `merge-gate.ts` (improvement-plan-2026-08-09
|
|
11
|
+
* phase 5): the extracted module imports its own state/coordination helpers
|
|
12
|
+
* directly; only the request-scoped values (config, loaded run, params,
|
|
13
|
+
* extension context) are passed via this context.
|
|
14
|
+
*
|
|
15
|
+
* Phase 2 (2026-08-10): added `params` + `ctx` and made handlers async-capable
|
|
16
|
+
* (several operations await live-agent or runtime resolution).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { TeamToolParamsValue } from "../../../schema/team-tool-schema.ts";
|
|
20
|
+
import type { TeamRunManifest, TeamTaskState } from "../../../state/types.ts";
|
|
21
|
+
import type { PiTeamsToolResult } from "../../tool-result.ts";
|
|
22
|
+
import type { result as resultFn, TeamContext } from "../context.ts";
|
|
23
|
+
import type { paramRequired as paramRequiredFn } from "../param-error.ts";
|
|
24
|
+
|
|
25
|
+
/** The config record produced by `configRecord(params.config)` in handleApi. */
|
|
26
|
+
export type ApiConfig = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
/** The loaded run snapshot returned by `loadRunManifestById`. */
|
|
29
|
+
export interface ApiLoadedRun {
|
|
30
|
+
manifest: TeamRunManifest;
|
|
31
|
+
tasks: TeamTaskState[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Bundle of closure-scoped dependencies that handleApi establishes once and
|
|
36
|
+
* passes to each extracted handler. Adding a field here is cheaper than
|
|
37
|
+
* growing the closure of a 1200-line function.
|
|
38
|
+
*/
|
|
39
|
+
export interface ApiHandlerContext {
|
|
40
|
+
cfg: ApiConfig;
|
|
41
|
+
loaded: ApiLoadedRun;
|
|
42
|
+
result: typeof resultFn;
|
|
43
|
+
paramRequired: typeof paramRequiredFn;
|
|
44
|
+
/** The raw tool params (runId, force, etc.) the caller passed. */
|
|
45
|
+
params: TeamToolParamsValue;
|
|
46
|
+
/** The extension TeamContext (cwd, events, metricRegistry, config…). */
|
|
47
|
+
ctx: TeamContext;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Type alias for an extracted operation handler. Async-capable. */
|
|
51
|
+
export type ApiOperationHandler = (ctx: ApiHandlerContext) => PiTeamsToolResult | Promise<PiTeamsToolResult>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handler for operations that run BEFORE the runId guard (no loaded run).
|
|
55
|
+
* Currently: metrics-snapshot, inventory.
|
|
56
|
+
*/
|
|
57
|
+
export type ApiPreHandler = (ctx: Omit<ApiHandlerContext, "loaded">) => PiTeamsToolResult | Promise<PiTeamsToolResult>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracted `api` operation handlers — heartbeat group (H3 phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `src/extension/team-tool/api.ts` on 2026-08-10. Behaviour is
|
|
5
|
+
* byte-identical to the inline `if` blocks they replace.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { touchWorkerHeartbeat } from "../../../runtime/heartbeat/worker-heartbeat.ts";
|
|
9
|
+
import { withRunLockSync } from "../../../state/coordination/locks.ts";
|
|
10
|
+
import { appendEvent } from "../../../state/event-log/event-log.ts";
|
|
11
|
+
import { saveRunTasks } from "../../../state/stores/state-store.ts";
|
|
12
|
+
import type { ApiOperationHandler } from "./handler-context.ts";
|
|
13
|
+
|
|
14
|
+
export const handleWriteHeartbeat: ApiOperationHandler = (hctx) => {
|
|
15
|
+
const { cfg, loaded, result, paramRequired } = hctx;
|
|
16
|
+
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
17
|
+
const task = loaded.tasks.find((item) => item.id === taskId || item.stepId === taskId);
|
|
18
|
+
if (!task)
|
|
19
|
+
return result(
|
|
20
|
+
paramRequired(
|
|
21
|
+
"API write-heartbeat",
|
|
22
|
+
"config.taskId matching a task id or step id",
|
|
23
|
+
"{ action: 'api', runId: 'team_...', config: { operation: 'write-heartbeat', taskId: '01_01-agent' } }",
|
|
24
|
+
),
|
|
25
|
+
{
|
|
26
|
+
action: "api",
|
|
27
|
+
status: "error",
|
|
28
|
+
runId: loaded.manifest.runId,
|
|
29
|
+
},
|
|
30
|
+
true,
|
|
31
|
+
);
|
|
32
|
+
try {
|
|
33
|
+
return withRunLockSync(loaded.manifest, () => {
|
|
34
|
+
const heartbeat = touchWorkerHeartbeat(
|
|
35
|
+
task.heartbeat ?? {
|
|
36
|
+
workerId: task.id,
|
|
37
|
+
lastSeenAt: new Date().toISOString(),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
alive: typeof cfg.alive === "boolean" ? cfg.alive : undefined,
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
const tasks = loaded.tasks.map((item) => (item.id === task.id ? { ...item, heartbeat } : item));
|
|
44
|
+
saveRunTasks(loaded.manifest, tasks);
|
|
45
|
+
appendEvent(loaded.manifest.eventsPath, {
|
|
46
|
+
type: "worker.heartbeat",
|
|
47
|
+
runId: loaded.manifest.runId,
|
|
48
|
+
taskId: task.id,
|
|
49
|
+
data: { ...heartbeat },
|
|
50
|
+
});
|
|
51
|
+
return result(JSON.stringify(heartbeat, null, 2), {
|
|
52
|
+
action: "api",
|
|
53
|
+
status: "ok",
|
|
54
|
+
runId: loaded.manifest.runId,
|
|
55
|
+
artifactsRoot: loaded.manifest.artifactsRoot,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
return result(
|
|
61
|
+
message,
|
|
62
|
+
{
|
|
63
|
+
action: "api",
|
|
64
|
+
status: "error",
|
|
65
|
+
runId: loaded.manifest.runId,
|
|
66
|
+
},
|
|
67
|
+
true,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Dispatcher map for the heartbeat group. Consumed by handleApi. */
|
|
73
|
+
export const HEARTBEAT_OPERATIONS: Record<string, ApiOperationHandler> = {
|
|
74
|
+
"write-heartbeat": handleWriteHeartbeat,
|
|
75
|
+
};
|