@sgtbeatdown/pi-tasks 0.1.0 → 0.1.1
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/README.md +48 -468
- package/package.json +4 -3
- package/skills/task-context-summary/SKILL.md +34 -0
- package/skills/wolfpack-pi-task-delegation/SKILL.md +29 -60
- package/src/extension.ts +165 -251
- package/src/gateway-client.ts +385 -0
- package/src/metrics-cli.ts +20 -0
- package/src/metrics.ts +288 -0
- package/src/task-artifacts.ts +153 -0
- package/src/task-board.ts +365 -0
- package/src/task-inbox.ts +141 -0
- package/src/auto-notify.ts +0 -24
- package/src/protocol.ts +0 -73
- package/src/store.ts +0 -494
- package/src/stores/filesystem.ts +0 -40
- package/src/task-communication.ts +0 -68
- package/src/transports/wolfpack.ts +0 -29
- package/src/types.ts +0 -114
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { readTaskStoreArtifacts } from "./task-artifacts";
|
|
2
|
+
import type { TaskStoreArtifactRecord, TaskStoreDiagnostic } from "./task-artifacts";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_GROUPS = 10;
|
|
5
|
+
const DEFAULT_MAX_TASKS_PER_GROUP = 20;
|
|
6
|
+
const DEFAULT_MAX_UNGROUPED_TASKS = 20;
|
|
7
|
+
const TERMINAL_STATUSES: ReadonlySet<string> = new Set(["completed", "failed", "cancelled", "timed_out", "rejected"]);
|
|
8
|
+
|
|
9
|
+
export interface TaskBoardOptions {
|
|
10
|
+
readonly maxGroups?: number;
|
|
11
|
+
readonly maxTasksPerGroup?: number;
|
|
12
|
+
readonly maxUngroupedTasks?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TaskBoardTask {
|
|
16
|
+
readonly taskId: string;
|
|
17
|
+
readonly status: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TaskBoardGroup {
|
|
21
|
+
readonly phaseId: string;
|
|
22
|
+
readonly issueId: string;
|
|
23
|
+
readonly rootCause: string;
|
|
24
|
+
readonly taskCount: number;
|
|
25
|
+
readonly tasks: readonly TaskBoardTask[];
|
|
26
|
+
readonly omittedTaskCount: number;
|
|
27
|
+
readonly statusCounts: Readonly<Record<string, number>>;
|
|
28
|
+
readonly hasActiveTasks: boolean;
|
|
29
|
+
readonly latestUpdatedAt: string | null;
|
|
30
|
+
readonly latestCompletedAt: string | null;
|
|
31
|
+
readonly verification: {
|
|
32
|
+
readonly entryCount: number;
|
|
33
|
+
readonly statusCounts: Readonly<Record<string, number>>;
|
|
34
|
+
};
|
|
35
|
+
readonly blockerCount: number;
|
|
36
|
+
readonly riskCount: number;
|
|
37
|
+
readonly loopIndicators: {
|
|
38
|
+
readonly issueTaskCount: number;
|
|
39
|
+
readonly rootCauseTaskCount: number;
|
|
40
|
+
readonly rootCauseIssueCount: number;
|
|
41
|
+
readonly repeatedIssue: boolean;
|
|
42
|
+
readonly sharedRootCause: boolean;
|
|
43
|
+
readonly possiblePingPong: boolean;
|
|
44
|
+
readonly splitWorkCandidate: boolean;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TaskBoardReport {
|
|
49
|
+
readonly schemaVersion: 1;
|
|
50
|
+
readonly tasksRoot: string;
|
|
51
|
+
readonly totalTaskRecords: number;
|
|
52
|
+
readonly groupedTaskCount: number;
|
|
53
|
+
readonly ungroupedTaskCount: number;
|
|
54
|
+
readonly totalGroupCount: number;
|
|
55
|
+
readonly groups: readonly TaskBoardGroup[];
|
|
56
|
+
readonly omittedGroupCount: number;
|
|
57
|
+
readonly ungrouped: {
|
|
58
|
+
readonly taskCount: number;
|
|
59
|
+
readonly tasks: readonly TaskBoardTask[];
|
|
60
|
+
readonly omittedTaskCount: number;
|
|
61
|
+
readonly statusCounts: Readonly<Record<string, number>>;
|
|
62
|
+
readonly reasonCounts: {
|
|
63
|
+
readonly missingPhaseId: number;
|
|
64
|
+
readonly missingIssueId: number;
|
|
65
|
+
readonly missingRootCause: number;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
readonly diagnostics: readonly TaskStoreDiagnostic[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface BoardTaskSource extends TaskBoardTask {
|
|
72
|
+
readonly phaseId: string | undefined;
|
|
73
|
+
readonly issueId: string | undefined;
|
|
74
|
+
readonly rootCause: string | undefined;
|
|
75
|
+
readonly updatedAt: string | undefined;
|
|
76
|
+
readonly completedAt: string | undefined;
|
|
77
|
+
readonly verificationEntryCount: number;
|
|
78
|
+
readonly verificationStatusCounts: Readonly<Record<string, number>>;
|
|
79
|
+
readonly blockerCount: number;
|
|
80
|
+
readonly riskCount: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface MutableBoardGroup {
|
|
84
|
+
readonly phaseId: string;
|
|
85
|
+
readonly issueId: string;
|
|
86
|
+
readonly rootCause: string;
|
|
87
|
+
readonly tasks: BoardTaskSource[];
|
|
88
|
+
readonly statusCounts: Record<string, number>;
|
|
89
|
+
readonly verificationStatusCounts: Record<string, number>;
|
|
90
|
+
verificationEntryCount: number;
|
|
91
|
+
blockerCount: number;
|
|
92
|
+
riskCount: number;
|
|
93
|
+
latestUpdatedAt: string | undefined;
|
|
94
|
+
latestCompletedAt: string | undefined;
|
|
95
|
+
hasActiveTasks: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function buildTaskBoard(tasksRoot: string, options: TaskBoardOptions = {}): Promise<TaskBoardReport> {
|
|
99
|
+
const maxGroups = normalizeLimit(options.maxGroups, DEFAULT_MAX_GROUPS, "maxGroups");
|
|
100
|
+
const maxTasksPerGroup = normalizeLimit(options.maxTasksPerGroup, DEFAULT_MAX_TASKS_PER_GROUP, "maxTasksPerGroup");
|
|
101
|
+
const maxUngroupedTasks = normalizeLimit(options.maxUngroupedTasks, DEFAULT_MAX_UNGROUPED_TASKS, "maxUngroupedTasks");
|
|
102
|
+
const artifactRead = await readTaskStoreArtifacts(tasksRoot);
|
|
103
|
+
const tasks = artifactRead.records.map(toBoardTaskSource);
|
|
104
|
+
const issueTaskCounts = countStringValues(tasks.map((task) => task.issueId));
|
|
105
|
+
const rootCauseTaskCounts = countStringValues(tasks.map((task) => task.rootCause));
|
|
106
|
+
const rootCauseIssues = collectRootCauseIssues(tasks);
|
|
107
|
+
const groupedTasks = tasks.filter(hasCompleteGrouping);
|
|
108
|
+
const ungroupedTasks = tasks.filter((task) => !hasCompleteGrouping(task));
|
|
109
|
+
const mutableGroups = groupTasks(groupedTasks);
|
|
110
|
+
const groups = mutableGroups
|
|
111
|
+
.map((group) => finalizeGroup(group, issueTaskCounts, rootCauseTaskCounts, rootCauseIssues, maxTasksPerGroup))
|
|
112
|
+
.sort(compareGroups);
|
|
113
|
+
const visibleGroups = groups.slice(0, maxGroups);
|
|
114
|
+
const sortedUngroupedTasks = [...ungroupedTasks].sort(compareTasks);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
schemaVersion: 1,
|
|
118
|
+
tasksRoot,
|
|
119
|
+
totalTaskRecords: tasks.length,
|
|
120
|
+
groupedTaskCount: groupedTasks.length,
|
|
121
|
+
ungroupedTaskCount: ungroupedTasks.length,
|
|
122
|
+
totalGroupCount: groups.length,
|
|
123
|
+
groups: visibleGroups,
|
|
124
|
+
omittedGroupCount: groups.length - visibleGroups.length,
|
|
125
|
+
ungrouped: {
|
|
126
|
+
taskCount: ungroupedTasks.length,
|
|
127
|
+
tasks: sortedUngroupedTasks.slice(0, maxUngroupedTasks).map(compactTask),
|
|
128
|
+
omittedTaskCount: Math.max(0, ungroupedTasks.length - maxUngroupedTasks),
|
|
129
|
+
statusCounts: sortedCounts(countStatuses(ungroupedTasks)),
|
|
130
|
+
reasonCounts: countUngroupedReasons(ungroupedTasks),
|
|
131
|
+
},
|
|
132
|
+
diagnostics: artifactRead.diagnostics,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function toBoardTaskSource(record: TaskStoreArtifactRecord): BoardTaskSource {
|
|
137
|
+
const task = record.task;
|
|
138
|
+
const structuredResult = record.result && isRecord(record.result.result) ? record.result.result : undefined;
|
|
139
|
+
const verification = structuredResult && Array.isArray(structuredResult.verification) ? structuredResult.verification : [];
|
|
140
|
+
const verificationStatusCounts: Record<string, number> = {};
|
|
141
|
+
for (const entry of verification) {
|
|
142
|
+
if (isRecord(entry) && nonEmptyString(entry.status)) {
|
|
143
|
+
increment(verificationStatusCounts, entry.status);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
taskId: nonEmptyString(task.id) ? task.id : record.taskDirectoryName,
|
|
149
|
+
status: nonEmptyString(task.status) ? task.status : "unknown",
|
|
150
|
+
phaseId: metadataValue(task.metadata, "phaseId"),
|
|
151
|
+
issueId: metadataValue(task.metadata, "issueId"),
|
|
152
|
+
rootCause: metadataValue(task.metadata, "rootCause"),
|
|
153
|
+
updatedAt: nonEmptyString(task.updatedAt) ? task.updatedAt : undefined,
|
|
154
|
+
completedAt: latestTimestamp(
|
|
155
|
+
nonEmptyString(task.completedAt) ? task.completedAt : undefined,
|
|
156
|
+
record.result && nonEmptyString(record.result.completedAt) ? record.result.completedAt : undefined,
|
|
157
|
+
),
|
|
158
|
+
verificationEntryCount: verification.length,
|
|
159
|
+
verificationStatusCounts,
|
|
160
|
+
blockerCount: structuredResult && Array.isArray(structuredResult.blockers) ? structuredResult.blockers.length : 0,
|
|
161
|
+
riskCount: structuredResult && Array.isArray(structuredResult.risks) ? structuredResult.risks.length : 0,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function metadataValue(metadata: unknown, field: "phaseId" | "issueId" | "rootCause"): string | undefined {
|
|
166
|
+
return isRecord(metadata) && nonEmptyString(metadata[field]) ? metadata[field] : undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hasCompleteGrouping(task: BoardTaskSource): task is BoardTaskSource & { readonly phaseId: string; readonly issueId: string; readonly rootCause: string } {
|
|
170
|
+
return task.phaseId !== undefined && task.issueId !== undefined && task.rootCause !== undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function groupTasks(tasks: readonly (BoardTaskSource & { readonly phaseId: string; readonly issueId: string; readonly rootCause: string })[]): readonly MutableBoardGroup[] {
|
|
174
|
+
const byPhase = new Map<string, Map<string, Map<string, MutableBoardGroup>>>();
|
|
175
|
+
const groups: MutableBoardGroup[] = [];
|
|
176
|
+
for (const task of tasks) {
|
|
177
|
+
let byIssue = byPhase.get(task.phaseId);
|
|
178
|
+
if (!byIssue) {
|
|
179
|
+
byIssue = new Map();
|
|
180
|
+
byPhase.set(task.phaseId, byIssue);
|
|
181
|
+
}
|
|
182
|
+
let byRootCause = byIssue.get(task.issueId);
|
|
183
|
+
if (!byRootCause) {
|
|
184
|
+
byRootCause = new Map();
|
|
185
|
+
byIssue.set(task.issueId, byRootCause);
|
|
186
|
+
}
|
|
187
|
+
let group = byRootCause.get(task.rootCause);
|
|
188
|
+
if (!group) {
|
|
189
|
+
group = {
|
|
190
|
+
phaseId: task.phaseId,
|
|
191
|
+
issueId: task.issueId,
|
|
192
|
+
rootCause: task.rootCause,
|
|
193
|
+
tasks: [],
|
|
194
|
+
statusCounts: {},
|
|
195
|
+
verificationStatusCounts: {},
|
|
196
|
+
verificationEntryCount: 0,
|
|
197
|
+
blockerCount: 0,
|
|
198
|
+
riskCount: 0,
|
|
199
|
+
latestUpdatedAt: undefined,
|
|
200
|
+
latestCompletedAt: undefined,
|
|
201
|
+
hasActiveTasks: false,
|
|
202
|
+
};
|
|
203
|
+
byRootCause.set(task.rootCause, group);
|
|
204
|
+
groups.push(group);
|
|
205
|
+
}
|
|
206
|
+
addTaskToGroup(group, task);
|
|
207
|
+
}
|
|
208
|
+
return groups;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function addTaskToGroup(group: MutableBoardGroup, task: BoardTaskSource): void {
|
|
212
|
+
group.tasks.push(task);
|
|
213
|
+
increment(group.statusCounts, task.status);
|
|
214
|
+
for (const [status, count] of Object.entries(task.verificationStatusCounts)) {
|
|
215
|
+
group.verificationStatusCounts[status] = (group.verificationStatusCounts[status] ?? 0) + count;
|
|
216
|
+
}
|
|
217
|
+
group.verificationEntryCount += task.verificationEntryCount;
|
|
218
|
+
group.blockerCount += task.blockerCount;
|
|
219
|
+
group.riskCount += task.riskCount;
|
|
220
|
+
group.latestUpdatedAt = latestTimestamp(group.latestUpdatedAt, task.updatedAt);
|
|
221
|
+
group.latestCompletedAt = latestTimestamp(group.latestCompletedAt, task.completedAt);
|
|
222
|
+
group.hasActiveTasks ||= !TERMINAL_STATUSES.has(task.status);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function finalizeGroup(
|
|
226
|
+
group: MutableBoardGroup,
|
|
227
|
+
issueTaskCounts: Readonly<Record<string, number>>,
|
|
228
|
+
rootCauseTaskCounts: Readonly<Record<string, number>>,
|
|
229
|
+
rootCauseIssues: ReadonlyMap<string, ReadonlySet<string>>,
|
|
230
|
+
maxTasksPerGroup: number,
|
|
231
|
+
): TaskBoardGroup {
|
|
232
|
+
const tasks = [...group.tasks].sort(compareTasks);
|
|
233
|
+
const issueTaskCount = issueTaskCounts[group.issueId] ?? group.tasks.length;
|
|
234
|
+
const rootCauseTaskCount = rootCauseTaskCounts[group.rootCause] ?? group.tasks.length;
|
|
235
|
+
const rootCauseIssueCount = rootCauseIssues.get(group.rootCause)?.size ?? 1;
|
|
236
|
+
const repeatedIssue = issueTaskCount > 1;
|
|
237
|
+
const sharedRootCause = rootCauseIssueCount > 1;
|
|
238
|
+
return {
|
|
239
|
+
phaseId: group.phaseId,
|
|
240
|
+
issueId: group.issueId,
|
|
241
|
+
rootCause: group.rootCause,
|
|
242
|
+
taskCount: tasks.length,
|
|
243
|
+
tasks: tasks.slice(0, maxTasksPerGroup).map(compactTask),
|
|
244
|
+
omittedTaskCount: Math.max(0, tasks.length - maxTasksPerGroup),
|
|
245
|
+
statusCounts: sortedCounts(group.statusCounts),
|
|
246
|
+
hasActiveTasks: group.hasActiveTasks,
|
|
247
|
+
latestUpdatedAt: group.latestUpdatedAt ?? null,
|
|
248
|
+
latestCompletedAt: group.latestCompletedAt ?? null,
|
|
249
|
+
verification: {
|
|
250
|
+
entryCount: group.verificationEntryCount,
|
|
251
|
+
statusCounts: sortedCounts(group.verificationStatusCounts),
|
|
252
|
+
},
|
|
253
|
+
blockerCount: group.blockerCount,
|
|
254
|
+
riskCount: group.riskCount,
|
|
255
|
+
loopIndicators: {
|
|
256
|
+
issueTaskCount,
|
|
257
|
+
rootCauseTaskCount,
|
|
258
|
+
rootCauseIssueCount,
|
|
259
|
+
repeatedIssue,
|
|
260
|
+
sharedRootCause,
|
|
261
|
+
possiblePingPong: repeatedIssue,
|
|
262
|
+
splitWorkCandidate: repeatedIssue || sharedRootCause,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function collectRootCauseIssues(tasks: readonly BoardTaskSource[]): ReadonlyMap<string, ReadonlySet<string>> {
|
|
268
|
+
const rootCauseIssues = new Map<string, Set<string>>();
|
|
269
|
+
for (const task of tasks) {
|
|
270
|
+
if (!task.rootCause || !task.issueId) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
let issueIds = rootCauseIssues.get(task.rootCause);
|
|
274
|
+
if (!issueIds) {
|
|
275
|
+
issueIds = new Set();
|
|
276
|
+
rootCauseIssues.set(task.rootCause, issueIds);
|
|
277
|
+
}
|
|
278
|
+
issueIds.add(task.issueId);
|
|
279
|
+
}
|
|
280
|
+
return rootCauseIssues;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function countUngroupedReasons(tasks: readonly BoardTaskSource[]): TaskBoardReport["ungrouped"]["reasonCounts"] {
|
|
284
|
+
let missingPhaseId = 0;
|
|
285
|
+
let missingIssueId = 0;
|
|
286
|
+
let missingRootCause = 0;
|
|
287
|
+
for (const task of tasks) {
|
|
288
|
+
if (!task.phaseId) missingPhaseId += 1;
|
|
289
|
+
if (!task.issueId) missingIssueId += 1;
|
|
290
|
+
if (!task.rootCause) missingRootCause += 1;
|
|
291
|
+
}
|
|
292
|
+
return { missingPhaseId, missingIssueId, missingRootCause };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function countStatuses(tasks: readonly BoardTaskSource[]): Record<string, number> {
|
|
296
|
+
const counts: Record<string, number> = {};
|
|
297
|
+
for (const task of tasks) {
|
|
298
|
+
increment(counts, task.status);
|
|
299
|
+
}
|
|
300
|
+
return counts;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function countStringValues(values: readonly (string | undefined)[]): Record<string, number> {
|
|
304
|
+
const counts: Record<string, number> = {};
|
|
305
|
+
for (const value of values) {
|
|
306
|
+
if (value) increment(counts, value);
|
|
307
|
+
}
|
|
308
|
+
return counts;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function compactTask(task: BoardTaskSource): TaskBoardTask {
|
|
312
|
+
return { taskId: task.taskId, status: task.status };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function compareGroups(left: TaskBoardGroup, right: TaskBoardGroup): number {
|
|
316
|
+
if (left.hasActiveTasks !== right.hasActiveTasks) {
|
|
317
|
+
return left.hasActiveTasks ? -1 : 1;
|
|
318
|
+
}
|
|
319
|
+
const leftLatest = latestTimestamp(left.latestUpdatedAt ?? undefined, left.latestCompletedAt ?? undefined) ?? "";
|
|
320
|
+
const rightLatest = latestTimestamp(right.latestUpdatedAt ?? undefined, right.latestCompletedAt ?? undefined) ?? "";
|
|
321
|
+
return (
|
|
322
|
+
compareStrings(rightLatest, leftLatest) ||
|
|
323
|
+
compareStrings(left.issueId, right.issueId) ||
|
|
324
|
+
compareStrings(left.rootCause, right.rootCause) ||
|
|
325
|
+
compareStrings(left.phaseId, right.phaseId)
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function compareTasks(left: BoardTaskSource, right: BoardTaskSource): number {
|
|
330
|
+
return compareStrings(left.taskId, right.taskId);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function latestTimestamp(left: string | undefined, right: string | undefined): string | undefined {
|
|
334
|
+
if (!left) return right;
|
|
335
|
+
if (!right) return left;
|
|
336
|
+
return compareStrings(left, right) >= 0 ? left : right;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function sortedCounts(counts: Readonly<Record<string, number>>): Record<string, number> {
|
|
340
|
+
return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => compareStrings(left, right)));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function normalizeLimit(value: number | undefined, fallback: number, name: string): number {
|
|
344
|
+
const limit = value ?? fallback;
|
|
345
|
+
if (!Number.isInteger(limit) || limit < 0) {
|
|
346
|
+
throw new RangeError(`${name} must be a non-negative integer`);
|
|
347
|
+
}
|
|
348
|
+
return limit;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function increment(counts: Record<string, number>, key: string): void {
|
|
352
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function compareStrings(left: string, right: string): number {
|
|
356
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function nonEmptyString(value: unknown): value is string {
|
|
360
|
+
return typeof value === "string" && value.length > 0;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
364
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
365
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { TaskAssignment, TaskEvent, TaskStatus, WolfpackGatewayClient } from "./gateway-client";
|
|
2
|
+
|
|
3
|
+
const TASK_EVENT_CUSTOM_TYPE = "wolfpack-task-event";
|
|
4
|
+
const TASK_CURSOR_CUSTOM_TYPE = "wolfpack-task-cursor";
|
|
5
|
+
|
|
6
|
+
interface InboxContext {
|
|
7
|
+
readonly isIdle: () => boolean;
|
|
8
|
+
readonly hasPendingMessages: () => boolean;
|
|
9
|
+
readonly sessionManager: { readonly buildContextEntries: () => readonly unknown[] };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface InboxPi {
|
|
13
|
+
sendMessage(message: { readonly customType: string; readonly content: string; readonly display: boolean; readonly details: TaskEventDetails }, options: { readonly triggerTurn: true }): void;
|
|
14
|
+
appendEntry(customType: string, data: { readonly cursor: string }): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TaskEventDetails {
|
|
18
|
+
readonly taskId: string;
|
|
19
|
+
readonly eventId: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface InboxClient {
|
|
23
|
+
inbox(cursor: string, includeAcknowledged: boolean): Promise<{ readonly events: readonly TaskEvent[]; readonly nextCursor: string; readonly hasMore: boolean }>;
|
|
24
|
+
status(taskId: string): Promise<TaskStatus>;
|
|
25
|
+
delivered(taskId: string, eventId: string): Promise<unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function deliverTaskInbox(pi: InboxPi, client: InboxClient, ctx: InboxContext, initialCursor: string): Promise<string> {
|
|
29
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return initialCursor;
|
|
30
|
+
const entries = ctx.sessionManager.buildContextEntries();
|
|
31
|
+
const incorporated = incorporatedEvents(entries);
|
|
32
|
+
const includeAcknowledged = initialCursor === "0" && !hasPersistedCursor(entries);
|
|
33
|
+
let cursor = initialCursor;
|
|
34
|
+
for (;;) {
|
|
35
|
+
const page = await client.inbox(cursor, includeAcknowledged);
|
|
36
|
+
for (const event of page.events) {
|
|
37
|
+
if (!modelRelevant(event)) continue;
|
|
38
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return cursor;
|
|
39
|
+
const eventKey = key(event.taskId, event.id);
|
|
40
|
+
const snapshot = await client.status(event.taskId);
|
|
41
|
+
if (!ctx.isIdle() || ctx.hasPendingMessages()) return cursor;
|
|
42
|
+
if (incorporated.has(eventKey)) {
|
|
43
|
+
if (event.destination.sessionId === snapshot.task.target.sessionId) await client.delivered(event.taskId, event.id);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (terminalHistoryAcknowledged(snapshot)) continue;
|
|
47
|
+
pi.sendMessage({
|
|
48
|
+
customType: TASK_EVENT_CUSTOM_TYPE,
|
|
49
|
+
content: renderTaskEvent(event, snapshot),
|
|
50
|
+
display: true,
|
|
51
|
+
details: { taskId: event.taskId, eventId: event.id },
|
|
52
|
+
}, { triggerTurn: true });
|
|
53
|
+
if (!incorporatedEvents(ctx.sessionManager.buildContextEntries()).has(eventKey)) return cursor;
|
|
54
|
+
incorporated.add(eventKey);
|
|
55
|
+
if (event.destination.sessionId === snapshot.task.target.sessionId) await client.delivered(event.taskId, event.id);
|
|
56
|
+
}
|
|
57
|
+
// Inbox cursors are machine-local delivery sequences, never task event sequences.
|
|
58
|
+
cursor = page.nextCursor;
|
|
59
|
+
pi.appendEntry(TASK_CURSOR_CUSTOM_TYPE, { cursor });
|
|
60
|
+
if (!page.hasMore) return cursor;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasPersistedCursor(entries: readonly unknown[]): boolean {
|
|
65
|
+
return entries.some((entry) => isRecord(entry) && entry.type === "custom" && entry.customType === TASK_CURSOR_CUSTOM_TYPE && isRecord(entry.data) && decimal(entry.data.cursor));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function modelRelevant(event: TaskEvent): boolean {
|
|
69
|
+
return event.type === "task.created" || event.type === "task.question" || event.type === "task.answer" || event.type === "task.information" || terminalEvent(event.type) || event.type === "task.cancel_requested";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function terminalHistoryAcknowledged(snapshot: TaskStatus): boolean {
|
|
73
|
+
return ["completed", "failed", "cancelled", "timed_out"].includes(snapshot.status) && snapshot.events.some((event) => event.type === "task.parent_acknowledged");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function restoredInboxCursor(entries: readonly unknown[]): string {
|
|
77
|
+
let cursor = "0";
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== TASK_CURSOR_CUSTOM_TYPE || !isRecord(entry.data) || !decimal(entry.data.cursor)) continue;
|
|
80
|
+
cursor = entry.data.cursor;
|
|
81
|
+
}
|
|
82
|
+
return cursor;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function incorporatedEvents(entries: readonly unknown[]): Set<string> {
|
|
86
|
+
const events = new Set<string>();
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (!isRecord(entry) || entry.type !== "custom_message" || entry.customType !== TASK_EVENT_CUSTOM_TYPE || !isRecord(entry.details) || !nonEmpty(entry.details.taskId) || !nonEmpty(entry.details.eventId)) continue;
|
|
89
|
+
events.add(key(entry.details.taskId, entry.details.eventId));
|
|
90
|
+
}
|
|
91
|
+
return events;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function renderTaskEvent(event: TaskEvent, snapshot: TaskStatus): string {
|
|
95
|
+
const header = `task: \`${event.taskId}\` · event: \`${event.id}\``;
|
|
96
|
+
if (event.type === "task.created") return renderAssignment(header, snapshot.task, snapshot.warnings);
|
|
97
|
+
const message = event.message ? `\n\n${event.message}` : "";
|
|
98
|
+
const completion = event.completion ?? snapshot.completion;
|
|
99
|
+
const completionText = completion ? `\n\n**summary:** ${completion.summary}` : "";
|
|
100
|
+
const parentPrompt = terminalEvent(event.type) && snapshot.task.onCompletePrompt
|
|
101
|
+
? `\n\n## parent follow-up\n${snapshot.task.onCompletePrompt}`
|
|
102
|
+
: "";
|
|
103
|
+
const warnings = renderWarnings(snapshot.warnings);
|
|
104
|
+
return `## task ${label(event.type)}\n${header}${message}${completionText}${parentPrompt}${warnings}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderAssignment(header: string, assignment: TaskAssignment, warnings: readonly { readonly code: string; readonly message: string }[]): string {
|
|
108
|
+
const context = assignment.context?.summary ? `\n\n## context\n${assignment.context.summary}` : "";
|
|
109
|
+
const refs = assignment.context?.refs?.length
|
|
110
|
+
? `\n\n## selected refs\n${assignment.context.refs.map((ref) => `- \`${ref.path}\`${ref.selector ? ` — ${ref.selector}` : ""}${ref.purpose ? ` — ${ref.purpose}` : ""}`).join("\n")}`
|
|
111
|
+
: "";
|
|
112
|
+
return `## task assignment\n${header}\n\n${assignment.task}${context}${refs}${renderWarnings(warnings)}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderWarnings(warnings: readonly { readonly code: string; readonly message: string }[]): string {
|
|
116
|
+
return warnings.length === 0 ? "" : `\n\n## gateway warnings\n${warnings.map((warning) => `- ${warning.code}: ${warning.message}`).join("\n")}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function label(type: string): string {
|
|
120
|
+
return type.replace(/^task\./, "").replaceAll("_", " ");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function terminalEvent(type: string): boolean {
|
|
124
|
+
return type === "task.completed" || type === "task.failed" || type === "task.cancelled" || type === "task.timed_out";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function key(taskId: string, eventId: string): string {
|
|
128
|
+
return `${taskId}\u0000${eventId}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
132
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function nonEmpty(value: unknown): value is string {
|
|
136
|
+
return typeof value === "string" && value.length > 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function decimal(value: unknown): value is string {
|
|
140
|
+
return typeof value === "string" && /^(0|[1-9][0-9]*)$/.test(value);
|
|
141
|
+
}
|
package/src/auto-notify.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { AgentTaskRecord } from "./types";
|
|
2
|
-
|
|
3
|
-
export function selectUnpromptedTerminalTasks(
|
|
4
|
-
tasks: readonly AgentTaskRecord[],
|
|
5
|
-
autoPromptedTaskIds: ReadonlySet<string>,
|
|
6
|
-
): readonly AgentTaskRecord[] {
|
|
7
|
-
return tasks.filter((task) => !autoPromptedTaskIds.has(task.id));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function buildBackgroundTaskNotificationPrompt(tasks: readonly AgentTaskRecord[]): string {
|
|
11
|
-
const taskLines = tasks.map((task) => {
|
|
12
|
-
const summary = task.error?.message ?? task.status;
|
|
13
|
-
return `- ${task.id}: status=${task.status}, target=${task.targetSession}, summary=${summary}`;
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
return [
|
|
17
|
-
"background task results are ready for this parent session.",
|
|
18
|
-
"call `agent_task_inbox` with `{ ack: true }` now to read and acknowledge the structured results.",
|
|
19
|
-
"summarize the completed task results to the user concisely.",
|
|
20
|
-
"do not call agent_task_wait; these tasks already reached a terminal state.",
|
|
21
|
-
"tasks:",
|
|
22
|
-
...taskLines,
|
|
23
|
-
].join("\n");
|
|
24
|
-
}
|
package/src/protocol.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SUMMARY_MAX_CHARS,
|
|
3
|
-
TASK_ASSIGNMENT_TYPE,
|
|
4
|
-
TASK_PROTOCOL_VERSION,
|
|
5
|
-
type AgentTaskRecord,
|
|
6
|
-
type StoredTaskResult,
|
|
7
|
-
type TaskError,
|
|
8
|
-
} from "./types";
|
|
9
|
-
|
|
10
|
-
export interface AssignmentInput {
|
|
11
|
-
readonly taskId: string;
|
|
12
|
-
readonly fromSession: string;
|
|
13
|
-
readonly instructions: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface AssignmentEnvelope {
|
|
17
|
-
readonly type: typeof TASK_ASSIGNMENT_TYPE;
|
|
18
|
-
readonly taskId: string;
|
|
19
|
-
readonly fromSession: string;
|
|
20
|
-
readonly instructions: string;
|
|
21
|
-
readonly finishByCalling: "agent_task_done";
|
|
22
|
-
readonly taskProtocol: typeof TASK_PROTOCOL_VERSION;
|
|
23
|
-
readonly resultContract: {
|
|
24
|
-
readonly summaryMaxChars: number;
|
|
25
|
-
readonly noFinalProseAfterTool: true;
|
|
26
|
-
readonly completionIsStructuredOnly: true;
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface CompactTaskResult {
|
|
31
|
-
readonly schemaVersion: 1;
|
|
32
|
-
readonly taskId: string;
|
|
33
|
-
readonly status: AgentTaskRecord["status"];
|
|
34
|
-
readonly summary: string;
|
|
35
|
-
readonly artifacts: readonly string[];
|
|
36
|
-
readonly error: TaskError | null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function buildAssignment(input: AssignmentInput): string {
|
|
40
|
-
const envelope: AssignmentEnvelope = {
|
|
41
|
-
type: TASK_ASSIGNMENT_TYPE,
|
|
42
|
-
taskId: input.taskId,
|
|
43
|
-
fromSession: input.fromSession,
|
|
44
|
-
instructions: input.instructions,
|
|
45
|
-
finishByCalling: "agent_task_done",
|
|
46
|
-
taskProtocol: TASK_PROTOCOL_VERSION,
|
|
47
|
-
resultContract: {
|
|
48
|
-
summaryMaxChars: SUMMARY_MAX_CHARS,
|
|
49
|
-
noFinalProseAfterTool: true,
|
|
50
|
-
completionIsStructuredOnly: true,
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
return [
|
|
55
|
-
"structured task assignment:",
|
|
56
|
-
"```json",
|
|
57
|
-
JSON.stringify(envelope, null, 2),
|
|
58
|
-
"```",
|
|
59
|
-
"finish by calling agent_task_done exactly once. do not mark completion in prose.",
|
|
60
|
-
].join("\n");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function compactTaskResult(task: AgentTaskRecord, result: StoredTaskResult | undefined): CompactTaskResult {
|
|
64
|
-
return {
|
|
65
|
-
schemaVersion: 1,
|
|
66
|
-
taskId: task.id,
|
|
67
|
-
status: task.status,
|
|
68
|
-
summary: result?.summary ?? task.error?.message ?? task.status,
|
|
69
|
-
artifacts: task.resultRef ? [task.resultRef] : [],
|
|
70
|
-
error: result?.error ?? task.error ?? null,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|