@bermudi/pi-delegate 0.1.2 → 0.1.3

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;
@@ -86,6 +87,16 @@ export interface AsyncTicket {
86
87
  spawnLeafId?: string | null;
87
88
  /** Active blocking waiters. Resolved by terminal delivery or timeout/abort. */
88
89
  waiters?: TicketWaiter[];
90
+ /** Telemetry call span id attached to this async ticket. */
91
+ callId?: string;
92
+ /** Telemetry call span start timestamp for accurate wall-time on cancellation. */
93
+ callStartedAt?: number;
94
+ /** Snapshot of the call row at spawn, used to write the cancelled/settled row. */
95
+ callRecord?: CallRecord;
96
+ /** Runtime generation for rejecting shutdown writes from stale tickets. */
97
+ telemetryGeneration?: number;
98
+ /** Resolves after every async worker has settled, including shutdown aborts. */
99
+ completion?: Promise<void>;
89
100
  }
90
101
 
91
102
  /** Live parent settings captured when a delegate call starts. The built-in
@@ -245,6 +256,12 @@ export interface TaskRunEnv {
245
256
  onProgress: (p: TaskProgress, u: AgentProgressUpdate) => void;
246
257
  /** Called after every TaskProgress mutation (early-returns, completion). Sync uses this to fire onUpdate. */
247
258
  onStatusChange?: () => void;
259
+ /** Telemetry call id for this dispatch. undefined when telemetry is disabled or not started. */
260
+ telemetryCallId?: string;
261
+ /** Runtime generation for rejecting writes from a stale shutdown worker. */
262
+ telemetryGeneration?: number;
263
+ /** Whether this task is part of an async ticket. */
264
+ async?: boolean;
248
265
  }
249
266
 
250
267
  /** 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
+ }