@bermudi/pi-delegate 0.1.2 → 0.1.4

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/tickets.ts CHANGED
@@ -24,7 +24,8 @@ import {
24
24
  import { isCrossLeafTicket } from "./leaf.ts";
25
25
  import { renderOutputForPoll } from "./spill.ts";
26
26
  import { scheduleDeadline } from "./timer.ts";
27
- import { emptyUsage } from "./usage.ts";
27
+ import { aggregateTaskResults, emptyUsage } from "./usage.ts";
28
+ import { recordCall } from "./telemetry.ts";
28
29
  import type {
29
30
  AsyncTicket,
30
31
  DelegateDetails,
@@ -125,6 +126,19 @@ export function cancelTicketForShutdown(ticket: AsyncTicket): void {
125
126
  ticket.completedAt = Date.now();
126
127
  syncTicketBusyIndex(ticket);
127
128
  settleTicketWaiters(ticket);
129
+ if (ticket.callRecord) {
130
+ const { totalTokens, totalCost } = aggregateTaskResults(ticket.results);
131
+ recordCall(
132
+ {
133
+ ...ticket.callRecord,
134
+ status: "cancelled",
135
+ wall_ms: ticket.completedAt - (ticket.callStartedAt ?? ticket.created),
136
+ total_tokens: totalTokens,
137
+ total_cost: totalCost,
138
+ },
139
+ ticket.telemetryGeneration,
140
+ );
141
+ }
128
142
  }
129
143
 
130
144
  /** Request cooperative cancellation of a live ticket: abort the workers and
@@ -325,13 +339,25 @@ function buildWaitTimeoutResult(
325
339
  ticket: AsyncTicket,
326
340
  timeoutMs: number,
327
341
  ): AgentToolResult<DelegateDetails> {
328
- const details = buildWaitDetails(ticket);
342
+ // A timeout must not be an information cliff. Reuse the same rich snapshot
343
+ // as poll so the caller can see activity and consume any completed outputs
344
+ // without making a second tool call.
345
+ const snapshot = handlePoll({ ticket: ticket.id }, {} as ExtensionContext);
346
+ const snapshotText = snapshot.content
347
+ .filter((item) => item.type === "text")
348
+ .map((item) => item.text)
349
+ .join("\n");
329
350
  const base = `Ticket ${ticket.id} still ${ticket.status} after ${fmtDuration(timeoutMs)} · wait timed out (ticket continues in background)`;
330
- const text =
331
- base + (details.overlapWarning ? `\n\n${details.overlapWarning}` : "");
351
+ const guidance =
352
+ "If you need the final result in this turn, call wait once with timeoutMs omitted; do not poll after a timeout. Otherwise stop calling ticket controls and let the final result auto-deliver.";
332
353
  return {
333
- content: [{ type: "text", text }],
334
- details,
354
+ content: [
355
+ {
356
+ type: "text",
357
+ text: `${base}\n\n${snapshotText}\n\n${guidance}`,
358
+ },
359
+ ],
360
+ details: snapshot.details,
335
361
  };
336
362
  }
337
363
 
@@ -693,12 +719,10 @@ export function handlePoll(
693
719
  const header = headerParts.join(" · ");
694
720
  const guidance =
695
721
  ticket.status === "cancelling"
696
- ? "Cancellation requested. Active subagents are aborting and returning partial results; poll again for the final status."
722
+ ? "Cancellation requested. Active subagents are aborting and returning partial results. Wait without timeoutMs for final status; do not repeatedly poll."
697
723
  : settledCount === totalCount
698
724
  ? ""
699
- : settledCount > 0
700
- ? "Tasks are progressing. Do other work while remaining tasks finish — results will be delivered automatically when all complete."
701
- : "Tasks are still running. Do other work while you wait — polling again immediately will not speed them up. Results are delivered automatically when all tasks complete.";
725
+ : "If you need the final result in this turn, call wait once with timeoutMs omitted. Otherwise stop calling ticket controls and let the final result auto-deliver after this turn; repeated polling will not speed it up.";
702
726
 
703
727
  return {
704
728
  content: [
package/types.ts CHANGED
@@ -12,6 +12,7 @@ import type {
12
12
  } from "@earendil-works/pi-coding-agent";
13
13
  import type { Static } from "@sinclair/typebox";
14
14
  import type { delegateArgumentsSchema } from "./schema.ts";
15
+ import type { CallRecord } from "./telemetry.ts";
15
16
 
16
17
  export interface AgentConfig {
17
18
  name: string;
@@ -42,6 +43,8 @@ export type TicketAction = NonNullable<
42
43
  >;
43
44
  /** Per-task session action: "prompt" | "close" | "list". */
44
45
  export type SessionAction = NonNullable<CanonicalTaskDef["sessionAction"]>;
46
+ /** Filesystem mode: shared source tree or an ephemeral CoW scratch copy. */
47
+ export type WorkspaceMode = NonNullable<CanonicalTaskDef["workspace"]>;
45
48
 
46
49
  export type TaskDef = CanonicalTaskDef & {
47
50
  /** @deprecated Use `sessionAction` instead. Runtime normalization still accepts this alias. */
@@ -86,6 +89,16 @@ export interface AsyncTicket {
86
89
  spawnLeafId?: string | null;
87
90
  /** Active blocking waiters. Resolved by terminal delivery or timeout/abort. */
88
91
  waiters?: TicketWaiter[];
92
+ /** Telemetry call span id attached to this async ticket. */
93
+ callId?: string;
94
+ /** Telemetry call span start timestamp for accurate wall-time on cancellation. */
95
+ callStartedAt?: number;
96
+ /** Snapshot of the call row at spawn, used to write the cancelled/settled row. */
97
+ callRecord?: CallRecord;
98
+ /** Runtime generation for rejecting shutdown writes from stale tickets. */
99
+ telemetryGeneration?: number;
100
+ /** Resolves after every async worker has settled, including shutdown aborts. */
101
+ completion?: Promise<void>;
89
102
  }
90
103
 
91
104
  /** Live parent settings captured when a delegate call starts. The built-in
@@ -112,6 +125,7 @@ export interface ResolvedTask {
112
125
  thinking: ThinkingLevel;
113
126
  systemPrompt: string;
114
127
  cwd: string;
128
+ workspace?: WorkspaceMode;
115
129
  context?: "fresh" | "with-parent-transcript";
116
130
  sessionId?: string;
117
131
  sessionAction?: SessionAction;
@@ -200,6 +214,8 @@ export interface TaskResult {
200
214
  * stay 0 because `getSessionStats()` exposes only the aggregate cost — and
201
215
  * Pi sums `cost.total` for nested usage anyway. */
202
216
  usage: Usage;
217
+ /** Scratch results are excluded from shared-file conflict detection and never resumable. */
218
+ workspace?: WorkspaceMode;
203
219
  sessionFile?: string;
204
220
  /** All files the subagent is known to have touched, including bash mutations
205
221
  * captured via git diff and other tasks' concurrent git changes. This is a
@@ -245,6 +261,12 @@ export interface TaskRunEnv {
245
261
  onProgress: (p: TaskProgress, u: AgentProgressUpdate) => void;
246
262
  /** Called after every TaskProgress mutation (early-returns, completion). Sync uses this to fire onUpdate. */
247
263
  onStatusChange?: () => void;
264
+ /** Telemetry call id for this dispatch. undefined when telemetry is disabled or not started. */
265
+ telemetryCallId?: string;
266
+ /** Runtime generation for rejecting writes from a stale shutdown worker. */
267
+ telemetryGeneration?: number;
268
+ /** Whether this task is part of an async ticket. */
269
+ async?: boolean;
248
270
  }
249
271
 
250
272
  /** Structural subset of Pi's `ExtensionContext` used by delegate's
package/usage.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
2
  import type { Usage } from "@earendil-works/pi-ai";
3
+ import type { TaskResult } from "./types.ts";
3
4
 
4
5
  /** Snapshot of the cumulative session usage fields we read for delta accounting. */
5
6
  export interface SessionUsageSnapshot {
@@ -119,3 +120,21 @@ export function sumUsage(usages: readonly (Usage | undefined)[]): Usage {
119
120
  emptyUsage(),
120
121
  );
121
122
  }
123
+
124
+ /** Aggregate the completed result rows used by async call telemetry. */
125
+ export function aggregateTaskResults(
126
+ results: readonly (TaskResult | undefined)[],
127
+ ): { totalTokens: number; totalCost: number } {
128
+ return results
129
+ .filter(
130
+ (result): result is TaskResult =>
131
+ result !== undefined && "touchedFiles" in result,
132
+ )
133
+ .reduce(
134
+ (total, result) => ({
135
+ totalTokens: total.totalTokens + result.tokens,
136
+ totalCost: total.totalCost + (result.usage?.cost?.total ?? 0),
137
+ }),
138
+ { totalTokens: 0, totalCost: 0 },
139
+ );
140
+ }