@bermudi/pi-delegate 0.1.18 → 0.1.19
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 +14 -13
- package/agents.ts +1 -1
- package/concurrency.ts +7 -0
- package/delegate.ts +6 -0
- package/dispatch.ts +460 -163
- package/extension.ts +35 -26
- package/format.ts +4 -1
- package/host.ts +1 -1
- package/isolated-workspace.ts +154 -8
- package/lifecycle.ts +31 -20
- package/manual.ts +11 -7
- package/package.json +2 -1
- package/parent-context.ts +1 -1
- package/pool.ts +492 -428
- package/render-branches.ts +2 -1
- package/render-result.ts +6 -0
- package/runtime.ts +36 -0
- package/schema.ts +22 -13
- package/status.ts +24 -11
- package/task-resolution.ts +12 -9
- package/test-harness.ts +81 -0
- package/ticket-format.ts +4 -3
- package/tickets.ts +672 -576
- package/types.ts +19 -0
- package/workspace.ts +58 -27
package/render-branches.ts
CHANGED
|
@@ -523,7 +523,8 @@ export function renderFinalBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
523
523
|
const integration = r.integration;
|
|
524
524
|
const tone =
|
|
525
525
|
integration.status === "applied_unverified" ||
|
|
526
|
-
integration.status === "no_changes"
|
|
526
|
+
integration.status === "no_changes" ||
|
|
527
|
+
integration.status === "retained"
|
|
527
528
|
? "warning"
|
|
528
529
|
: "error";
|
|
529
530
|
lines.push(
|
package/render-result.ts
CHANGED
|
@@ -213,6 +213,12 @@ export function renderDelegateResult(
|
|
|
213
213
|
lines.push(truncLine(theme.fg("warning", `⚠ ${warning}`), w), "");
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
+
if (details?.serializedNotice) {
|
|
217
|
+
const notice = sanitizeTerminalLine(details.serializedNotice);
|
|
218
|
+
if (notice) {
|
|
219
|
+
lines.push(truncLine(theme.fg("dim", `⏳ ${notice}`), w), "");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
216
222
|
if (details?.overlapWarning) {
|
|
217
223
|
const warning = sanitizeTerminalLine(details.overlapWarning);
|
|
218
224
|
if (warning) {
|
package/runtime.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { SessionPool, defaultSessionPool } from "./pool.ts";
|
|
2
|
+
import { TicketRegistry, ticketRegistry } from "./tickets.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Injectable runtime context for one delegate extension lifetime.
|
|
6
|
+
*
|
|
7
|
+
* A runtime bundles a {@link SessionPool} and a {@link TicketRegistry} so
|
|
8
|
+
* tests and nested callers can create fully isolated dispatch/lifecycle
|
|
9
|
+
* environments without touching the module-level default pool or ticket
|
|
10
|
+
* registry. Production Pi uses the single default runtime returned by
|
|
11
|
+
* {@link getDefaultDelegateRuntime}; the public barrel still exposes the
|
|
12
|
+
* familiar checkout/commit/ticket wrapper functions for compatibility.
|
|
13
|
+
*/
|
|
14
|
+
export interface DelegateRuntime {
|
|
15
|
+
pool: SessionPool;
|
|
16
|
+
tickets: TicketRegistry;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Create a fresh, isolated runtime with its own pool and ticket registry. */
|
|
20
|
+
export function createDelegateRuntime(): DelegateRuntime {
|
|
21
|
+
return {
|
|
22
|
+
pool: new SessionPool(),
|
|
23
|
+
tickets: new TicketRegistry(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const defaultRuntime: DelegateRuntime = {
|
|
28
|
+
pool: defaultSessionPool,
|
|
29
|
+
tickets: ticketRegistry,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The runtime used by the one-argument Pi extension entry point and the
|
|
33
|
+
* default-runtime compatibility wrappers exported from the barrel. */
|
|
34
|
+
export function getDefaultDelegateRuntime(): DelegateRuntime {
|
|
35
|
+
return defaultRuntime;
|
|
36
|
+
}
|
package/schema.ts
CHANGED
|
@@ -42,7 +42,7 @@ export const delegateTaskSchema = Type.Object({
|
|
|
42
42
|
agent: Type.Optional(
|
|
43
43
|
Type.String({
|
|
44
44
|
description:
|
|
45
|
-
"
|
|
45
|
+
"Use built-ins first: default=general; scout/coder/reviewer specialize. Omit only for inline; it gets * even if parent is narrower.",
|
|
46
46
|
}),
|
|
47
47
|
),
|
|
48
48
|
cwd: Type.Optional(
|
|
@@ -65,19 +65,20 @@ export const delegateTaskSchema = Type.Object({
|
|
|
65
65
|
),
|
|
66
66
|
model: Type.Optional(
|
|
67
67
|
Type.String({
|
|
68
|
-
description:
|
|
68
|
+
description:
|
|
69
|
+
"Override only if requested or required; else keep its default.",
|
|
69
70
|
}),
|
|
70
71
|
),
|
|
71
72
|
tools: Type.Optional(
|
|
72
73
|
Type.Array(Type.String(), {
|
|
73
74
|
description:
|
|
74
|
-
"
|
|
75
|
+
"Override only if requested or required. *=read/write/edit/bash; ro=read/grep/find/ls (read-only).",
|
|
75
76
|
}),
|
|
76
77
|
),
|
|
77
78
|
thinking: Type.Optional(
|
|
78
79
|
StringEnum(VALID_THINKING_LEVELS, {
|
|
79
80
|
description:
|
|
80
|
-
"off/minimal/low/medium/high/xhigh/max. Omit
|
|
81
|
+
"off/minimal/low/medium/high/xhigh/max. Omit by default: overrides the agent's configured thinking budget.",
|
|
81
82
|
}),
|
|
82
83
|
),
|
|
83
84
|
sessionId: Type.Optional(
|
|
@@ -101,7 +102,7 @@ export const delegateTaskSchema = Type.Object({
|
|
|
101
102
|
workspace: Type.Optional(
|
|
102
103
|
StringEnum(["shared", "scratch", "isolated"], {
|
|
103
104
|
description:
|
|
104
|
-
"shared
|
|
105
|
+
"shared/scratch/isolated. Override if requested or required. scratch discards; isolated reconciles Git; not a security boundary.",
|
|
105
106
|
}),
|
|
106
107
|
),
|
|
107
108
|
});
|
|
@@ -159,7 +160,7 @@ export const delegateArgumentsSchema = Type.Object(
|
|
|
159
160
|
Type.Array(delegateTaskSchema, {
|
|
160
161
|
minItems: 0,
|
|
161
162
|
description:
|
|
162
|
-
"Tasks run concurrently; shared
|
|
163
|
+
"Use built-in defaults first. Tasks run concurrently; overlapping shared writers run in task order. []=manual.",
|
|
163
164
|
}),
|
|
164
165
|
),
|
|
165
166
|
},
|
|
@@ -286,9 +287,21 @@ function validateTicketMode(params: DelegateArguments): string | undefined {
|
|
|
286
287
|
[...TASK_FIELD_NAMES, "tasks", "sessionAction"] as const
|
|
287
288
|
).filter((field) => rawParams[field] !== undefined);
|
|
288
289
|
if (incompatibleFields.length) {
|
|
289
|
-
|
|
290
|
+
const base = `ticket control cannot be combined with field(s) ${incompatibleFields
|
|
290
291
|
.map((field) => `'${field}'`)
|
|
291
292
|
.join(", ")}; call it separately.`;
|
|
293
|
+
// Kitchen-sink callers attach default-shaped ticket control to a real
|
|
294
|
+
// dispatch and then repeat the identical rejected call (observed in the
|
|
295
|
+
// wild: glm-5.3 sent `ticketAction:"wait"` + tasks + empty ticket six
|
|
296
|
+
// times in a row). With tasks present and no ticket id, address the
|
|
297
|
+
// likely intent — dispatch already blocks; async creates the tickets
|
|
298
|
+
// that ticketAction manages — instead of restating the field rule.
|
|
299
|
+
const wantsDispatch =
|
|
300
|
+
Array.isArray(params.tasks) && params.tasks.length > 0 && !params.ticket;
|
|
301
|
+
if (wantsDispatch) {
|
|
302
|
+
return `${base} Dispatched tasks run to completion before returning — omit ticketAction entirely; only async:true produces a ticket to wait on.`;
|
|
303
|
+
}
|
|
304
|
+
return base;
|
|
292
305
|
}
|
|
293
306
|
if (params.async === true) {
|
|
294
307
|
return "ticket control cannot include async; call it separately.";
|
|
@@ -327,10 +340,6 @@ function validateDispatchOrHelpMode(
|
|
|
327
340
|
: undefined; // Intentional help request.
|
|
328
341
|
}
|
|
329
342
|
|
|
330
|
-
if (params.async && tasks.some((task) => task.workspace === "isolated")) {
|
|
331
|
-
return 'workspace "isolated" is synchronous; remove async.';
|
|
332
|
-
}
|
|
333
|
-
|
|
334
343
|
// Reject mixed shapes: flat task fields at the top level alongside a
|
|
335
344
|
// nonempty tasks array. The normalize shim only wraps flat fields when
|
|
336
345
|
// there is no tasks array, so a mixed call silently lets tasks win —
|
|
@@ -450,7 +459,7 @@ function wrapFlatTaskFields(record: Record<string, unknown>): void {
|
|
|
450
459
|
}
|
|
451
460
|
|
|
452
461
|
/** Per-entry recovery for one task: stringified (or bare-token) `tools` → a
|
|
453
|
-
* real array, and `agent: ""` → omitted (
|
|
462
|
+
* real array, and `agent: ""` → omitted (inline). Other malformed input is
|
|
454
463
|
* left for schema validation to reject loudly. */
|
|
455
464
|
function normalizeTaskEntry(entry: unknown): unknown {
|
|
456
465
|
if (!entry || typeof entry !== "object") return entry;
|
|
@@ -476,7 +485,7 @@ function normalizeTaskEntry(entry: unknown): unknown {
|
|
|
476
485
|
* unless ticket- or session-control intent makes the call legitimately
|
|
477
486
|
* taskless (see `hasTicketControlIntent` / `hasSessionControlIntent`);
|
|
478
487
|
* - `tools` as a JSON string (or bare token) inside a task entry;
|
|
479
|
-
* - `agent: ""` inside a task entry — treated as omitted (
|
|
488
|
+
* - `agent: ""` inside a task entry — treated as omitted (inline);
|
|
480
489
|
* All other invalid input is left for normal schema validation to reject
|
|
481
490
|
* loudly.
|
|
482
491
|
*
|
package/status.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* there.
|
|
27
27
|
*/
|
|
28
28
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
29
|
-
import {
|
|
29
|
+
import { getDefaultDelegateRuntime, type DelegateRuntime } from "./runtime.ts";
|
|
30
30
|
import type { AsyncTicket } from "./types.ts";
|
|
31
31
|
|
|
32
32
|
const STATUS_KEY = "delegate";
|
|
@@ -39,10 +39,12 @@ export interface ActiveTicketSummary {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** Snapshot the live background work from the ticket registry. */
|
|
42
|
-
export function activeTicketSummary(
|
|
42
|
+
export function activeTicketSummary(
|
|
43
|
+
runtime: DelegateRuntime = getDefaultDelegateRuntime(),
|
|
44
|
+
): ActiveTicketSummary {
|
|
43
45
|
const tickets: AsyncTicket[] = [];
|
|
44
46
|
let activeSubagents = 0;
|
|
45
|
-
for (const ticket of
|
|
47
|
+
for (const ticket of runtime.tickets.values()) {
|
|
46
48
|
if (ticket.status !== "running" && ticket.status !== "cancelling") continue;
|
|
47
49
|
tickets.push(ticket);
|
|
48
50
|
activeSubagents += ticket.progress.filter(
|
|
@@ -88,10 +90,13 @@ const settledWarnedTicketIds = new Set<string>();
|
|
|
88
90
|
* Called on every ticket lifecycle mutation (create, progress, complete,
|
|
89
91
|
* cancel); event-driven only — no timers, so the text never goes stale
|
|
90
92
|
* (counts are the only content). */
|
|
91
|
-
export function syncDelegateStatus(
|
|
93
|
+
export function syncDelegateStatus(
|
|
94
|
+
ctx?: ExtensionContext,
|
|
95
|
+
runtime?: DelegateRuntime,
|
|
96
|
+
): void {
|
|
92
97
|
if (ctx) lastCtx = ctx;
|
|
93
98
|
|
|
94
|
-
const summary = activeTicketSummary();
|
|
99
|
+
const summary = activeTicketSummary(runtime);
|
|
95
100
|
const text = buildStatusText(summary);
|
|
96
101
|
|
|
97
102
|
if (settledWarnedTicketIds.size) {
|
|
@@ -127,9 +132,12 @@ export function clearDelegateStatusContext(): void {
|
|
|
127
132
|
/** Warn once per ticket at the first agent_settled with that ticket active —
|
|
128
133
|
* the "looks idle but isn't" moment. The persistent footer status carries
|
|
129
134
|
* the information from then on, so later settles stay quiet. */
|
|
130
|
-
export function notifyActiveTicketsOnSettled(
|
|
135
|
+
export function notifyActiveTicketsOnSettled(
|
|
136
|
+
ctx: ExtensionContext,
|
|
137
|
+
runtime?: DelegateRuntime,
|
|
138
|
+
): void {
|
|
131
139
|
lastCtx = ctx;
|
|
132
|
-
const summary = activeTicketSummary();
|
|
140
|
+
const summary = activeTicketSummary(runtime);
|
|
133
141
|
const fresh = summary.tickets.filter(
|
|
134
142
|
(t) => !settledWarnedTicketIds.has(t.id),
|
|
135
143
|
);
|
|
@@ -167,9 +175,10 @@ export function notifyActiveTicketsOnSettled(ctx: ExtensionContext): void {
|
|
|
167
175
|
export async function guardSessionReplacement(
|
|
168
176
|
ctx: ExtensionContext,
|
|
169
177
|
action: "switch" | "fork",
|
|
178
|
+
runtime?: DelegateRuntime,
|
|
170
179
|
): Promise<{ cancel: true } | undefined> {
|
|
171
180
|
lastCtx = ctx;
|
|
172
|
-
const summary = activeTicketSummary();
|
|
181
|
+
const summary = activeTicketSummary(runtime);
|
|
173
182
|
if (!summary.tickets.length || !ctx.hasUI) return undefined;
|
|
174
183
|
|
|
175
184
|
const ids = summary.tickets.map((t) => t.id).join(", ");
|
|
@@ -194,9 +203,11 @@ export async function guardSessionReplacement(
|
|
|
194
203
|
* extension) is still handled at delivery time via leaf affinity. */
|
|
195
204
|
export async function guardTreeNavigation(
|
|
196
205
|
ctx: ExtensionContext,
|
|
206
|
+
runtime?: DelegateRuntime,
|
|
197
207
|
): Promise<{ cancel: true } | undefined> {
|
|
198
208
|
lastCtx = ctx;
|
|
199
|
-
const
|
|
209
|
+
const rt = runtime ?? getDefaultDelegateRuntime();
|
|
210
|
+
const summary = activeTicketSummary(rt);
|
|
200
211
|
if (!summary.tickets.length || !ctx.hasUI) return undefined;
|
|
201
212
|
|
|
202
213
|
const ids = summary.tickets.map((t) => t.id).join(", ");
|
|
@@ -220,8 +231,10 @@ export async function guardTreeNavigation(
|
|
|
220
231
|
|
|
221
232
|
if (choice === hold) return undefined;
|
|
222
233
|
if (choice === cancel) {
|
|
223
|
-
for (const ticket of summary.tickets)
|
|
224
|
-
|
|
234
|
+
for (const ticket of summary.tickets) {
|
|
235
|
+
rt.tickets.requestTicketCancel(ticket);
|
|
236
|
+
}
|
|
237
|
+
syncDelegateStatus(ctx, rt);
|
|
225
238
|
return undefined;
|
|
226
239
|
}
|
|
227
240
|
return { cancel: true };
|
package/task-resolution.ts
CHANGED
|
@@ -12,8 +12,7 @@ import {
|
|
|
12
12
|
availableToolNames,
|
|
13
13
|
resolveToolGroups,
|
|
14
14
|
} from "./tools.ts";
|
|
15
|
-
import {
|
|
16
|
-
import { isSessionBusy } from "./tickets.ts";
|
|
15
|
+
import { getDefaultDelegateRuntime, type DelegateRuntime } from "./runtime.ts";
|
|
17
16
|
import {
|
|
18
17
|
isResumeFromQuarantined,
|
|
19
18
|
isSessionIdQuarantined,
|
|
@@ -115,6 +114,7 @@ export function validateTasks(
|
|
|
115
114
|
tasks: DispatchableTask[],
|
|
116
115
|
agents: Map<string, AgentConfig>,
|
|
117
116
|
parentModelId: string | undefined,
|
|
117
|
+
runtime: DelegateRuntime = getDefaultDelegateRuntime(),
|
|
118
118
|
): DelegateToolResult | null {
|
|
119
119
|
const unknown: string[] = [];
|
|
120
120
|
for (const task of tasks) {
|
|
@@ -203,7 +203,7 @@ export function validateTasks(
|
|
|
203
203
|
// Disallow sessionIds already claimed by a running async ticket.
|
|
204
204
|
const busyConflicts: string[] = [];
|
|
205
205
|
for (const sid of sessionIds) {
|
|
206
|
-
const owner = isSessionBusy(sid);
|
|
206
|
+
const owner = runtime.tickets.isSessionBusy(sid);
|
|
207
207
|
if (owner) busyConflicts.push(`${sid} (ticket ${owner})`);
|
|
208
208
|
}
|
|
209
209
|
if (busyConflicts.length) {
|
|
@@ -247,6 +247,7 @@ export function resolveTasks(
|
|
|
247
247
|
agents: Map<string, AgentConfig>,
|
|
248
248
|
parentDefaults: ParentAgentDefaults,
|
|
249
249
|
dispatchConfig: DelegateConfig = getDelegateConfigSnapshot(),
|
|
250
|
+
runtime: DelegateRuntime = getDefaultDelegateRuntime(),
|
|
250
251
|
): ResolveTasksResult {
|
|
251
252
|
// Build parent transcript lazily — only computed once if any task uses with-parent-transcript
|
|
252
253
|
let parentTranscript: string | null = null;
|
|
@@ -302,10 +303,12 @@ export function resolveTasks(
|
|
|
302
303
|
: undefined;
|
|
303
304
|
|
|
304
305
|
// Build system prompt. Explicit task prompts and named agent prompts
|
|
305
|
-
// win;
|
|
306
|
+
// win; inline subagents inherit the parent's base prompt when Pi exposes
|
|
306
307
|
// it. The assembled parent project-context section was stripped above;
|
|
307
308
|
// the child ResourceLoader supplies context for this task's cwd.
|
|
308
|
-
const pooledConfig = t.sessionId
|
|
309
|
+
const pooledConfig = t.sessionId
|
|
310
|
+
? runtime.pool.configFor(t.sessionId)
|
|
311
|
+
: undefined;
|
|
309
312
|
const isPoolHit = pooledConfig !== undefined;
|
|
310
313
|
const parentNativeTools = parentDefaults.tools.filter((name) =>
|
|
311
314
|
Object.hasOwn(TOOL_FACTORIES, name),
|
|
@@ -625,12 +628,12 @@ export function resolveTasks(
|
|
|
625
628
|
// display code treats "" and absent alike (`t.prompt || …`).
|
|
626
629
|
prompt: prompt ?? "",
|
|
627
630
|
// Keep the built-in selector visible in progress/results. Omitted-agent
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
//
|
|
631
|
+
// tasks use the `inline` label and config namespace — except resumes: a
|
|
632
|
+
// continued transcript is not a fresh inline spawn, so it carries the
|
|
633
|
+
// resumed-transcript identity instead.
|
|
631
634
|
agentName:
|
|
632
635
|
agent?.name ??
|
|
633
|
-
(resumeFromDisplay ? `resume:${resumeFromDisplay}` : "
|
|
636
|
+
(resumeFromDisplay ? `resume:${resumeFromDisplay}` : "inline"),
|
|
634
637
|
resumeFromDisplay,
|
|
635
638
|
warnings,
|
|
636
639
|
reuseIntent: {
|
package/test-harness.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTestSession,
|
|
3
|
+
type TestSession,
|
|
4
|
+
type TestSessionOptions,
|
|
5
|
+
} from "@marcfargas/pi-test-harness";
|
|
6
|
+
import type {
|
|
7
|
+
AgentSession,
|
|
8
|
+
AgentToolResult,
|
|
9
|
+
ExtensionContext,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
12
|
+
import type { DelegateDetails, TaskResult } from "./types.ts";
|
|
13
|
+
import { resolve } from "node:path";
|
|
14
|
+
|
|
15
|
+
export const DELEGATE_EXTENSION = resolve(import.meta.dirname, "./delegate.ts");
|
|
16
|
+
|
|
17
|
+
export { type TestSession };
|
|
18
|
+
|
|
19
|
+
export function createDelegateTestSession(
|
|
20
|
+
options: TestSessionOptions = {},
|
|
21
|
+
): Promise<TestSession> {
|
|
22
|
+
return createTestSession({
|
|
23
|
+
...options,
|
|
24
|
+
extensions: [...(options.extensions ?? []), DELEGATE_EXTENSION],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TestToolDefinition {
|
|
29
|
+
name: string;
|
|
30
|
+
label: string;
|
|
31
|
+
description: string;
|
|
32
|
+
promptSnippet?: string;
|
|
33
|
+
promptGuidelines?: string[];
|
|
34
|
+
parameters: unknown;
|
|
35
|
+
prepareArguments?: (args: unknown) => unknown;
|
|
36
|
+
execute(...args: unknown[]): Promise<AgentToolResult<DelegateDetails>>;
|
|
37
|
+
renderCall: (...args: unknown[]) => Component;
|
|
38
|
+
renderResult: (...args: unknown[]) => Component;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getToolDef(ts: TestSession, name: string): TestToolDefinition {
|
|
42
|
+
const tool = (ts.session as AgentSession).extensionRunner.getToolDefinition(
|
|
43
|
+
name,
|
|
44
|
+
);
|
|
45
|
+
if (!tool) throw new Error(`${name} tool not found`);
|
|
46
|
+
return tool as unknown as TestToolDefinition;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getDelegateTool(ts: TestSession): TestToolDefinition {
|
|
50
|
+
return getToolDef(ts, "delegate");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getExecContext(ts: TestSession): ExtensionContext {
|
|
54
|
+
return (ts.session as AgentSession).extensionRunner.createContext();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function firstText(result: { content: readonly unknown[] }): string {
|
|
58
|
+
const content = result.content[0];
|
|
59
|
+
if (
|
|
60
|
+
!content ||
|
|
61
|
+
typeof content !== "object" ||
|
|
62
|
+
!("type" in content) ||
|
|
63
|
+
content.type !== "text" ||
|
|
64
|
+
!("text" in content) ||
|
|
65
|
+
typeof content.text !== "string"
|
|
66
|
+
) {
|
|
67
|
+
throw new Error("Expected first tool result content item to be text");
|
|
68
|
+
}
|
|
69
|
+
return content.text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function taskResultAt(
|
|
73
|
+
results: readonly (TaskResult | { error: string })[],
|
|
74
|
+
index: number,
|
|
75
|
+
): TaskResult {
|
|
76
|
+
const result = results[index];
|
|
77
|
+
if (!result || !("agent" in result)) {
|
|
78
|
+
throw new Error(`Expected task result at index ${index}`);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
package/ticket-format.ts
CHANGED
|
@@ -246,15 +246,16 @@ export function formatLiveTicketPoll(
|
|
|
246
246
|
findTouchedOverlaps(completedForOverlap),
|
|
247
247
|
);
|
|
248
248
|
const guidance = liveTicketGuidance(ticket);
|
|
249
|
+
const serializedNotice = ticket.serializedNotice ?? "";
|
|
249
250
|
const dispatchWarning = ticket.dispatchWarning
|
|
250
251
|
? `WARNING: ${ticket.dispatchWarning}`
|
|
251
252
|
: "";
|
|
252
253
|
return {
|
|
253
254
|
text: `${formatLiveTicketHeader(ticket, now)}\n${lines.join("\n")}${
|
|
254
255
|
guidance ? `\n\n${guidance}` : ""
|
|
255
|
-
}${
|
|
256
|
-
|
|
257
|
-
}`,
|
|
256
|
+
}${serializedNotice ? `\n\n${serializedNotice}` : ""}${
|
|
257
|
+
dispatchWarning ? `\n\n${dispatchWarning}` : ""
|
|
258
|
+
}${overlapWarning ? `\n\n${overlapWarning}` : ""}`,
|
|
258
259
|
completedResults,
|
|
259
260
|
overlapWarning,
|
|
260
261
|
};
|