@ganglion/xacpx 0.11.0 → 0.12.0

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.
@@ -68,6 +68,10 @@ function encodeBridgePromptPlanEvent(event) {
68
68
  return `${JSON.stringify(event)}
69
69
  `;
70
70
  }
71
+ function encodeBridgePromptUsageEvent(event) {
72
+ return `${JSON.stringify(event)}
73
+ `;
74
+ }
71
75
  function encodeBridgeSessionProgressEvent(event) {
72
76
  return `${JSON.stringify(event)}
73
77
  `;
@@ -424,6 +428,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
424
428
  let onToolEvent;
425
429
  let onThought;
426
430
  let onPlan;
431
+ let onUsage;
427
432
  let rawStream = false;
428
433
  if (options === undefined) {
429
434
  toolEventMode = "text";
@@ -435,6 +440,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
435
440
  onToolEvent = options.onToolEvent;
436
441
  onThought = options.onThought;
437
442
  onPlan = options.onPlan;
443
+ onUsage = options.onUsage;
438
444
  rawStream = options.rawStream ?? false;
439
445
  toolEventMode = resolveToolEventMode({
440
446
  toolEventMode: options.mode,
@@ -453,6 +459,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
453
459
  onToolEvent,
454
460
  onThought,
455
461
  onPlan,
462
+ onUsage,
456
463
  finalize() {
457
464
  if (this.pendingLine.trim().length > 0) {
458
465
  parseStreamingChunks(this, this.pendingLine);
@@ -517,6 +524,13 @@ function parseStreamingChunks(state, line) {
517
524
  state.onPlan?.(entries);
518
525
  return;
519
526
  }
527
+ if (update.sessionUpdate === "usage_update") {
528
+ const used = typeof update.used === "number" && Number.isFinite(update.used) ? update.used : undefined;
529
+ const size = typeof update.size === "number" && Number.isFinite(update.size) ? update.size : undefined;
530
+ if (used !== undefined && size !== undefined && size > 0)
531
+ state.onUsage?.({ used, size });
532
+ return;
533
+ }
520
534
  const isThoughtChunk = update.sessionUpdate === "agent_thought_chunk" && update.content?.type === "text" && typeof update.content.text === "string";
521
535
  if (isThoughtChunk) {
522
536
  const chunk2 = update.content.text;
@@ -3989,7 +4003,8 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
3989
4003
  rawStream,
3990
4004
  ...onEvent && (toolEventMode === "structured" || toolEventMode === "both") ? { onToolEvent: (toolEvent) => onEvent({ type: "prompt.tool_event", event: toolEvent }) } : {},
3991
4005
  ...onEvent ? { onThought: (chunk) => onEvent({ type: "prompt.thought", text: chunk }) } : {},
3992
- ...onEvent ? { onPlan: (entries) => onEvent({ type: "prompt.plan", entries }) } : {}
4006
+ ...onEvent ? { onPlan: (entries) => onEvent({ type: "prompt.plan", entries }) } : {},
4007
+ ...onEvent ? { onUsage: (usage) => onEvent({ type: "prompt.usage", used: usage.used, size: usage.size }) } : {}
3993
4008
  });
3994
4009
  let lastReplyAt = now();
3995
4010
  const flushBuffer = () => {
@@ -4280,6 +4295,13 @@ class BridgeServer {
4280
4295
  event: "prompt.plan",
4281
4296
  entries: event.entries
4282
4297
  }));
4298
+ } else if (event.type === "prompt.usage") {
4299
+ writeLine?.(encodeBridgePromptUsageEvent({
4300
+ id: requestId,
4301
+ event: "prompt.usage",
4302
+ used: event.used,
4303
+ size: event.size
4304
+ }));
4283
4305
  }
4284
4306
  });
4285
4307
  case "resumeAgentSession":
package/dist/cli.js CHANGED
@@ -22355,7 +22355,7 @@ async function handleSessionRemove(context, chatKey, alias) {
22355
22355
  return { text: lines.join(`
22356
22356
  `) };
22357
22357
  }
22358
- async function promptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan) {
22358
+ async function promptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage) {
22359
22359
  const effectiveReplyMode = resolveEffectiveReplyMode(context.config, chatKey, session3.replyMode);
22360
22360
  if (!session3.replyMode)
22361
22361
  session3.replyMode = effectiveReplyMode;
@@ -22387,7 +22387,7 @@ async function promptWithSession(context, session3, chatKey, text, reply, replyC
22387
22387
  const { promptText, taskIds, groupIds, claimHumanReply } = await preparePromptWithFallback(context, session3, chatKey, text, replyContextToken, accountId);
22388
22388
  try {
22389
22389
  const replyContext = transportReply && context.quota && getChannelIdFromChatKey(chatKey) === "weixin" ? { chatKey, quota: context.quota } : undefined;
22390
- const result = await context.interaction.promptTransportSession(session3, promptText, transportReply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan, onPlan);
22390
+ const result = await context.interaction.promptTransportSession(session3, promptText, transportReply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan, onPlan, onUsage);
22391
22391
  if (claimHumanReply) {
22392
22392
  try {
22393
22393
  await context.orchestration?.claimActiveHumanReply?.(claimHumanReply);
@@ -22407,23 +22407,23 @@ async function promptWithSession(context, session3, chatKey, text, reply, replyC
22407
22407
  throw error2;
22408
22408
  }
22409
22409
  }
22410
- async function handlePromptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan) {
22410
+ async function handlePromptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage) {
22411
22411
  try {
22412
- return await promptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
22412
+ return await promptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
22413
22413
  } catch (error2) {
22414
22414
  const recovered = await context.recovery.tryRecoverMissingSession(session3, error2);
22415
22415
  if (recovered) {
22416
- return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
22416
+ return await promptWithSession(context, recovered, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
22417
22417
  }
22418
22418
  return context.recovery.renderTransportError(session3, error2);
22419
22419
  }
22420
22420
  }
22421
- async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan) {
22421
+ async function handlePrompt(context, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage) {
22422
22422
  const session3 = metadata?.boundSessionAlias ? context.sessions.getResolvedSessionByInternalAlias(metadata.boundSessionAlias) : await context.sessions.getCurrentSession(chatKey);
22423
22423
  if (!session3) {
22424
22424
  return { text: t().session.noCurrent };
22425
22425
  }
22426
- return await handlePromptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
22426
+ return await handlePromptWithSession(context, session3, chatKey, text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
22427
22427
  }
22428
22428
  function toCoordinatorRouteChatMetadata(metadata) {
22429
22429
  if (!metadata) {
@@ -24682,7 +24682,7 @@ class CommandRouter {
24682
24682
  this.logger = logger2 ?? createNoopAppLogger();
24683
24683
  this.activeTurns = activeTurns;
24684
24684
  }
24685
- async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, onThought, perfSpan, onPlan) {
24685
+ async handle(chatKey, input, reply, replyContextToken, accountId, media, metadata, abortSignal, onToolEvent, onThought, perfSpan, onPlan, onUsage) {
24686
24686
  const startedAt = Date.now();
24687
24687
  let command = parseCommand(input);
24688
24688
  if (metadata?.channel === "control" && command.kind !== "prompt") {
@@ -24842,16 +24842,16 @@ class CommandRouter {
24842
24842
  ...this.sessions.resolveSession(descriptor.alias, descriptor.agent, descriptor.workspace, descriptor.transportSession),
24843
24843
  transient: true
24844
24844
  };
24845
- return await handlePromptWithSession(sessionContext, transientSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
24845
+ return await handlePromptWithSession(sessionContext, transientSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
24846
24846
  }
24847
24847
  if (metadata?.scheduledSessionAlias) {
24848
24848
  const scheduledSession = await this.sessions.getSession(metadata.scheduledSessionAlias);
24849
24849
  if (!scheduledSession) {
24850
24850
  throw new Error(`session "${metadata.scheduledSessionAlias}" not found for scheduled prompt`);
24851
24851
  }
24852
- return await handlePromptWithSession(sessionContext, scheduledSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
24852
+ return await handlePromptWithSession(sessionContext, scheduledSession, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
24853
24853
  }
24854
- return await handlePrompt(sessionContext, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan);
24854
+ return await handlePrompt(sessionContext, chatKey, command.text, reply, replyContextToken, accountId, media, abortSignal, onToolEvent, onThought, perfSpan, metadata, onPlan, onUsage);
24855
24855
  }
24856
24856
  }
24857
24857
  });
@@ -24999,7 +24999,7 @@ class CommandRouter {
24999
24999
  setModelTransportSession: (session3, modelId) => this.setModelTransportSession(session3, modelId),
25000
25000
  getModelTransportSession: (session3) => this.getModelTransportSession(session3),
25001
25001
  cancelTransportSession: (session3) => this.cancelTransportSession(session3),
25002
- promptTransportSession: (session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpanOverride, onPlan) => this.promptTransportSession(session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpanOverride ?? perfSpan, onPlan)
25002
+ promptTransportSession: (session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpanOverride, onPlan, onUsage) => this.promptTransportSession(session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpanOverride ?? perfSpan, onPlan, onUsage)
25003
25003
  };
25004
25004
  }
25005
25005
  createSessionRenderRecoveryOps() {
@@ -25178,7 +25178,7 @@ class CommandRouter {
25178
25178
  async checkTransportSession(session3) {
25179
25179
  return await this.measureTransportCall("has_session", session3, () => this.transport.hasSession(session3));
25180
25180
  }
25181
- async promptTransportSession(session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan, onPlan) {
25181
+ async promptTransportSession(session3, text, reply, replyContext, media, abortSignal, onToolEvent, onThought, perfSpan, onPlan, onUsage) {
25182
25182
  session3.mcpCoordinatorSession ??= stableCoordinatorSession(session3.transportSession);
25183
25183
  let done = false;
25184
25184
  let abortRequested = false;
@@ -25236,7 +25236,8 @@ class CommandRouter {
25236
25236
  ...reply ? { onSegment } : {},
25237
25237
  ...onToolEvent ? { onToolEvent } : {},
25238
25238
  ...onThought ? { onThought } : {},
25239
- ...onPlan ? { onPlan } : {}
25239
+ ...onPlan ? { onPlan } : {},
25240
+ ...onUsage ? { onUsage } : {}
25240
25241
  }));
25241
25242
  } catch (error2) {
25242
25243
  localOutcome = isAbortError2(error2) || abortRequested ? "aborted" : "error";
@@ -25422,7 +25423,7 @@ class ConsoleAgent {
25422
25423
  ...m.fileName ? { fileName: m.fileName } : {}
25423
25424
  })) : undefined;
25424
25425
  request.perfSpan?.mark("agent.dispatched");
25425
- return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId, promptMedia, request.metadata, request.abortSignal, request.onToolEvent, request.onThought, request.perfSpan, request.onPlan);
25426
+ return await this.router.handle(request.conversationId, request.text, request.reply, request.replyContextToken, request.accountId, promptMedia, request.metadata, request.abortSignal, request.onToolEvent, request.onThought, request.perfSpan, request.onPlan, request.onUsage);
25426
25427
  }
25427
25428
  isKnownCommand(text) {
25428
25429
  return isKnownXacpxCommandText(text);
@@ -30478,6 +30479,10 @@ function encodeBridgePromptPlanEvent(event) {
30478
30479
  return `${JSON.stringify(event)}
30479
30480
  `;
30480
30481
  }
30482
+ function encodeBridgePromptUsageEvent(event) {
30483
+ return `${JSON.stringify(event)}
30484
+ `;
30485
+ }
30481
30486
  function encodeBridgeSessionProgressEvent(event) {
30482
30487
  return `${JSON.stringify(event)}
30483
30488
  `;
@@ -30780,6 +30785,12 @@ class AcpxBridgeClient {
30780
30785
  type: "prompt.plan",
30781
30786
  entries: message.entries
30782
30787
  });
30788
+ } else if (message.event === "prompt.usage") {
30789
+ pending.onEvent?.({
30790
+ type: "prompt.usage",
30791
+ used: message.used,
30792
+ size: message.size
30793
+ });
30783
30794
  } else if (message.event === "session.progress") {
30784
30795
  pending.onEvent?.({
30785
30796
  type: "session.progress",
@@ -31239,6 +31250,8 @@ class AcpxBridgeTransport {
31239
31250
  let thoughtChain = Promise.resolve();
31240
31251
  let planError;
31241
31252
  let planChain = Promise.resolve();
31253
+ let usageError;
31254
+ let usageChain = Promise.resolve();
31242
31255
  let toolEventMode = resolveToolEventMode(options);
31243
31256
  if ((toolEventMode === "structured" || toolEventMode === "both") && !options?.onToolEvent) {
31244
31257
  toolEventMode = "text";
@@ -31291,11 +31304,22 @@ class AcpxBridgeTransport {
31291
31304
  }
31292
31305
  return;
31293
31306
  }
31307
+ if (event.type === "prompt.usage") {
31308
+ const onUsage = options?.onUsage;
31309
+ if (onUsage) {
31310
+ const usage = { used: event.used, size: event.size };
31311
+ usageChain = usageChain.then(() => onUsage(usage)).catch((error2) => {
31312
+ usageError ??= error2;
31313
+ });
31314
+ }
31315
+ return;
31316
+ }
31294
31317
  });
31295
31318
  await segmentChain;
31296
31319
  await toolEventChain;
31297
31320
  await thoughtChain;
31298
31321
  await planChain;
31322
+ await usageChain;
31299
31323
  if (sink) {
31300
31324
  const { overflowCount } = sink.finalize();
31301
31325
  await sink.drain({ timeoutMs: 30000 });
@@ -31316,6 +31340,9 @@ class AcpxBridgeTransport {
31316
31340
  if (planError) {
31317
31341
  throw planError;
31318
31342
  }
31343
+ if (usageError) {
31344
+ throw usageError;
31345
+ }
31319
31346
  return { text: summary ? `${summary}
31320
31347
 
31321
31348
  ${result.text}` : "" };
@@ -31332,6 +31359,9 @@ ${result.text}` : "" };
31332
31359
  if (planError) {
31333
31360
  throw planError;
31334
31361
  }
31362
+ if (usageError) {
31363
+ throw usageError;
31364
+ }
31335
31365
  return result;
31336
31366
  }
31337
31367
  async setMode(session3, modeId) {
@@ -31537,6 +31567,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
31537
31567
  let onToolEvent;
31538
31568
  let onThought;
31539
31569
  let onPlan;
31570
+ let onUsage;
31540
31571
  let rawStream = false;
31541
31572
  if (options === undefined) {
31542
31573
  toolEventMode = "text";
@@ -31548,6 +31579,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
31548
31579
  onToolEvent = options.onToolEvent;
31549
31580
  onThought = options.onThought;
31550
31581
  onPlan = options.onPlan;
31582
+ onUsage = options.onUsage;
31551
31583
  rawStream = options.rawStream ?? false;
31552
31584
  toolEventMode = resolveToolEventMode({
31553
31585
  toolEventMode: options.mode,
@@ -31566,6 +31598,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
31566
31598
  onToolEvent,
31567
31599
  onThought,
31568
31600
  onPlan,
31601
+ onUsage,
31569
31602
  finalize() {
31570
31603
  if (this.pendingLine.trim().length > 0) {
31571
31604
  parseStreamingChunks(this, this.pendingLine);
@@ -31630,6 +31663,13 @@ function parseStreamingChunks(state, line) {
31630
31663
  state.onPlan?.(entries);
31631
31664
  return;
31632
31665
  }
31666
+ if (update.sessionUpdate === "usage_update") {
31667
+ const used = typeof update.used === "number" && Number.isFinite(update.used) ? update.used : undefined;
31668
+ const size = typeof update.size === "number" && Number.isFinite(update.size) ? update.size : undefined;
31669
+ if (used !== undefined && size !== undefined && size > 0)
31670
+ state.onUsage?.({ used, size });
31671
+ return;
31672
+ }
31633
31673
  const isThoughtChunk = update.sessionUpdate === "agent_thought_chunk" && update.content?.type === "text" && typeof update.content.text === "string";
31634
31674
  if (isThoughtChunk) {
31635
31675
  const chunk2 = update.content.text;
@@ -33259,7 +33299,29 @@ class WorkspaceFs {
33259
33299
  }
33260
33300
  }
33261
33301
  const truncated = diff.length > DIFF_CAP;
33262
- return { workspace: workspace3, files, diff: truncated ? diff.slice(0, DIFF_CAP) : diff, truncated };
33302
+ return { workspace: workspace3, files, diff: truncated ? diff.slice(0, DIFF_CAP) : diff, truncated, ...await this.gitContext(root) };
33303
+ }
33304
+ async gitContext(root) {
33305
+ const run = async (...args) => {
33306
+ try {
33307
+ return (await execFileAsync("git", ["-C", root, ...args], { maxBuffer: GIT_MAX_BUFFER })).stdout.trim();
33308
+ } catch {
33309
+ return null;
33310
+ }
33311
+ };
33312
+ const ctx = {};
33313
+ const head = await run("rev-parse", "--abbrev-ref", "HEAD");
33314
+ if (head === "HEAD")
33315
+ ctx.detached = true;
33316
+ else if (head)
33317
+ ctx.branch = head;
33318
+ const top = await run("rev-parse", "--show-toplevel");
33319
+ if (top) {
33320
+ const gitDir = await run("rev-parse", "--absolute-git-dir");
33321
+ const commonDir = await run("rev-parse", "--path-format=absolute", "--git-common-dir");
33322
+ ctx.worktree = { root: top, linked: !!gitDir && !!commonDir && gitDir !== commonDir };
33323
+ }
33324
+ return ctx;
33263
33325
  }
33264
33326
  }
33265
33327
  var execFileAsync, MAX_ENTRIES = 2000, FILE_READ_CAP, DIFF_CAP, GIT_MAX_BUFFER, SEARCH_MAX_RESULTS = 200, SEARCH_MAX_SCAN = 20000, SEARCH_SKIP_DIRS;
@@ -33526,6 +33588,15 @@ ${chunk}` : chunk
33526
33588
  sessionAlias: params.sessionAlias,
33527
33589
  entries
33528
33590
  });
33591
+ },
33592
+ onUsage: (usage) => {
33593
+ this.deps.events.emit({
33594
+ type: "turn-usage",
33595
+ chatKey: params.chatKey,
33596
+ sessionAlias: params.sessionAlias,
33597
+ used: usage.used,
33598
+ size: usage.size
33599
+ });
33529
33600
  }
33530
33601
  });
33531
33602
  if (response.text) {
@@ -38,5 +38,11 @@ export declare function handleCancel(context: SessionHandlerContext, chatKey: st
38
38
  export declare function handleSessionReset(context: SessionHandlerContext, chatKey: string): Promise<RouterResponse>;
39
39
  export declare function handleSessionTail(context: SessionHandlerContext, chatKey: string, lines?: number): Promise<RouterResponse>;
40
40
  export declare function handleSessionRemove(context: SessionHandlerContext, chatKey: string, alias: string): Promise<RouterResponse>;
41
- export declare function handlePromptWithSession(context: SessionHandlerContext, session: ResolvedSession, chatKey: string, text: string, reply?: (text: string) => Promise<void>, replyContextToken?: string, accountId?: string, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, metadata?: ChatRequestMetadata, onPlan?: (entries: PlanEntry[]) => void | Promise<void>): Promise<RouterResponse>;
42
- export declare function handlePrompt(context: SessionHandlerContext, chatKey: string, text: string, reply?: (text: string) => Promise<void>, replyContextToken?: string, accountId?: string, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, metadata?: ChatRequestMetadata, onPlan?: (entries: PlanEntry[]) => void | Promise<void>): Promise<RouterResponse>;
41
+ export declare function handlePromptWithSession(context: SessionHandlerContext, session: ResolvedSession, chatKey: string, text: string, reply?: (text: string) => Promise<void>, replyContextToken?: string, accountId?: string, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, metadata?: ChatRequestMetadata, onPlan?: (entries: PlanEntry[]) => void | Promise<void>, onUsage?: (usage: {
42
+ used: number;
43
+ size: number;
44
+ }) => void | Promise<void>): Promise<RouterResponse>;
45
+ export declare function handlePrompt(context: SessionHandlerContext, chatKey: string, text: string, reply?: (text: string) => Promise<void>, replyContextToken?: string, accountId?: string, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, metadata?: ChatRequestMetadata, onPlan?: (entries: PlanEntry[]) => void | Promise<void>, onUsage?: (usage: {
46
+ used: number;
47
+ size: number;
48
+ }) => void | Promise<void>): Promise<RouterResponse>;
@@ -113,7 +113,10 @@ export interface SessionInteractionOps {
113
113
  cancelled: boolean;
114
114
  message: string;
115
115
  }>;
116
- promptTransportSession: (session: import("../transport/types").ResolvedSession, text: string, reply?: (text: string) => Promise<void>, replyContext?: ReplyQuotaContext, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, onPlan?: (entries: PlanEntry[]) => void | Promise<void>) => Promise<{
116
+ promptTransportSession: (session: import("../transport/types").ResolvedSession, text: string, reply?: (text: string) => Promise<void>, replyContext?: ReplyQuotaContext, media?: PromptMediaInput, abortSignal?: AbortSignal, onToolEvent?: (event: ToolUseEvent) => void | Promise<void>, onThought?: (chunk: string) => void | Promise<void>, perfSpan?: PerfSpan, onPlan?: (entries: PlanEntry[]) => void | Promise<void>, onUsage?: (usage: {
117
+ used: number;
118
+ size: number;
119
+ }) => void | Promise<void>) => Promise<{
117
120
  text: string;
118
121
  }>;
119
122
  }
@@ -31,6 +31,12 @@ export type ControlEvent = {
31
31
  chatKey: string;
32
32
  sessionAlias: string;
33
33
  entries: PlanEntry[];
34
+ } | {
35
+ type: "turn-usage";
36
+ chatKey: string;
37
+ sessionAlias: string;
38
+ used: number;
39
+ size: number;
34
40
  } | {
35
41
  type: "turn-finished";
36
42
  chatKey: string;
@@ -31,6 +31,15 @@ export interface WorkspaceDiff {
31
31
  files: DiffFile[];
32
32
  diff: string;
33
33
  truncated: boolean;
34
+ /** Symbolic branch name (abbrev-ref HEAD); omitted when HEAD is detached. */
35
+ branch?: string;
36
+ /** True when HEAD is detached (no branch). */
37
+ detached?: boolean;
38
+ /** Working-tree context: its top-level root, and whether it's a linked (non-primary) worktree. */
39
+ worktree?: {
40
+ root: string;
41
+ linked: boolean;
42
+ };
34
43
  }
35
44
  export interface WorkspaceRef {
36
45
  name: string;
@@ -49,4 +58,7 @@ export declare class WorkspaceFs {
49
58
  * (so it stays contained), bounded by a scan budget and a result cap. */
50
59
  search(workspace: string, query: string): Promise<SearchResult>;
51
60
  gitDiff(workspace: string, relPath?: string): Promise<WorkspaceDiff>;
61
+ /** Branch + worktree context for a repo root. Best-effort: any git hiccup just
62
+ * omits the fields so the diff itself still returns. */
63
+ private gitContext;
52
64
  }
@@ -114,6 +114,16 @@ export interface PromptOptions {
114
114
  * re-sent on every update (REPLACE, not append). Optional — text channels omit it.
115
115
  */
116
116
  onPlan?: (entries: PlanEntry[]) => void | Promise<void>;
117
+ /**
118
+ * Context-usage side-channel: the agent's ACP `usage_update` — `used` tokens
119
+ * currently in context and `size`, the model's total context window. Replace-latest
120
+ * scalar (re-sent during a turn). Optional — only agents that report it fire this
121
+ * (e.g. claude does, codex does not), and text channels omit the handler.
122
+ */
123
+ onUsage?: (usage: {
124
+ used: number;
125
+ size: number;
126
+ }) => void | Promise<void>;
117
127
  /**
118
128
  * How tool_call / tool_call_update events are surfaced for this prompt.
119
129
  *
@@ -50,6 +50,11 @@ export interface ChatRequest {
50
50
  onThought?: (chunk: string) => void | Promise<void>;
51
51
  /** Structured plan/todo side-channel; see PromptOptions.onPlan. */
52
52
  onPlan?: (entries: PlanEntry[]) => void | Promise<void>;
53
+ /** Context-usage side-channel; see PromptOptions.onUsage. */
54
+ onUsage?: (usage: {
55
+ used: number;
56
+ size: number;
57
+ }) => void | Promise<void>;
53
58
  /**
54
59
  * Optional per-turn performance tracing span. When `logging.perf.enabled` is
55
60
  * true, the channel handler attaches a `PerfSpan` so downstream layers can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",