@bermudi/pi-delegate 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -10
- package/agents.ts +176 -19
- package/concurrency.ts +70 -7
- package/constants.ts +3 -0
- package/delegate.ts +26 -1
- package/dispatch.ts +65 -9
- package/extension.ts +86 -6
- package/file-tracking.ts +27 -5
- package/format.ts +46 -7
- package/host-compat.ts +47 -12
- package/host.ts +93 -16
- package/leaf.ts +48 -0
- package/lifecycle.ts +292 -95
- package/manual.ts +45 -11
- package/model.ts +3 -4
- package/package.json +25 -20
- package/patches/@marcfargas%2Fpi-test-harness@0.6.1.patch +13 -0
- package/pool.ts +169 -51
- package/render-branches.ts +52 -16
- package/render-result.ts +12 -0
- package/runner.ts +255 -51
- package/schema.ts +252 -63
- package/status.ts +269 -0
- package/task-resolution.ts +196 -77
- package/tickets.ts +173 -62
- package/tools.ts +16 -15
- package/types.ts +62 -13
package/schema.ts
CHANGED
|
@@ -18,15 +18,29 @@ function StringEnum<const T extends readonly string[]>(
|
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const TASK_ID_PATTERN = "^[A-Za-z0-9._-]{1,64}$";
|
|
22
|
+
const TASK_ID_RE = new RegExp(TASK_ID_PATTERN);
|
|
23
|
+
|
|
21
24
|
export const delegateTaskSchema = Type.Object({
|
|
25
|
+
id: Type.Optional(
|
|
26
|
+
Type.String({
|
|
27
|
+
pattern: TASK_ID_PATTERN,
|
|
28
|
+
minLength: 1,
|
|
29
|
+
maxLength: 64,
|
|
30
|
+
description:
|
|
31
|
+
"Optional task correlation key; 1-64 chars; A-Z a-z 0-9 . _ - only; duplicate ids rejected. Omit for index.",
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
22
34
|
prompt: Type.Optional(
|
|
23
35
|
Type.String({
|
|
24
|
-
description:
|
|
36
|
+
description:
|
|
37
|
+
"Self-contained task prompt; fresh context cannot see this chat. Omit only for close, list, or resumeFrom.",
|
|
25
38
|
}),
|
|
26
39
|
),
|
|
27
40
|
agent: Type.Optional(
|
|
28
41
|
Type.String({
|
|
29
|
-
description:
|
|
42
|
+
description:
|
|
43
|
+
"Use `default`: parent model/thinking/native tools/base prompt. Omit=ad-hoc; unknown fails call.",
|
|
30
44
|
}),
|
|
31
45
|
),
|
|
32
46
|
cwd: Type.Optional(
|
|
@@ -36,13 +50,14 @@ export const delegateTaskSchema = Type.Object({
|
|
|
36
50
|
),
|
|
37
51
|
systemPrompt: Type.Optional(
|
|
38
52
|
Type.String({
|
|
39
|
-
description:
|
|
53
|
+
description:
|
|
54
|
+
"Base system prompt; project resources from the task cwd are added automatically.",
|
|
40
55
|
}),
|
|
41
56
|
),
|
|
42
57
|
context: Type.Optional(
|
|
43
58
|
StringEnum(["fresh", "with-parent-transcript"], {
|
|
44
59
|
description:
|
|
45
|
-
"fresh omits
|
|
60
|
+
"fresh omits this chat; with-parent-transcript copies it (token-expensive).",
|
|
46
61
|
default: "fresh",
|
|
47
62
|
}),
|
|
48
63
|
),
|
|
@@ -54,22 +69,22 @@ export const delegateTaskSchema = Type.Object({
|
|
|
54
69
|
tools: Type.Optional(
|
|
55
70
|
Type.Array(Type.String(), {
|
|
56
71
|
description:
|
|
57
|
-
"
|
|
72
|
+
"Names/presets: *=read/write/edit/bash (mutating); ro=read/grep/find/ls (read-only). Ad-hoc defaults to *.",
|
|
58
73
|
}),
|
|
59
74
|
),
|
|
60
75
|
thinking: Type.Optional(
|
|
61
76
|
StringEnum(VALID_THINKING_LEVELS, {
|
|
62
77
|
description:
|
|
63
|
-
"Thinking: off/minimal/low/medium/high/xhigh/max;
|
|
78
|
+
"Thinking: off/minimal/low/medium/high/xhigh/max; default=parent; others=agent/off.",
|
|
64
79
|
}),
|
|
65
80
|
),
|
|
66
81
|
sessionId: Type.Optional(
|
|
67
82
|
Type.String({
|
|
68
83
|
description:
|
|
69
|
-
"
|
|
84
|
+
"Live pool key for multi-turn reuse; omit for one-shot tasks.",
|
|
70
85
|
}),
|
|
71
86
|
),
|
|
72
|
-
|
|
87
|
+
sessionAction: Type.Optional(
|
|
73
88
|
StringEnum(["prompt", "close", "list"], {
|
|
74
89
|
description:
|
|
75
90
|
"Session action; close needs sessionId; list shows active pooled sessions.",
|
|
@@ -82,21 +97,29 @@ export const delegateTaskSchema = Type.Object({
|
|
|
82
97
|
"Exact absolute .jsonl session path from retry output; never a ticket ID.",
|
|
83
98
|
}),
|
|
84
99
|
),
|
|
100
|
+
deadlineMs: Type.Optional(
|
|
101
|
+
Type.Number({
|
|
102
|
+
description:
|
|
103
|
+
"Wall-clock budget (ms) from run start after queueing. Cooperative abort; side effects remain. Omit disables.",
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
85
106
|
});
|
|
86
107
|
|
|
87
|
-
// Single source of truth for registration
|
|
88
|
-
//
|
|
108
|
+
// Single source of truth for registration and generated help. The exported
|
|
109
|
+
// argument types in types.ts project this canonical schema and add deprecated
|
|
110
|
+
// `action` aliases as a type-only compatibility overlay; providers never see
|
|
111
|
+
// those legacy fields in this schema.
|
|
89
112
|
export const delegateArgumentsSchema = Type.Object({
|
|
90
|
-
|
|
113
|
+
ticketAction: Type.Optional(
|
|
91
114
|
StringEnum(["poll", "cancel", "wait"], {
|
|
92
115
|
description:
|
|
93
|
-
"Ticket control: poll
|
|
116
|
+
"Ticket control: poll=snapshot; wait=block until settled; cancel=abort. Prefer wait; never cancel for time.",
|
|
94
117
|
}),
|
|
95
118
|
),
|
|
96
119
|
async: Type.Optional(
|
|
97
120
|
Type.Boolean({
|
|
98
121
|
description:
|
|
99
|
-
"Detach work
|
|
122
|
+
"Detach work, return a ticket; applies to ALL tasks. Results auto-deliver. Wait only if blocked.",
|
|
100
123
|
default: false,
|
|
101
124
|
}),
|
|
102
125
|
),
|
|
@@ -108,7 +131,7 @@ export const delegateArgumentsSchema = Type.Object({
|
|
|
108
131
|
force: Type.Optional(
|
|
109
132
|
Type.Boolean({
|
|
110
133
|
description:
|
|
111
|
-
"
|
|
134
|
+
"With cancel: false previews active work; true confirms abort. Completed writes/commands remain.",
|
|
112
135
|
default: false,
|
|
113
136
|
}),
|
|
114
137
|
),
|
|
@@ -116,47 +139,116 @@ export const delegateArgumentsSchema = Type.Object({
|
|
|
116
139
|
Type.Number({
|
|
117
140
|
minimum: 0,
|
|
118
141
|
description:
|
|
119
|
-
"
|
|
142
|
+
"Bounds wait (ms); omit to block until settled. Timeout never cancels the ticket.",
|
|
120
143
|
}),
|
|
121
144
|
),
|
|
122
145
|
tasks: Type.Optional(
|
|
123
146
|
Type.Array(delegateTaskSchema, {
|
|
124
147
|
minItems: 0,
|
|
125
148
|
description:
|
|
126
|
-
"
|
|
149
|
+
"Tasks share the real filesystem and run concurrently; separate dependent/shared-file work. []=full manual.",
|
|
127
150
|
}),
|
|
128
151
|
),
|
|
129
152
|
});
|
|
130
153
|
|
|
154
|
+
/** Fields that belong to a task entry. Models sometimes place these at the
|
|
155
|
+
* top level of the arguments; the shim folds them back into a single task. */
|
|
156
|
+
const TASK_FIELD_NAMES = [
|
|
157
|
+
"id",
|
|
158
|
+
"prompt",
|
|
159
|
+
"agent",
|
|
160
|
+
"cwd",
|
|
161
|
+
"systemPrompt",
|
|
162
|
+
"context",
|
|
163
|
+
"model",
|
|
164
|
+
"tools",
|
|
165
|
+
"thinking",
|
|
166
|
+
"sessionId",
|
|
167
|
+
"sessionAction",
|
|
168
|
+
"resumeFrom",
|
|
169
|
+
"deadlineMs",
|
|
170
|
+
] as const;
|
|
171
|
+
|
|
172
|
+
/** Every field a task entry may carry. Anything else is a model mistake —
|
|
173
|
+
* e.g. `async` placed inside a task — and must fail loudly with a
|
|
174
|
+
* corrective message instead of being silently ignored (observed in the
|
|
175
|
+
* wild: a task-level `async: true` the caller believed had backgrounded
|
|
176
|
+
* the work while the call in fact ran synchronously). */
|
|
177
|
+
const VALID_TASK_KEYS = new Set<string>([...TASK_FIELD_NAMES, "sessionAction"]);
|
|
178
|
+
|
|
179
|
+
/** Top-level ticket actions the legacy `action` field may map to. */
|
|
180
|
+
const TICKET_ACTIONS = new Set(["poll", "cancel", "wait"]);
|
|
181
|
+
|
|
182
|
+
/** Session actions that are valid at the task level. A flat `action` at the
|
|
183
|
+
* top level may also fold into a wrapped task's `sessionAction`. */
|
|
184
|
+
const TASK_ACTIONS = new Set(["prompt", "close", "list"]);
|
|
185
|
+
|
|
186
|
+
/** Every value the legacy `action` field can carry before it is normalized to
|
|
187
|
+
* `ticketAction` or `sessionAction`. */
|
|
188
|
+
const LEGACY_ACTIONS = new Set([...TICKET_ACTIONS, ...TASK_ACTIONS]);
|
|
189
|
+
|
|
131
190
|
/** Validate the three operation modes after compatibility reshaping. */
|
|
132
191
|
export function validateDelegateOperation(
|
|
133
192
|
params: DelegateArguments,
|
|
134
193
|
): string | undefined {
|
|
194
|
+
const rawParams = params as Record<string, unknown>;
|
|
135
195
|
const tasks = params.tasks ?? [];
|
|
136
|
-
|
|
196
|
+
|
|
197
|
+
const hasLegacyAction = typeof rawParams.action === "string";
|
|
198
|
+
const hasTicketAction = params.ticketAction !== undefined;
|
|
199
|
+
|
|
200
|
+
if (hasLegacyAction) {
|
|
201
|
+
if (!LEGACY_ACTIONS.has(rawParams.action as string)) {
|
|
202
|
+
return `unknown action '${rawParams.action}'; valid ticket actions are poll/cancel/wait, valid session actions are prompt/close/list.`;
|
|
203
|
+
}
|
|
204
|
+
if (hasTicketAction) {
|
|
205
|
+
return "ambiguous: supply only ticketAction (or only legacy action), not both.";
|
|
206
|
+
}
|
|
207
|
+
if (TASK_ACTIONS.has(rawParams.action as string) && tasks.length > 0) {
|
|
208
|
+
return `legacy top-level action '${rawParams.action}' cannot be combined with an explicit tasks array; move it into the task's sessionAction or remove tasks.`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const ticketAction: string | undefined =
|
|
213
|
+
params.ticketAction ??
|
|
214
|
+
(hasLegacyAction && TICKET_ACTIONS.has(rawParams.action as string)
|
|
215
|
+
? (rawParams.action as string)
|
|
216
|
+
: undefined);
|
|
217
|
+
|
|
218
|
+
const isTicketControl = ticketAction !== undefined;
|
|
137
219
|
|
|
138
220
|
if (isTicketControl) {
|
|
139
|
-
|
|
140
|
-
|
|
221
|
+
const topLevelTaskIntentFields = [...TASK_FIELD_NAMES, "tasks"] as const;
|
|
222
|
+
const taskIntentFields = topLevelTaskIntentFields.filter(
|
|
223
|
+
(field) => rawParams[field] !== undefined,
|
|
224
|
+
);
|
|
225
|
+
if (taskIntentFields.length) {
|
|
226
|
+
return `ticket control cannot be combined with task-intent field(s) ${taskIntentFields
|
|
227
|
+
.map((field) => `'${field}'`)
|
|
228
|
+
.join(", ")}; call it separately.`;
|
|
141
229
|
}
|
|
142
|
-
if (params.
|
|
143
|
-
return
|
|
230
|
+
if (params.async === true) {
|
|
231
|
+
return "ticket control cannot include async; call it separately.";
|
|
144
232
|
}
|
|
145
|
-
if (
|
|
146
|
-
return
|
|
233
|
+
if (ticketAction !== "poll" && !params.ticket) {
|
|
234
|
+
return `ticketAction '${ticketAction}' requires ticket.`;
|
|
147
235
|
}
|
|
148
|
-
if (
|
|
149
|
-
return "
|
|
236
|
+
if (ticketAction !== "cancel" && params.force === true) {
|
|
237
|
+
return "force is valid only with ticketAction 'cancel'.";
|
|
238
|
+
}
|
|
239
|
+
if (ticketAction !== "wait" && params.timeoutMs !== undefined) {
|
|
240
|
+
return "timeoutMs is valid only with ticketAction 'wait'.";
|
|
150
241
|
}
|
|
151
242
|
return undefined;
|
|
152
243
|
}
|
|
153
244
|
|
|
154
245
|
if (params.ticket !== undefined) {
|
|
155
|
-
return "ticket requires
|
|
246
|
+
return "ticket requires ticketAction 'poll', 'cancel', or 'wait'.";
|
|
156
247
|
}
|
|
157
|
-
if (params.force === true)
|
|
248
|
+
if (params.force === true)
|
|
249
|
+
return "force is valid only with ticketAction 'cancel'.";
|
|
158
250
|
if (params.timeoutMs !== undefined) {
|
|
159
|
-
return "timeoutMs is valid only with
|
|
251
|
+
return "timeoutMs is valid only with ticketAction 'wait'.";
|
|
160
252
|
}
|
|
161
253
|
if (!tasks.length) {
|
|
162
254
|
return params.async === true
|
|
@@ -164,22 +256,88 @@ export function validateDelegateOperation(
|
|
|
164
256
|
: undefined; // Intentional help request.
|
|
165
257
|
}
|
|
166
258
|
|
|
259
|
+
// Reject mixed shapes: flat task fields at the top level alongside a
|
|
260
|
+
// nonempty tasks array. The normalize shim only wraps flat fields when
|
|
261
|
+
// there is no tasks array, so a mixed call silently lets tasks win —
|
|
262
|
+
// a model mistake that should fail loudly.
|
|
263
|
+
if (tasks.length > 0) {
|
|
264
|
+
const flatTaskFields = [
|
|
265
|
+
...new Set([...TASK_FIELD_NAMES, "sessionAction", "action"]),
|
|
266
|
+
].filter((field) => rawParams[field] !== undefined);
|
|
267
|
+
if (flatTaskFields.length) {
|
|
268
|
+
return `cannot mix top-level task field(s) ${flatTaskFields
|
|
269
|
+
.map((field) => `'${field}'`)
|
|
270
|
+
.join(
|
|
271
|
+
", ",
|
|
272
|
+
)} with an explicit tasks array; move them into a task entry or remove tasks.`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
167
276
|
for (const [index, task] of tasks.entries()) {
|
|
168
|
-
|
|
277
|
+
const rawTask = task as Record<string, unknown>;
|
|
278
|
+
const hasLegacyTaskAction = typeof rawTask.action === "string";
|
|
279
|
+
const hasSessionAction = task.sessionAction !== undefined;
|
|
280
|
+
|
|
281
|
+
if (hasLegacyTaskAction) {
|
|
282
|
+
if (!TASK_ACTIONS.has(rawTask.action as string)) {
|
|
283
|
+
return `task ${index + 1}: unknown action '${rawTask.action}'; valid session actions are prompt/close/list.`;
|
|
284
|
+
}
|
|
285
|
+
if (hasSessionAction) {
|
|
286
|
+
return `task ${index + 1}: ambiguous: supply only sessionAction (or only legacy action), not both.`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const sessionAction: string | undefined =
|
|
291
|
+
task.sessionAction ??
|
|
292
|
+
(hasLegacyTaskAction && TASK_ACTIONS.has(rawTask.action as string)
|
|
293
|
+
? (rawTask.action as string)
|
|
294
|
+
: undefined);
|
|
295
|
+
|
|
296
|
+
const unknownKeys = Object.keys(rawTask).filter((key) => {
|
|
297
|
+
if (VALID_TASK_KEYS.has(key)) return false;
|
|
298
|
+
if (key === "action" && sessionAction !== undefined) return false;
|
|
299
|
+
return true;
|
|
300
|
+
});
|
|
301
|
+
if (unknownKeys.length) {
|
|
302
|
+
const asyncHint = unknownKeys.includes("async")
|
|
303
|
+
? " 'async' is a top-level flag; move it out of the task entry."
|
|
304
|
+
: "";
|
|
305
|
+
return (
|
|
306
|
+
`task ${index + 1}: unknown field(s) ${unknownKeys
|
|
307
|
+
.map((key) => `'${key}'`)
|
|
308
|
+
.join(", ")}.${asyncHint} ` +
|
|
309
|
+
`Valid task fields: ${[...VALID_TASK_KEYS].join(", ")}.`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (rawTask.id !== undefined) {
|
|
313
|
+
if (typeof rawTask.id !== "string" || !TASK_ID_RE.test(rawTask.id)) {
|
|
314
|
+
return `task ${index + 1}: id must be 1-64 characters using only A-Z, a-z, 0-9, '.', '_', or '-'.`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (typeof rawTask.deadlineMs === "number" && !(rawTask.deadlineMs > 0)) {
|
|
318
|
+
return `task ${index + 1}: deadlineMs must be a positive number of milliseconds.`;
|
|
319
|
+
}
|
|
320
|
+
if (sessionAction === "close") {
|
|
169
321
|
if (!task.sessionId) {
|
|
170
|
-
return `task ${index + 1}:
|
|
322
|
+
return `task ${index + 1}: sessionAction 'close' requires sessionId.`;
|
|
171
323
|
}
|
|
172
|
-
const extras = Object.keys(
|
|
173
|
-
(key) =>
|
|
324
|
+
const extras = Object.keys(rawTask).filter(
|
|
325
|
+
(key) =>
|
|
326
|
+
key !== "sessionAction" &&
|
|
327
|
+
key !== "sessionId" &&
|
|
328
|
+
key !== "action" &&
|
|
329
|
+
key !== "id",
|
|
174
330
|
);
|
|
175
331
|
if (extras.length) {
|
|
176
|
-
return `task ${index + 1}:
|
|
332
|
+
return `task ${index + 1}: sessionAction 'close' accepts only sessionAction and sessionId.`;
|
|
177
333
|
}
|
|
178
334
|
}
|
|
179
|
-
if (
|
|
180
|
-
const extras = Object.keys(
|
|
335
|
+
if (sessionAction === "list") {
|
|
336
|
+
const extras = Object.keys(rawTask).filter(
|
|
337
|
+
(key) => key !== "sessionAction" && key !== "action" && key !== "id",
|
|
338
|
+
);
|
|
181
339
|
if (extras.length) {
|
|
182
|
-
return `task ${index + 1}:
|
|
340
|
+
return `task ${index + 1}: sessionAction 'list' accepts only sessionAction.`;
|
|
183
341
|
}
|
|
184
342
|
}
|
|
185
343
|
}
|
|
@@ -187,26 +345,6 @@ export function validateDelegateOperation(
|
|
|
187
345
|
return undefined;
|
|
188
346
|
}
|
|
189
347
|
|
|
190
|
-
/** Fields that belong to a task entry. Models sometimes place these at the
|
|
191
|
-
* top level of the arguments; the shim folds them back into a single task. */
|
|
192
|
-
const TASK_FIELD_NAMES = [
|
|
193
|
-
"prompt",
|
|
194
|
-
"agent",
|
|
195
|
-
"cwd",
|
|
196
|
-
"systemPrompt",
|
|
197
|
-
"context",
|
|
198
|
-
"model",
|
|
199
|
-
"tools",
|
|
200
|
-
"thinking",
|
|
201
|
-
"sessionId",
|
|
202
|
-
"resumeFrom",
|
|
203
|
-
] as const;
|
|
204
|
-
|
|
205
|
-
/** Actions that are only valid at task level. The top-level `action` is
|
|
206
|
-
* ticket-scoped (poll/cancel/wait), so a flat close/list/prompt belongs to
|
|
207
|
-
* the wrapped task. */
|
|
208
|
-
const TASK_ACTIONS = new Set(["prompt", "close", "list"]);
|
|
209
|
-
|
|
210
348
|
function parseStringifiedArray(value: string): unknown[] | undefined {
|
|
211
349
|
try {
|
|
212
350
|
const parsed: unknown = JSON.parse(value);
|
|
@@ -233,9 +371,14 @@ function normalizeToolsField(value: string): unknown {
|
|
|
233
371
|
* - `tasks` as a JSON string instead of an array;
|
|
234
372
|
* - task fields (`prompt`, `systemPrompt`, `tools`, ...) placed at the top
|
|
235
373
|
* level instead of inside a `tasks` entry — wrapped into a single task;
|
|
236
|
-
* - `tools` as a JSON string (or bare token) inside a task entry
|
|
237
|
-
*
|
|
238
|
-
*
|
|
374
|
+
* - `tools` as a JSON string (or bare token) inside a task entry;
|
|
375
|
+
* - `agent: ""` inside a task entry — treated as omitted (ad-hoc);
|
|
376
|
+
* - legacy `action` folded into `ticketAction` (top level) or `sessionAction`
|
|
377
|
+
* (per task) for runtime compatibility.
|
|
378
|
+
* Skipped when a ticket action is in play. Conflicts between the legacy
|
|
379
|
+
* `action` field and its canonical replacement are left for
|
|
380
|
+
* `validateDelegateOperation` to report. All other invalid input is left for
|
|
381
|
+
* normal schema validation to reject loudly.
|
|
239
382
|
*
|
|
240
383
|
* Silent by design: these rewrites are lossless re-shaping, so unlike the
|
|
241
384
|
* model-suffix warning in task-resolution (which fires because thinking
|
|
@@ -252,11 +395,26 @@ export function normalizeDelegateArguments(args: unknown): DelegateArguments {
|
|
|
252
395
|
if (parsed) record.tasks = parsed;
|
|
253
396
|
}
|
|
254
397
|
|
|
398
|
+
// Legacy top-level `action` (ticket verb) → canonical `ticketAction`.
|
|
399
|
+
// If both are present, leave the conflict for validateDelegateOperation.
|
|
400
|
+
if (
|
|
401
|
+
typeof record.action === "string" &&
|
|
402
|
+
["poll", "cancel", "wait"].includes(record.action)
|
|
403
|
+
) {
|
|
404
|
+
if (record.ticketAction === undefined) {
|
|
405
|
+
record.ticketAction = record.action;
|
|
406
|
+
delete record.action;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
255
410
|
// Flat task fields at the top level → wrap into a single task. Only fires
|
|
256
411
|
// when there is no usable tasks array and no ticket action (`ticket`,
|
|
257
412
|
// poll/cancel/wait) — those calls are legitimately taskless.
|
|
258
413
|
const hasTasks = Array.isArray(record.tasks) && record.tasks.length > 0;
|
|
259
414
|
const isTicketAction =
|
|
415
|
+
record.ticketAction === "poll" ||
|
|
416
|
+
record.ticketAction === "cancel" ||
|
|
417
|
+
record.ticketAction === "wait" ||
|
|
260
418
|
record.action === "poll" ||
|
|
261
419
|
record.action === "cancel" ||
|
|
262
420
|
record.action === "wait";
|
|
@@ -268,20 +426,51 @@ export function normalizeDelegateArguments(args: unknown): DelegateArguments {
|
|
|
268
426
|
delete record[key];
|
|
269
427
|
}
|
|
270
428
|
}
|
|
429
|
+
// Canonical `sessionAction` at the top level folds into the wrapped task.
|
|
430
|
+
if (typeof record.sessionAction === "string") {
|
|
431
|
+
if (task.sessionAction === undefined) {
|
|
432
|
+
task.sessionAction = record.sessionAction;
|
|
433
|
+
}
|
|
434
|
+
delete record.sessionAction;
|
|
435
|
+
}
|
|
436
|
+
// Legacy top-level session `action` folds into the wrapped task's
|
|
437
|
+
// `sessionAction`. A conflict with an explicit `sessionAction` is left
|
|
438
|
+
// for validateDelegateOperation to report.
|
|
271
439
|
if (typeof record.action === "string" && TASK_ACTIONS.has(record.action)) {
|
|
272
|
-
task.
|
|
440
|
+
if (task.sessionAction === undefined) {
|
|
441
|
+
task.sessionAction = record.action;
|
|
442
|
+
} else {
|
|
443
|
+
task.action = record.action;
|
|
444
|
+
}
|
|
273
445
|
delete record.action;
|
|
274
446
|
}
|
|
275
447
|
if (Object.keys(task).length > 0) record.tasks = [task];
|
|
276
448
|
}
|
|
277
449
|
|
|
278
|
-
//
|
|
450
|
+
// Per-entry recovery: stringified (or bare-token) `tools` → real arrays,
|
|
451
|
+
// `agent: ""` → omitted, and legacy `action` → `sessionAction`.
|
|
279
452
|
if (Array.isArray(record.tasks)) {
|
|
280
453
|
record.tasks = record.tasks.map((entry: unknown) => {
|
|
281
454
|
if (!entry || typeof entry !== "object") return entry;
|
|
282
455
|
const e = entry as Record<string, unknown>;
|
|
283
|
-
|
|
284
|
-
|
|
456
|
+
const rawTools = e.tools;
|
|
457
|
+
const fixAgent = e.agent === "";
|
|
458
|
+
const needsActionNorm =
|
|
459
|
+
typeof e.action === "string" &&
|
|
460
|
+
TASK_ACTIONS.has(e.action) &&
|
|
461
|
+
e.sessionAction === undefined;
|
|
462
|
+
if (typeof rawTools !== "string" && !fixAgent && !needsActionNorm)
|
|
463
|
+
return entry;
|
|
464
|
+
const out = { ...e };
|
|
465
|
+
if (typeof rawTools === "string") {
|
|
466
|
+
out.tools = normalizeToolsField(rawTools);
|
|
467
|
+
}
|
|
468
|
+
if (fixAgent) delete out.agent;
|
|
469
|
+
if (needsActionNorm) {
|
|
470
|
+
out.sessionAction = out.action;
|
|
471
|
+
delete out.action;
|
|
472
|
+
}
|
|
473
|
+
return out;
|
|
285
474
|
});
|
|
286
475
|
}
|
|
287
476
|
|