nolo-cli 0.1.19 → 0.1.21

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.
Files changed (111) hide show
  1. package/README.md +9 -1
  2. package/agent-runtime/agentConfigOptions.ts +12 -0
  3. package/agent-runtime/agentRecordConfig.ts +99 -0
  4. package/agent-runtime/agentRecordKeys.ts +14 -0
  5. package/agent-runtime/dialogMessageRecord.ts +16 -0
  6. package/agent-runtime/dialogWritePlan.ts +130 -0
  7. package/agent-runtime/hostAdapter.ts +13 -0
  8. package/agent-runtime/hybridRecordStore.ts +147 -0
  9. package/agent-runtime/index.ts +69 -0
  10. package/agent-runtime/localLoop.ts +69 -5
  11. package/agent-runtime/localToolPolicy.ts +130 -0
  12. package/agent-runtime/localWorkspaceTools.ts +1532 -0
  13. package/agent-runtime/openAiCompatibleProvider.ts +70 -0
  14. package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
  15. package/agent-runtime/platformChatProvider.ts +241 -0
  16. package/agent-runtime/taskWorkspace.ts +193 -0
  17. package/agent-runtime/types.ts +1 -0
  18. package/agent-runtime/workspaceSession.ts +76 -0
  19. package/agentAliases.ts +37 -0
  20. package/agentPullCommand.ts +1 -1
  21. package/agentRunCommand.ts +278 -52
  22. package/agentRuntimeCommands.ts +354 -164
  23. package/agentRuntimeLocal.ts +38 -0
  24. package/ai/agent/agentSlice.ts +10 -0
  25. package/ai/agent/buildEditingContext.ts +5 -0
  26. package/ai/agent/buildSystemPrompt.ts +41 -18
  27. package/ai/agent/canvasEditingContext.ts +49 -0
  28. package/ai/agent/cliExecutor.ts +15 -4
  29. package/ai/agent/createAgentSchema.ts +2 -0
  30. package/ai/agent/executeToolCall.ts +3 -2
  31. package/ai/agent/hooks/usePublicAgents.ts +6 -0
  32. package/ai/agent/pageBuilderHandoffRules.ts +75 -0
  33. package/ai/agent/runAgentClientLoop.ts +4 -1
  34. package/ai/agent/runtimeGuidance.ts +19 -0
  35. package/ai/agent/server/fetchPublicAgents.ts +51 -1
  36. package/ai/agent/streamAgentChatTurn.ts +20 -2
  37. package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
  38. package/ai/chat/accumulateToolCallChunks.ts +40 -9
  39. package/ai/chat/parseApiError.ts +3 -0
  40. package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
  41. package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
  42. package/ai/chat/updateTotalUsage.ts +26 -9
  43. package/ai/llm/deepinfra.ts +51 -0
  44. package/ai/llm/getPricing.ts +6 -0
  45. package/ai/llm/kimi.ts +2 -0
  46. package/ai/llm/openrouterModels.ts +0 -135
  47. package/ai/llm/providers.ts +1 -0
  48. package/ai/llm/types.ts +8 -0
  49. package/ai/taskRun/taskRunProtocol.ts +882 -0
  50. package/ai/token/calculatePrice.ts +30 -0
  51. package/ai/token/externalToolCost.ts +49 -29
  52. package/ai/token/prepareTokenUsageData.ts +6 -1
  53. package/ai/token/serverTokenWriter.ts +4 -2
  54. package/ai/tools/agent/agentTools.ts +21 -0
  55. package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
  56. package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
  57. package/ai/tools/agent/taskRunTool.ts +112 -0
  58. package/ai/tools/applyEditTool.ts +6 -3
  59. package/ai/tools/applyLineEditsTool.ts +6 -3
  60. package/ai/tools/checkEnvTool.ts +14 -9
  61. package/ai/tools/codeSearchTool.ts +17 -5
  62. package/ai/tools/execBashTool.ts +33 -29
  63. package/ai/tools/fetchWebpageSupport.ts +24 -0
  64. package/ai/tools/fetchWebpageTool.ts +18 -5
  65. package/ai/tools/index.ts +158 -0
  66. package/ai/tools/jdProductScraperTool.ts +821 -0
  67. package/ai/tools/listFilesTool.ts +6 -3
  68. package/ai/tools/localFilesTool.ts +200 -0
  69. package/ai/tools/readFileTool.ts +6 -3
  70. package/ai/tools/searchRepoTool.ts +6 -3
  71. package/ai/tools/table/rowTools.ts +6 -1
  72. package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
  73. package/ai/tools/toolApiClient.ts +20 -6
  74. package/ai/tools/wereadGatewayTool.ts +152 -0
  75. package/ai/tools/writeFileTool.ts +6 -3
  76. package/client/agentConfigResolver.test.ts +70 -0
  77. package/client/agentConfigResolver.ts +1 -0
  78. package/client/agentRun.test.ts +430 -7
  79. package/client/agentRun.ts +504 -64
  80. package/client/hybridRecordStore.test.ts +115 -0
  81. package/client/hybridRecordStore.ts +41 -0
  82. package/client/localAgentRecords.test.ts +27 -0
  83. package/client/localAgentRecords.ts +7 -0
  84. package/client/localDialogRecords.test.ts +124 -0
  85. package/client/localDialogRecords.ts +30 -0
  86. package/client/localProviderResolver.test.ts +78 -0
  87. package/client/localProviderResolver.ts +1 -0
  88. package/client/localRuntimeAdapter.test.ts +621 -9
  89. package/client/localRuntimeAdapter.ts +275 -250
  90. package/client/localRuntimeDryRun.test.ts +116 -0
  91. package/client/localToolPolicy.ts +8 -81
  92. package/client/taskRunPrompt.ts +26 -0
  93. package/client/taskWorktree.ts +8 -0
  94. package/client/workspaceSession.test.ts +57 -0
  95. package/client/workspaceSession.ts +11 -0
  96. package/commandRegistry.ts +23 -6
  97. package/connectorRunArtifact.ts +121 -0
  98. package/database/actions/write.ts +16 -2
  99. package/database/hooks/useUserData.ts +9 -3
  100. package/database/server/dataHandlers.ts +18 -20
  101. package/database/server/emailRepository.ts +3 -3
  102. package/database/server/patch.ts +18 -10
  103. package/database/server/query.ts +43 -4
  104. package/database/server/read.ts +24 -38
  105. package/database/server/recordIdentity.ts +100 -0
  106. package/database/server/write.ts +21 -25
  107. package/index.ts +70 -33
  108. package/machineCommands.ts +318 -144
  109. package/package.json +4 -1
  110. package/tableCommands.ts +181 -0
  111. package/taskRunCommand.ts +265 -0
