@abelxiaoxing/cadence 1.0.2 → 1.0.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.
@@ -8,7 +8,7 @@ import {
8
8
  SettingsManager,
9
9
  } from "@earendil-works/pi-coding-agent";
10
10
  import { Type } from "typebox";
11
- import type { DiffResult, EvidenceResult } from "./contracts.ts";
11
+ import type { ChildFailure, DiffResult, EvidenceResult } from "./contracts.ts";
12
12
  import { EmptyResourceLoader } from "./empty-resource-loader.ts";
13
13
  import { createScopedTools } from "./scoped-tools.ts";
14
14
  import {
@@ -31,6 +31,22 @@ function abortable<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
31
31
  });
32
32
  }
33
33
 
34
+ const ABORT_SETTLE_GRACE_MS = 250;
35
+
36
+ async function settleAbort(promise: Promise<void>): Promise<void> {
37
+ let timer: ReturnType<typeof setTimeout> | undefined;
38
+ try {
39
+ await Promise.race([
40
+ promise.catch(() => undefined),
41
+ new Promise<void>((resolve) => {
42
+ timer = setTimeout(resolve, ABORT_SETTLE_GRACE_MS);
43
+ }),
44
+ ]);
45
+ } finally {
46
+ if (timer !== undefined) clearTimeout(timer);
47
+ }
48
+ }
49
+
34
50
  const ZERO_USAGE: Usage = {
35
51
  input: 0,
36
52
  output: 0,
@@ -46,11 +62,21 @@ export class UsageAggregator {
46
62
  add(id: string, value: Usage): boolean {
47
63
  if (this.ids.has(id)) return false;
48
64
  this.ids.add(id);
65
+ const cacheWrite1h =
66
+ this.usage.cacheWrite1h === undefined && value.cacheWrite1h === undefined
67
+ ? undefined
68
+ : (this.usage.cacheWrite1h ?? 0) + (value.cacheWrite1h ?? 0);
69
+ const reasoning =
70
+ this.usage.reasoning === undefined && value.reasoning === undefined
71
+ ? undefined
72
+ : (this.usage.reasoning ?? 0) + (value.reasoning ?? 0);
49
73
  this.usage = {
50
74
  input: this.usage.input + value.input,
51
75
  output: this.usage.output + value.output,
52
76
  cacheRead: this.usage.cacheRead + value.cacheRead,
53
77
  cacheWrite: this.usage.cacheWrite + value.cacheWrite,
78
+ ...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
79
+ ...(reasoning === undefined ? {} : { reasoning }),
54
80
  totalTokens: this.usage.totalTokens + value.totalTokens,
55
81
  cost: {
56
82
  input: this.usage.cost.input + value.cost.input,
@@ -62,6 +88,9 @@ export class UsageAggregator {
62
88
  };
63
89
  return true;
64
90
  }
91
+ hasUsage(): boolean {
92
+ return this.ids.size > 0;
93
+ }
65
94
  total(): Usage {
66
95
  return structuredClone(this.usage);
67
96
  }
@@ -108,21 +137,7 @@ function wrapScopedTools(
108
137
 
109
138
  export type ChildFailureKind = "failed" | "cancelled" | "timed-out";
110
139
 
111
- export async function runChildSession(input: {
112
- cwd: string;
113
- modelRuntime: ModelRuntime;
114
- model: Model<string>;
115
- systemPrompt: string;
116
- requestId: string;
117
- taskId?: string;
118
- role: string;
119
- phase?: string;
120
- output: "evidence" | "diff";
121
- roots: string[];
122
- allowedPaths?: string[];
123
- timeoutMs: number;
124
- signal?: AbortSignal;
125
- }): Promise<
140
+ export type ChildSessionResult =
126
141
  | {
127
142
  ok: true;
128
143
  result: EvidenceResult | DiffResult;
@@ -135,12 +150,29 @@ export async function runChildSession(input: {
135
150
  | {
136
151
  ok: false;
137
152
  error: string;
153
+ failure: ChildFailure;
138
154
  failureKind: ChildFailureKind;
139
155
  transportFailure: boolean;
140
156
  disposeCount: number;
157
+ usage: Usage;
141
158
  classification: SubmitClassification;
142
- }
143
- > {
159
+ };
160
+
161
+ export async function runChildSession(input: {
162
+ cwd: string;
163
+ modelRuntime: ModelRuntime;
164
+ model: Model<string>;
165
+ systemPrompt: string;
166
+ requestId: string;
167
+ taskId?: string;
168
+ role: string;
169
+ phase?: string;
170
+ output: "evidence" | "diff";
171
+ roots: string[];
172
+ allowedPaths?: string[];
173
+ timeoutMs: number;
174
+ signal?: AbortSignal;
175
+ }): Promise<ChildSessionResult> {
144
176
  const submit = createSubmitTool({
145
177
  requestId: input.requestId,
146
178
  taskId: input.taskId,
@@ -170,6 +202,8 @@ export async function runChildSession(input: {
170
202
  | Awaited<ReturnType<typeof createAgentSession>>["session"]
171
203
  | undefined;
172
204
  let unsubscribe: (() => void) | undefined;
205
+ let abortSettlement: Promise<void> | undefined;
206
+ let assistantSequence = 0;
173
207
  const disposeOnce = () => {
174
208
  if (session) {
175
209
  session.dispose();
@@ -244,19 +278,24 @@ export async function runChildSession(input: {
244
278
  .catch(() => undefined);
245
279
  throw error;
246
280
  }
247
- abort.signal.throwIfAborted();
248
281
  unsubscribe = session.subscribe((event) => {
249
282
  if (event.type === "message_end" && event.message.role === "assistant") {
250
- usage.add(
251
- `${event.message.provider}:${event.message.model}:${event.message.timestamp}`,
252
- event.message.usage,
253
- );
283
+ usage.add(`assistant:${assistantSequence++}`, event.message.usage);
254
284
  }
255
285
  });
256
- const onAbort = () => void session?.abort();
286
+ const startAbortSettlement = () => {
287
+ if (session && !abortSettlement) abortSettlement = session.abort();
288
+ return abortSettlement;
289
+ };
290
+ const onAbort = () => {
291
+ void startAbortSettlement()?.catch(() => undefined);
292
+ };
257
293
  abort.signal.addEventListener("abort", onAbort, { once: true });
258
294
  try {
259
- if (abort.signal.aborted) throw abort.signal.reason;
295
+ if (abort.signal.aborted) {
296
+ onAbort();
297
+ throw abort.signal.reason;
298
+ }
260
299
  await Promise.race([
261
300
  session.prompt(input.systemPrompt, { expandPromptTemplates: false }),
262
301
  new Promise<never>((_, reject) =>
@@ -275,21 +314,30 @@ export async function runChildSession(input: {
275
314
  const classification = classifySession();
276
315
  if (!result || attempts !== 1) {
277
316
  const transportFailure = transportFailed();
317
+ const failure =
318
+ submit.getFailure() ??
319
+ (transportFailure
320
+ ? ({ kind: "transport", code: "transport-failure" } as const)
321
+ : ({
322
+ kind: "artifact",
323
+ code: "invalid-structural-result",
324
+ } as const));
278
325
  disposeOnce();
279
326
  return {
280
327
  ok: false,
281
328
  error: "child did not retain exactly one structural submission",
329
+ failure,
282
330
  failureKind: "failed",
283
331
  transportFailure,
284
332
  disposeCount,
333
+ usage: usage.total(),
285
334
  classification,
286
335
  };
287
336
  }
288
337
  const assistants = session.messages.filter((m) => m.role === "assistant");
289
338
  const final = assistants.at(-1);
290
339
  if (
291
- !final ||
292
- final.role !== "assistant" ||
340
+ final?.role !== "assistant" ||
293
341
  final.content.length !== 1 ||
294
342
  final.content[0]?.type !== "toolCall" ||
295
343
  final.content[0]?.name !== "abel_submit_result"
@@ -298,9 +346,11 @@ export async function runChildSession(input: {
298
346
  return {
299
347
  ok: false,
300
348
  error: "final assistant message is not one structural submit",
349
+ failure: { kind: "artifact", code: "invalid-structural-result" },
301
350
  failureKind: "failed",
302
351
  transportFailure: false,
303
352
  disposeCount,
353
+ usage: usage.total(),
304
354
  classification,
305
355
  };
306
356
  }
@@ -315,6 +365,11 @@ export async function runChildSession(input: {
315
365
  classification,
316
366
  };
317
367
  } catch (error) {
368
+ if (abort.signal.aborted && session) {
369
+ const settlement = abortSettlement ?? session.abort();
370
+ abortSettlement = settlement;
371
+ await settleAbort(settlement);
372
+ }
318
373
  const failureKind: ChildFailureKind = timedOut
319
374
  ? "timed-out"
320
375
  : input.signal !== undefined &&
@@ -323,13 +378,23 @@ export async function runChildSession(input: {
323
378
  : "failed";
324
379
  const classification = classifySession();
325
380
  const transportFailure = transportFailed();
381
+ const failure: ChildFailure = timedOut
382
+ ? { kind: "transport", code: "timeout" }
383
+ : failureKind === "cancelled"
384
+ ? { kind: "cancelled", code: "cancelled" }
385
+ : (submit.getFailure() ??
386
+ (transportFailure
387
+ ? { kind: "transport", code: "transport-failure" }
388
+ : { kind: "artifact", code: "invalid-structural-result" }));
326
389
  disposeOnce();
327
390
  return {
328
391
  ok: false,
329
392
  error: (error as Error).message,
393
+ failure,
330
394
  failureKind,
331
395
  transportFailure,
332
396
  disposeCount,
397
+ usage: usage.total(),
333
398
  classification,
334
399
  };
335
400
  } finally {