@@ -0,0 +1,882 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ export const TASK_RUN_SCHEMA_VERSION = 1;
4
+
5
+ export const TASK_RUN_TOOL_ACTIONS = [
6
+ "readContext",
7
+ "readTaskContext",
8
+ "queryReadyWorkItems",
9
+ "updateWorkItems",
10
+ "upsertWorkItem",
11
+ "claimOrDispatch",
12
+ "claimWorkItem",
13
+ "recordAgentRun",
14
+ "openDialog",
15
+ "recordArtifact",
16
+ "markBlocked",
17
+ "setBlocker",
18
+ "clearBlocker",
19
+ "requestReview",
20
+ "recordReview",
21
+ "submitOutcome",
22
+ "completeWorkItem",
23
+ "closeWorkItem",
24
+ ] as const;
25
+
26
+ export type TaskRunToolAction = (typeof TASK_RUN_TOOL_ACTIONS)[number];
27
+
28
+ export type TaskRunPhase =
29
+ | "planned"
30
+ | "running"
31
+ | "blocked"
32
+ | "needs_review"
33
+ | "needs_changes"
34
+ | "ready_for_alpha"
35
+ | "alpha_deployed"
36
+ | "main_approval_required"
37
+ | "main_ready"
38
+ | "ready_to_commit"
39
+ | "completed";
40
+
41
+ export type TaskRunWorkItem = {
42
+ id: string;
43
+ kind?: "work" | "review" | "coordination" | string;
44
+ title: string;
45
+ ownerAgent?: string;
46
+ status:
47
+ | "pending"
48
+ | "ready"
49
+ | "running"
50
+ | "blocked"
51
+ | "needs_review"
52
+ | "needs_changes"
53
+ | "completed";
54
+ dependsOn?: string[];
55
+ parallelGroup?: string;
56
+ writeScope?: string[];
57
+ reviewPolicy?: "none" | "after_item" | "after_group" | "before_commit";
58
+ reviewOfWorkItemId?: string;
59
+ dialogIds?: string[];
60
+ artifactRefs?: string[];
61
+ blockerIds?: string[];
62
+ outcome?: {
63
+ summary: string;
64
+ submittedAt: string;
65
+ artifactRefs?: string[];
66
+ closedAt?: string;
67
+ closeSummary?: string;
68
+ closeReason?: string;
69
+ };
70
+ };
71
+
72
+ export type TaskRunAgentRun = {
73
+ id: string;
74
+ agentKey: string;
75
+ role?: string;
76
+ workItemId?: string;
77
+ dialogId?: string;
78
+ status: "queued" | "running" | "completed" | "failed" | "timed_out";
79
+ startedAt: string;
80
+ updatedAt: string;
81
+ resultSummary?: string;
82
+ errorCode?: string;
83
+ toolsUsed?: string[];
84
+ toolTraceSummary?: {
85
+ callCount: number;
86
+ resultCount: number;
87
+ errorCount: number;
88
+ rounds: number;
89
+ };
90
+ };
91
+
92
+ export type TaskRunArtifact = {
93
+ id: string;
94
+ kind: "diff" | "patch" | "commit" | "test" | "screenshot" | "log" | "handoff" | string;
95
+ ref: string;
96
+ summary: string;
97
+ createdAt: string;
98
+ producerRunId?: string;
99
+ workItemId?: string;
100
+ };
101
+
102
+ export type TaskRunDialog = {
103
+ id: string;
104
+ purpose: "assignment" | "review" | "status" | "question" | "handoff" | string;
105
+ participantAgentKeys: string[];
106
+ workItemIds?: string[];
107
+ message?: string;
108
+ status: "open" | "closed";
109
+ openedAt: string;
110
+ closedAt?: string;
111
+ };
112
+
113
+ export type TaskRunEvent = {
114
+ id: string;
115
+ type: string;
116
+ actorId?: string;
117
+ agentKey?: string;
118
+ workItemId?: string;
119
+ dialogId?: string;
120
+ artifactId?: string;
121
+ reviewStatus?: TaskRunReview["status"];
122
+ reason?: string;
123
+ payload?: Record<string, unknown>;
124
+ createdAt: string;
125
+ };
126
+
127
+ export type TaskRunBlocker = {
128
+ id: string;
129
+ layer: string;
130
+ code: string;
131
+ message: string;
132
+ createdAt: string;
133
+ evidence?: string[];
134
+ resolvedAt?: string;
135
+ };
136
+
137
+ export type TaskRunReviewFinding = {
138
+ priority: "P0" | "P1" | "P2" | "P3";
139
+ summary: string;
140
+ file?: string;
141
+ line?: number;
142
+ workItemId?: string;
143
+ };
144
+
145
+ export type TaskRunReview = {
146
+ status: "not_requested" | "requested" | "passed" | "needs_changes" | "blocked";
147
+ reviewerAgentKey?: string;
148
+ targetWorkItemId?: string;
149
+ reviewWorkItemId?: string;
150
+ dialogId?: string;
151
+ reviewedArtifactIds?: string[];
152
+ findings?: TaskRunReviewFinding[];
153
+ updatedAt: string;
154
+ };
155
+
156
+ export type TaskRunAutonomy = "manual" | "supervised" | "auto24h";
157
+
158
+ export type TaskRunBudgetRule = {
159
+ provider?: string;
160
+ model?: string;
161
+ freeUntil?: string;
162
+ dailyUsdLimit?: number;
163
+ hardStopAfter?: string;
164
+ note?: string;
165
+ };
166
+
167
+ export type TaskRunCodexPolicy = {
168
+ mode: "quality_gate_only" | "supervisor" | "allowed";
169
+ requireFor?: string[];
170
+ avoidFor?: string[];
171
+ note?: string;
172
+ };
173
+
174
+ export type TaskRunCodexUsageSnapshot = {
175
+ source: "local_codex_session";
176
+ observedAt?: string;
177
+ planType?: string;
178
+ primary?: {
179
+ usedPercent?: number;
180
+ windowMinutes?: number;
181
+ resetsAt?: number;
182
+ };
183
+ secondary?: {
184
+ usedPercent?: number;
185
+ windowMinutes?: number;
186
+ resetsAt?: number;
187
+ };
188
+ lastTotalTokens?: number;
189
+ totalTokens?: number;
190
+ };
191
+
192
+ export type TaskRunControlPolicy = {
193
+ autonomy?: TaskRunAutonomy;
194
+ nextWakeAt?: string;
195
+ pausedReason?: string;
196
+ budget?: {
197
+ dailyUsdLimit?: number;
198
+ modelBudget?: Record<string, TaskRunBudgetRule>;
199
+ runnerBudget?: Record<string, TaskRunBudgetRule>;
200
+ };
201
+ codexPolicy?: TaskRunCodexPolicy;
202
+ codexUsageSnapshot?: TaskRunCodexUsageSnapshot;
203
+ };
204
+
205
+ export type TaskRunContext = {
206
+ schemaVersion: typeof TASK_RUN_SCHEMA_VERSION;
207
+ taskRunId: string;
208
+ taskRowId: string;
209
+ objective: string;
210
+ phase: TaskRunPhase;
211
+ control?: TaskRunControlPolicy;
212
+ updatedAt: string;
213
+ };
214
+
215
+ export type TaskRunMeta = {
216
+ taskRun?: TaskRunContext;
217
+ workItems?: TaskRunWorkItem[];
218
+ agentRuns?: TaskRunAgentRun[];
219
+ artifacts?: TaskRunArtifact[];
220
+ dialogs?: TaskRunDialog[];
221
+ taskEvents?: TaskRunEvent[];
222
+ review?: TaskRunReview;
223
+ blockers?: TaskRunBlocker[];
224
+ [key: string]: unknown;
225
+ };
226
+
227
+ export type TaskRunRowLike = {
228
+ rowId: string;
229
+ title?: string;
230
+ meta?: TaskRunMeta;
231
+ };
232
+
233
+ export type TaskRunDispatchPlan = {
234
+ ready: TaskRunWorkItem[];
235
+ waiting: Array<TaskRunWorkItem & { waitingOn: string[] }>;
236
+ active: TaskRunWorkItem[];
237
+ completed: TaskRunWorkItem[];
238
+ };
239
+
240
+ const createProtocolId = (): string => randomUUID().replace(/-/g, "");
241
+
242
+ const unique = (values: Array<string | undefined>): string[] => [
243
+ ...new Set(values.filter((value): value is string => Boolean(value))),
244
+ ];
245
+
246
+ export const DEFAULT_TASK_RUN_CONTROL_POLICY: TaskRunControlPolicy = {
247
+ autonomy: "supervised",
248
+ budget: {
249
+ modelBudget: {
250
+ projectManager: {
251
+ provider: "custom",
252
+ model: "kimi-k2.5",
253
+ note: "Default low-cost PM routing.",
254
+ },
255
+ frontend: {
256
+ model: "kimi-2.6",
257
+ freeUntil: "2026-05-22",
258
+ dailyUsdLimit: 1,
259
+ note: "Frontend implementation should prefer Kimi 2.6 while the free window is active.",
260
+ },
261
+ mimoCustomKey: {
262
+ provider: "mimo",
263
+ freeUntil: "2026-05-31",
264
+ note: "Free-use window before normal budget enforcement.",
265
+ },
266
+ },
267
+ },
268
+ codexPolicy: {
269
+ mode: "quality_gate_only",
270
+ requireFor: ["supervision", "critical_review", "complex_unblock", "main_or_release_decision"],
271
+ avoidFor: ["routine_implementation", "first_pass_triage", "formatting", "low_risk_probe"],
272
+ note: "Codex membership usage is still window-limited, so spend it on supervision and quality gates.",
273
+ },
274
+ };
275
+
276
+ const cloneTaskRunControlPolicy = (policy: TaskRunControlPolicy): TaskRunControlPolicy =>
277
+ JSON.parse(JSON.stringify(policy));
278
+
279
+ const cloneMeta = (meta: TaskRunMeta | undefined): TaskRunMeta => ({ ...(meta ?? {}) });
280
+
281
+ const touchTaskRun = (meta: TaskRunMeta, now: string, phase?: TaskRunPhase): void => {
282
+ if (!meta.taskRun) return;
283
+ meta.taskRun = {
284
+ ...meta.taskRun,
285
+ ...(phase ? { phase } : {}),
286
+ updatedAt: now,
287
+ };
288
+ };
289
+
290
+ const phaseForWorkItemStatus = (status: TaskRunWorkItem["status"]): TaskRunPhase | undefined => {
291
+ if (status === "running") return "running";
292
+ if (status === "blocked") return "blocked";
293
+ if (status === "needs_review") return "needs_review";
294
+ if (status === "needs_changes") return "needs_changes";
295
+ return undefined;
296
+ };
297
+
298
+ const appendTaskRunEvent = (args: {
299
+ meta: TaskRunMeta;
300
+ type: string;
301
+ now: string;
302
+ createId?: () => string;
303
+ actorId?: string;
304
+ agentKey?: string;
305
+ workItemId?: string;
306
+ dialogId?: string;
307
+ artifactId?: string;
308
+ reviewStatus?: TaskRunReview["status"];
309
+ reason?: string;
310
+ payload?: Record<string, unknown>;
311
+ }): TaskRunEvent => {
312
+ const event: TaskRunEvent = {
313
+ id: args.createId?.() ?? createProtocolId(),
314
+ type: args.type,
315
+ ...(args.actorId ? { actorId: args.actorId } : {}),
316
+ ...(args.agentKey ? { agentKey: args.agentKey } : {}),
317
+ ...(args.workItemId ? { workItemId: args.workItemId } : {}),
318
+ ...(args.dialogId ? { dialogId: args.dialogId } : {}),
319
+ ...(args.artifactId ? { artifactId: args.artifactId } : {}),
320
+ ...(args.reviewStatus ? { reviewStatus: args.reviewStatus } : {}),
321
+ ...(args.reason ? { reason: args.reason } : {}),
322
+ ...(args.payload ? { payload: args.payload } : {}),
323
+ createdAt: args.now,
324
+ };
325
+ args.meta.taskEvents = [...(args.meta.taskEvents ?? []), event];
326
+ return event;
327
+ };
328
+
329
+ export function prepareTaskRunMeta(args: {
330
+ row: TaskRunRowLike;
331
+ now?: string;
332
+ createId?: () => string;
333
+ }): { meta: TaskRunMeta; taskRun: TaskRunContext } {
334
+ const now = args.now ?? new Date().toISOString();
335
+ const meta = cloneMeta(args.row.meta);
336
+ const existing = meta.taskRun;
337
+ const { allowedActions: _legacyAllowedActions, ...existingTaskRun } = existing ?? {};
338
+ const taskRun: TaskRunContext = {
339
+ ...existingTaskRun,
340
+ schemaVersion: TASK_RUN_SCHEMA_VERSION,
341
+ taskRunId: existing?.taskRunId ?? `taskrun-${args.row.rowId}-${args.createId?.() ?? createProtocolId()}`,
342
+ taskRowId: existing?.taskRowId ?? args.row.rowId,
343
+ objective: existing?.objective ?? args.row.title ?? args.row.rowId,
344
+ phase: existing?.phase ?? "planned",
345
+ control: existing?.control ?? cloneTaskRunControlPolicy(DEFAULT_TASK_RUN_CONTROL_POLICY),
346
+ updatedAt: now,
347
+ };
348
+ meta.taskRun = taskRun;
349
+ return { meta, taskRun };
350
+ }
351
+
352
+ export function planTaskRunDispatch(meta: TaskRunMeta): TaskRunDispatchPlan {
353
+ const workItems = meta.workItems ?? [];
354
+ const completedIds = new Set(workItems.filter((item) => item.status === "completed").map((item) => item.id));
355
+ const ready: TaskRunWorkItem[] = [];
356
+ const waiting: Array<TaskRunWorkItem & { waitingOn: string[] }> = [];
357
+ const active: TaskRunWorkItem[] = [];
358
+ const completed: TaskRunWorkItem[] = [];
359
+
360
+ for (const item of workItems) {
361
+ if (item.status === "completed") {
362
+ completed.push(item);
363
+ continue;
364
+ }
365
+ if (item.status === "running" || item.status === "needs_review") {
366
+ active.push(item);
367
+ continue;
368
+ }
369
+ if (item.status === "blocked") {
370
+ waiting.push({ ...item, waitingOn: item.blockerIds ?? [] });
371
+ continue;
372
+ }
373
+ if (item.status === "needs_changes") {
374
+ ready.push(item);
375
+ continue;
376
+ }
377
+ const missingDeps = (item.dependsOn ?? []).filter((depId) => !completedIds.has(depId));
378
+ if (missingDeps.length > 0) {
379
+ waiting.push({ ...item, waitingOn: missingDeps });
380
+ continue;
381
+ }
382
+ ready.push(item);
383
+ }
384
+
385
+ return { ready, waiting, active, completed };
386
+ }
387
+
388
+ export function upsertTaskRunWorkItems(args: {
389
+ meta: TaskRunMeta;
390
+ workItems: TaskRunWorkItem[];
391
+ now?: string;
392
+ }): TaskRunMeta {
393
+ const now = args.now ?? new Date().toISOString();
394
+ const meta = cloneMeta(args.meta);
395
+ const items = [...(meta.workItems ?? [])];
396
+ for (const input of args.workItems) {
397
+ const index = items.findIndex((item) => item.id === input.id);
398
+ const existing = index >= 0 ? items[index] : undefined;
399
+ const workItem = existing
400
+ ? {
401
+ ...existing,
402
+ ...input,
403
+ dependsOn: unique([...(existing.dependsOn ?? []), ...(input.dependsOn ?? [])]),
404
+ writeScope: unique([...(existing.writeScope ?? []), ...(input.writeScope ?? [])]),
405
+ dialogIds: unique([...(existing.dialogIds ?? []), ...(input.dialogIds ?? [])]),
406
+ artifactRefs: unique([...(existing.artifactRefs ?? []), ...(input.artifactRefs ?? [])]),
407
+ blockerIds: unique([...(existing.blockerIds ?? []), ...(input.blockerIds ?? [])]),
408
+ }
409
+ : input;
410
+ if (index >= 0) {
411
+ items[index] = workItem;
412
+ } else {
413
+ items.push(workItem);
414
+ }
415
+ appendTaskRunEvent({
416
+ meta,
417
+ type: index >= 0 ? "work_item.updated" : "work_item.created",
418
+ workItemId: workItem.id,
419
+ now,
420
+ payload: { status: workItem.status, ownerAgent: workItem.ownerAgent },
421
+ });
422
+ touchTaskRun(meta, now, phaseForWorkItemStatus(workItem.status));
423
+ }
424
+ meta.workItems = items;
425
+ return meta;
426
+ }
427
+
428
+ export function updateTaskRunWorkItem(args: {
429
+ meta: TaskRunMeta;
430
+ id: string;
431
+ status?: TaskRunWorkItem["status"];
432
+ dialogIds?: string[];
433
+ artifactRefs?: string[];
434
+ blockerIds?: string[];
435
+ now?: string;
436
+ }): { meta: TaskRunMeta; workItem?: TaskRunWorkItem } {
437
+ const now = args.now ?? new Date().toISOString();
438
+ const meta = cloneMeta(args.meta);
439
+ const items = [...(meta.workItems ?? [])];
440
+ const index = items.findIndex((item) => item.id === args.id);
441
+ if (index < 0) return { meta };
442
+ const existing = items[index];
443
+ const workItem = {
444
+ ...existing,
445
+ ...(args.status ? { status: args.status } : {}),
446
+ dialogIds: unique([...(existing.dialogIds ?? []), ...(args.dialogIds ?? [])]),
447
+ artifactRefs: unique([...(existing.artifactRefs ?? []), ...(args.artifactRefs ?? [])]),
448
+ blockerIds: unique([...(existing.blockerIds ?? []), ...(args.blockerIds ?? [])]),
449
+ };
450
+ items[index] = workItem;
451
+ meta.workItems = items;
452
+ appendTaskRunEvent({
453
+ meta,
454
+ type: args.status ? "work_item.status_changed" : "work_item.updated",
455
+ workItemId: workItem.id,
456
+ now,
457
+ payload: {
458
+ fromStatus: existing.status,
459
+ toStatus: workItem.status,
460
+ artifactRefs: args.artifactRefs,
461
+ blockerIds: args.blockerIds,
462
+ dialogIds: args.dialogIds,
463
+ },
464
+ });
465
+ touchTaskRun(meta, now, args.status ? phaseForWorkItemStatus(args.status) : undefined);
466
+ return { meta, workItem };
467
+ }
468
+
469
+ export function recordTaskRunDialog(args: {
470
+ meta: TaskRunMeta;
471
+ purpose: TaskRunDialog["purpose"];
472
+ participantAgentKeys: string[];
473
+ workItemIds?: string[];
474
+ message?: string;
475
+ now?: string;
476
+ createId?: () => string;
477
+ }): { meta: TaskRunMeta; dialog: TaskRunDialog } {
478
+ const now = args.now ?? new Date().toISOString();
479
+ const meta = cloneMeta(args.meta);
480
+ const dialog: TaskRunDialog = {
481
+ id: args.createId?.() ?? createProtocolId(),
482
+ purpose: args.purpose,
483
+ participantAgentKeys: unique(args.participantAgentKeys),
484
+ ...(args.workItemIds?.length ? { workItemIds: unique(args.workItemIds) } : {}),
485
+ ...(args.message ? { message: args.message } : {}),
486
+ status: "open",
487
+ openedAt: now,
488
+ };
489
+ meta.dialogs = [...(meta.dialogs ?? []), dialog];
490
+ appendTaskRunEvent({
491
+ meta,
492
+ type: "dialog.opened",
493
+ dialogId: dialog.id,
494
+ now,
495
+ payload: {
496
+ purpose: dialog.purpose,
497
+ participantAgentKeys: dialog.participantAgentKeys,
498
+ workItemIds: dialog.workItemIds,
499
+ },
500
+ });
501
+ let nextMeta = meta;
502
+ for (const workItemId of dialog.workItemIds ?? []) {
503
+ nextMeta = updateTaskRunWorkItem({ meta: nextMeta, id: workItemId, dialogIds: [dialog.id], now }).meta;
504
+ }
505
+ touchTaskRun(nextMeta, now);
506
+ return { meta: nextMeta, dialog };
507
+ }
508
+
509
+ export function recordTaskRunArtifact(args: {
510
+ meta: TaskRunMeta;
511
+ artifact: Omit<TaskRunArtifact, "id" | "createdAt">;
512
+ now?: string;
513
+ createId?: () => string;
514
+ }): { meta: TaskRunMeta; artifact: TaskRunArtifact } {
515
+ const now = args.now ?? new Date().toISOString();
516
+ const meta = cloneMeta(args.meta);
517
+ const artifact = { id: args.createId?.() ?? createProtocolId(), ...args.artifact, createdAt: now };
518
+ meta.artifacts = [...(meta.artifacts ?? []), artifact];
519
+ appendTaskRunEvent({
520
+ meta,
521
+ type: "artifact.recorded",
522
+ artifactId: artifact.id,
523
+ workItemId: artifact.workItemId,
524
+ now,
525
+ payload: { kind: artifact.kind, ref: artifact.ref, summary: artifact.summary },
526
+ });
527
+ touchTaskRun(meta, now);
528
+ return { meta, artifact };
529
+ }
530
+
531
+ export function recordTaskRunBlocker(args: {
532
+ meta: TaskRunMeta;
533
+ blocker: Omit<TaskRunBlocker, "id" | "createdAt">;
534
+ now?: string;
535
+ createId?: () => string;
536
+ }): { meta: TaskRunMeta; blocker: TaskRunBlocker } {
537
+ const now = args.now ?? new Date().toISOString();
538
+ const meta = cloneMeta(args.meta);
539
+ const blocker = { id: args.createId?.() ?? createProtocolId(), ...args.blocker, createdAt: now };
540
+ meta.blockers = [...(meta.blockers ?? []), blocker];
541
+ appendTaskRunEvent({
542
+ meta,
543
+ type: "blocker.recorded",
544
+ now,
545
+ payload: { blockerId: blocker.id, layer: blocker.layer, code: blocker.code, message: blocker.message },
546
+ });
547
+ touchTaskRun(meta, now, "blocked");
548
+ return { meta, blocker };
549
+ }
550
+
551
+ export function clearTaskRunBlocker(args: {
552
+ meta: TaskRunMeta;
553
+ blockerId: string;
554
+ workItemId?: string;
555
+ now?: string;
556
+ }): { meta: TaskRunMeta; blocker?: TaskRunBlocker; workItem?: TaskRunWorkItem } {
557
+ const now = args.now ?? new Date().toISOString();
558
+ const meta = cloneMeta(args.meta);
559
+ const blockers = [...(meta.blockers ?? [])];
560
+ const blockerIndex = blockers.findIndex((blocker) => blocker.id === args.blockerId);
561
+ if (blockerIndex < 0) return { meta };
562
+ const blocker = { ...blockers[blockerIndex], resolvedAt: now };
563
+ blockers[blockerIndex] = blocker;
564
+ meta.blockers = blockers;
565
+ let workItem: TaskRunWorkItem | undefined;
566
+ if (args.workItemId) {
567
+ const items = [...(meta.workItems ?? [])];
568
+ const itemIndex = items.findIndex((item) => item.id === args.workItemId);
569
+ if (itemIndex >= 0) {
570
+ const existing = items[itemIndex];
571
+ const blockerIds = (existing.blockerIds ?? []).filter((id) => id !== args.blockerId);
572
+ workItem = {
573
+ ...existing,
574
+ blockerIds,
575
+ ...(existing.status === "blocked" && blockerIds.length === 0 ? { status: "pending" as const } : {}),
576
+ };
577
+ items[itemIndex] = workItem;
578
+ meta.workItems = items;
579
+ }
580
+ }
581
+ appendTaskRunEvent({
582
+ meta,
583
+ type: "blocker.cleared",
584
+ workItemId: args.workItemId,
585
+ now,
586
+ payload: { blockerId: blocker.id },
587
+ });
588
+ touchTaskRun(meta, now);
589
+ return { meta, blocker, workItem };
590
+ }
591
+
592
+ export function recordTaskRunAgentDispatch(args: {
593
+ meta: TaskRunMeta;
594
+ workItemId: string;
595
+ agentKey: string;
596
+ role?: string;
597
+ now?: string;
598
+ createId?: () => string;
599
+ }): { meta: TaskRunMeta; workItem?: TaskRunWorkItem; agentRun?: TaskRunAgentRun } {
600
+ const now = args.now ?? new Date().toISOString();
601
+ const updated = updateTaskRunWorkItem({ meta: args.meta, id: args.workItemId, status: "running", now });
602
+ if (!updated.workItem) return { meta: updated.meta };
603
+ const agentRun: TaskRunAgentRun = {
604
+ id: args.createId?.() ?? createProtocolId(),
605
+ agentKey: args.agentKey,
606
+ role: args.role,
607
+ workItemId: args.workItemId,
608
+ status: "queued",
609
+ startedAt: now,
610
+ updatedAt: now,
611
+ };
612
+ updated.meta.agentRuns = [...(updated.meta.agentRuns ?? []), agentRun];
613
+ appendTaskRunEvent({
614
+ meta: updated.meta,
615
+ type: "work_item.claimed",
616
+ agentKey: args.agentKey,
617
+ workItemId: args.workItemId,
618
+ now,
619
+ payload: { agentRunId: agentRun.id, role: args.role },
620
+ });
621
+ touchTaskRun(updated.meta, now, "running");
622
+ return { meta: updated.meta, workItem: updated.workItem, agentRun };
623
+ }
624
+
625
+ export function recordTaskRunAgentRun(args: {
626
+ meta: TaskRunMeta;
627
+ run: Omit<TaskRunAgentRun, "id" | "startedAt" | "updatedAt"> & {
628
+ id?: string;
629
+ startedAt?: string;
630
+ };
631
+ now?: string;
632
+ createId?: () => string;
633
+ }): { meta: TaskRunMeta; agentRun: TaskRunAgentRun } {
634
+ const now = args.now ?? new Date().toISOString();
635
+ const meta = cloneMeta(args.meta);
636
+ const agentRuns = [...(meta.agentRuns ?? [])];
637
+ const id = args.run.id ?? args.createId?.() ?? createProtocolId();
638
+ const index = agentRuns.findIndex((run) => run.id === id);
639
+ const existing = index >= 0 ? agentRuns[index] : undefined;
640
+ const agentRun: TaskRunAgentRun = {
641
+ ...(existing ?? {
642
+ id,
643
+ agentKey: args.run.agentKey,
644
+ role: args.run.role,
645
+ startedAt: args.run.startedAt ?? now,
646
+ }),
647
+ ...args.run,
648
+ id,
649
+ startedAt: existing?.startedAt ?? args.run.startedAt ?? now,
650
+ updatedAt: now,
651
+ };
652
+ if (index >= 0) {
653
+ agentRuns[index] = agentRun;
654
+ } else {
655
+ agentRuns.push(agentRun);
656
+ }
657
+ meta.agentRuns = agentRuns;
658
+ appendTaskRunEvent({
659
+ meta,
660
+ type: "agent_run.recorded",
661
+ agentKey: agentRun.agentKey,
662
+ workItemId: agentRun.workItemId,
663
+ dialogId: agentRun.dialogId,
664
+ now,
665
+ payload: {
666
+ agentRunId: agentRun.id,
667
+ status: agentRun.status,
668
+ toolsUsed: agentRun.toolsUsed,
669
+ toolTraceSummary: agentRun.toolTraceSummary,
670
+ },
671
+ });
672
+ touchTaskRun(meta, now, agentRun.status === "running" ? "running" : undefined);
673
+ return { meta, agentRun };
674
+ }
675
+
676
+ export function submitTaskRunOutcome(args: {
677
+ meta: TaskRunMeta;
678
+ workItemId: string;
679
+ summary: string;
680
+ artifactIds?: string[];
681
+ dialogId?: string;
682
+ now?: string;
683
+ }): { meta: TaskRunMeta; workItem?: TaskRunWorkItem } {
684
+ const now = args.now ?? new Date().toISOString();
685
+ const updated = updateTaskRunWorkItem({
686
+ meta: args.meta,
687
+ id: args.workItemId,
688
+ status: "needs_review",
689
+ dialogIds: args.dialogId ? [args.dialogId] : undefined,
690
+ artifactRefs: args.artifactIds,
691
+ now,
692
+ });
693
+ if (!updated.workItem) return updated;
694
+ const items = [...(updated.meta.workItems ?? [])];
695
+ const index = items.findIndex((item) => item.id === args.workItemId);
696
+ const workItem = {
697
+ ...updated.workItem,
698
+ outcome: {
699
+ summary: args.summary,
700
+ submittedAt: now,
701
+ ...(args.artifactIds?.length ? { artifactRefs: unique(args.artifactIds) } : {}),
702
+ },
703
+ };
704
+ if (index >= 0) items[index] = workItem;
705
+ updated.meta.workItems = items;
706
+ appendTaskRunEvent({
707
+ meta: updated.meta,
708
+ type: "work_item.outcome_submitted",
709
+ workItemId: args.workItemId,
710
+ dialogId: args.dialogId,
711
+ now,
712
+ payload: { summary: args.summary, artifactIds: args.artifactIds },
713
+ });
714
+ touchTaskRun(updated.meta, now, "needs_review");
715
+ return { meta: updated.meta, workItem };
716
+ }
717
+
718
+ export function closeTaskRunWorkItem(args: {
719
+ meta: TaskRunMeta;
720
+ workItemId: string;
721
+ summary: string;
722
+ reason?: string;
723
+ artifactIds?: string[];
724
+ dialogId?: string;
725
+ now?: string;
726
+ }): { meta: TaskRunMeta; workItem?: TaskRunWorkItem } {
727
+ const now = args.now ?? new Date().toISOString();
728
+ const updated = updateTaskRunWorkItem({
729
+ meta: args.meta,
730
+ id: args.workItemId,
731
+ status: "completed",
732
+ dialogIds: args.dialogId ? [args.dialogId] : undefined,
733
+ artifactRefs: args.artifactIds,
734
+ now,
735
+ });
736
+ if (!updated.workItem) return updated;
737
+ const items = [...(updated.meta.workItems ?? [])];
738
+ const index = items.findIndex((item) => item.id === args.workItemId);
739
+ const workItem = {
740
+ ...updated.workItem,
741
+ outcome: {
742
+ ...(updated.workItem.outcome ?? { summary: args.summary, submittedAt: now }),
743
+ closedAt: now,
744
+ closeSummary: args.summary,
745
+ ...(args.reason ? { closeReason: args.reason } : {}),
746
+ ...(args.artifactIds?.length
747
+ ? { artifactRefs: unique([...(updated.workItem.outcome?.artifactRefs ?? []), ...args.artifactIds]) }
748
+ : {}),
749
+ },
750
+ };
751
+ if (index >= 0) items[index] = workItem;
752
+ updated.meta.workItems = items;
753
+ appendTaskRunEvent({
754
+ meta: updated.meta,
755
+ type: "work_item.closed",
756
+ workItemId: args.workItemId,
757
+ dialogId: args.dialogId,
758
+ now,
759
+ reason: args.reason,
760
+ payload: { summary: args.summary, artifactIds: args.artifactIds },
761
+ });
762
+ touchTaskRun(updated.meta, now);
763
+ return { meta: updated.meta, workItem };
764
+ }
765
+
766
+ export function requestTaskRunReview(args: {
767
+ meta: TaskRunMeta;
768
+ reviewerAgentKey: string;
769
+ targetWorkItemId?: string;
770
+ artifactIds?: string[];
771
+ dialogId?: string;
772
+ message?: string;
773
+ now?: string;
774
+ createId?: () => string;
775
+ }): { meta: TaskRunMeta; review: TaskRunReview } {
776
+ const now = args.now ?? new Date().toISOString();
777
+ const meta = cloneMeta(args.meta);
778
+ const reviewWorkItemId = args.targetWorkItemId
779
+ ? `review-${args.targetWorkItemId}-${args.createId?.() ?? createProtocolId()}`
780
+ : undefined;
781
+ let reviewDialogId = args.dialogId;
782
+ let nextMeta = meta;
783
+ if (args.targetWorkItemId) {
784
+ const reviewWorkItem: TaskRunWorkItem = {
785
+ id: reviewWorkItemId ?? `review-${args.targetWorkItemId}`,
786
+ kind: "review",
787
+ title: `Review ${args.targetWorkItemId}`,
788
+ ownerAgent: args.reviewerAgentKey,
789
+ status: "ready",
790
+ dependsOn: [args.targetWorkItemId],
791
+ reviewPolicy: "none",
792
+ reviewOfWorkItemId: args.targetWorkItemId,
793
+ artifactRefs: args.artifactIds,
794
+ };
795
+ nextMeta = upsertTaskRunWorkItems({ meta: nextMeta, workItems: [reviewWorkItem], now });
796
+ if (!reviewDialogId) {
797
+ const opened = recordTaskRunDialog({
798
+ meta: nextMeta,
799
+ purpose: "review",
800
+ participantAgentKeys: [args.reviewerAgentKey],
801
+ workItemIds: [args.targetWorkItemId, reviewWorkItem.id],
802
+ message: args.message ?? `Please review ${args.targetWorkItemId}.`,
803
+ now,
804
+ createId: args.createId,
805
+ });
806
+ nextMeta = opened.meta;
807
+ reviewDialogId = opened.dialog.id;
808
+ }
809
+ }
810
+ const review: TaskRunReview = {
811
+ ...(nextMeta.review ?? { status: "not_requested", updatedAt: now }),
812
+ status: "requested",
813
+ reviewerAgentKey: args.reviewerAgentKey,
814
+ ...(args.targetWorkItemId ? { targetWorkItemId: args.targetWorkItemId } : {}),
815
+ ...(reviewWorkItemId ? { reviewWorkItemId } : {}),
816
+ ...(reviewDialogId ? { dialogId: reviewDialogId } : {}),
817
+ reviewedArtifactIds: unique([...(nextMeta.review?.reviewedArtifactIds ?? []), ...(args.artifactIds ?? [])]),
818
+ updatedAt: now,
819
+ };
820
+ nextMeta.review = review;
821
+ appendTaskRunEvent({
822
+ meta: nextMeta,
823
+ type: "review.requested",
824
+ workItemId: args.targetWorkItemId,
825
+ dialogId: reviewDialogId,
826
+ reviewStatus: "requested",
827
+ agentKey: args.reviewerAgentKey,
828
+ now,
829
+ payload: { artifactIds: args.artifactIds, reviewWorkItemId },
830
+ });
831
+ touchTaskRun(nextMeta, now, "needs_review");
832
+ return { meta: nextMeta, review };
833
+ }
834
+
835
+ export function recordTaskRunReview(args: {
836
+ meta: TaskRunMeta;
837
+ status: Exclude<TaskRunReview["status"], "not_requested" | "requested">;
838
+ targetWorkItemId?: string;
839
+ reviewerAgentKey?: string;
840
+ artifactIds?: string[];
841
+ dialogId?: string;
842
+ findings?: TaskRunReviewFinding[];
843
+ now?: string;
844
+ }): { meta: TaskRunMeta; review: TaskRunReview } {
845
+ const now = args.now ?? new Date().toISOString();
846
+ const meta = cloneMeta(args.meta);
847
+ const review: TaskRunReview = {
848
+ ...(meta.review ?? { status: "not_requested", updatedAt: now }),
849
+ status: args.status,
850
+ ...(args.reviewerAgentKey ? { reviewerAgentKey: args.reviewerAgentKey } : {}),
851
+ ...(args.targetWorkItemId ? { targetWorkItemId: args.targetWorkItemId } : {}),
852
+ ...(args.dialogId ? { dialogId: args.dialogId } : {}),
853
+ reviewedArtifactIds: unique([...(meta.review?.reviewedArtifactIds ?? []), ...(args.artifactIds ?? [])]),
854
+ findings: args.findings ?? meta.review?.findings,
855
+ updatedAt: now,
856
+ };
857
+ meta.review = review;
858
+ let nextMeta = meta;
859
+ const targetWorkItemId = args.targetWorkItemId ?? meta.review?.targetWorkItemId;
860
+ if (args.status === "needs_changes" && targetWorkItemId) {
861
+ nextMeta = updateTaskRunWorkItem({
862
+ meta: nextMeta,
863
+ id: targetWorkItemId,
864
+ status: "needs_changes",
865
+ artifactRefs: args.artifactIds,
866
+ dialogIds: args.dialogId ? [args.dialogId] : undefined,
867
+ now,
868
+ }).meta;
869
+ }
870
+ appendTaskRunEvent({
871
+ meta: nextMeta,
872
+ type: "review.recorded",
873
+ dialogId: args.dialogId,
874
+ workItemId: targetWorkItemId,
875
+ reviewStatus: args.status,
876
+ agentKey: args.reviewerAgentKey,
877
+ now,
878
+ payload: { artifactIds: args.artifactIds, findings: args.findings },
879
+ });
880
+ touchTaskRun(nextMeta, now, args.status === "passed" ? "ready_for_alpha" : args.status);
881
+ return { meta: nextMeta, review };
882
+ }