@cjhyy/code-shell-core 0.7.0 → 0.7.1

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.
Files changed (118) hide show
  1. package/dist/arena/arena.d.ts +2 -0
  2. package/dist/arena/arena.js +30 -1
  3. package/dist/arena/phases/adjudication.d.ts +2 -1
  4. package/dist/arena/phases/adjudication.js +2 -1
  5. package/dist/arena/phases/build-consensus.d.ts +2 -1
  6. package/dist/arena/phases/build-consensus.js +3 -1
  7. package/dist/arena/phases/cross-review.d.ts +3 -1
  8. package/dist/arena/phases/cross-review.js +8 -2
  9. package/dist/arena/phases/debate-rounds.d.ts +2 -1
  10. package/dist/arena/phases/debate-rounds.js +4 -2
  11. package/dist/arena/phases/participant-research.d.ts +2 -1
  12. package/dist/arena/phases/participant-research.js +3 -1
  13. package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
  14. package/dist/arena/phases/planning-detail-expansion.js +2 -1
  15. package/dist/arena/planner.d.ts +2 -1
  16. package/dist/arena/planner.js +2 -1
  17. package/dist/arena/types.d.ts +5 -1
  18. package/dist/cli/agent-server-tcp.js +13 -2
  19. package/dist/context/manager.d.ts +3 -3
  20. package/dist/context/manager.js +6 -6
  21. package/dist/context/token-counter.js +5 -3
  22. package/dist/context/tool-result-storage.d.ts +6 -0
  23. package/dist/context/tool-result-storage.js +25 -4
  24. package/dist/credentials/access.d.ts +10 -1
  25. package/dist/credentials/access.js +76 -2
  26. package/dist/credentials/index.d.ts +3 -2
  27. package/dist/credentials/index.js +2 -1
  28. package/dist/credentials/oauth.d.ts +6 -1
  29. package/dist/credentials/oauth.js +66 -1
  30. package/dist/credentials/store.d.ts +1 -1
  31. package/dist/credentials/store.js +16 -7
  32. package/dist/credentials/types.d.ts +38 -1
  33. package/dist/credentials/types.js +16 -1
  34. package/dist/engine/engine.d.ts +61 -32
  35. package/dist/engine/engine.js +289 -55
  36. package/dist/engine/goal.d.ts +5 -3
  37. package/dist/engine/session-title.d.ts +2 -1
  38. package/dist/engine/session-title.js +4 -1
  39. package/dist/engine/turn-loop.d.ts +18 -0
  40. package/dist/engine/turn-loop.js +202 -27
  41. package/dist/engine/types.d.ts +6 -2
  42. package/dist/git/worktree/crud.d.ts +3 -0
  43. package/dist/git/worktree/crud.js +32 -3
  44. package/dist/git/worktree/git-exec.d.ts +2 -2
  45. package/dist/git/worktree/git-exec.js +47 -11
  46. package/dist/git/worktree/query.d.ts +8 -7
  47. package/dist/git/worktree/query.js +27 -20
  48. package/dist/hooks/events.d.ts +3 -0
  49. package/dist/hooks/events.js +0 -3
  50. package/dist/hooks/goal-stop-hook.d.ts +14 -4
  51. package/dist/hooks/goal-stop-hook.js +626 -71
  52. package/dist/hooks/registry.js +3 -0
  53. package/dist/hooks/shell-runner.d.ts +12 -1
  54. package/dist/hooks/shell-runner.js +160 -9
  55. package/dist/index.d.ts +7 -6
  56. package/dist/index.js +6 -5
  57. package/dist/llm/client-base.js +12 -10
  58. package/dist/llm/types.d.ts +12 -5
  59. package/dist/plugins/pluginCommandHook.d.ts +4 -4
  60. package/dist/plugins/pluginCommandHook.js +111 -13
  61. package/dist/protocol/chat-session-manager.d.ts +13 -2
  62. package/dist/protocol/chat-session-manager.js +90 -18
  63. package/dist/protocol/chat-session.d.ts +12 -0
  64. package/dist/protocol/chat-session.js +30 -5
  65. package/dist/protocol/client.d.ts +5 -2
  66. package/dist/protocol/client.js +22 -1
  67. package/dist/protocol/server.d.ts +25 -11
  68. package/dist/protocol/server.js +290 -72
  69. package/dist/protocol/types.d.ts +34 -2
  70. package/dist/protocol/types.js +2 -0
  71. package/dist/services/dream-consolidation.d.ts +3 -0
  72. package/dist/services/dream-consolidation.js +4 -1
  73. package/dist/services/index.d.ts +1 -1
  74. package/dist/services/index.js +1 -1
  75. package/dist/services/oauth.d.ts +34 -10
  76. package/dist/services/oauth.js +233 -98
  77. package/dist/session/session-manager.d.ts +35 -6
  78. package/dist/session/session-manager.js +362 -26
  79. package/dist/session/transcript.d.ts +30 -1
  80. package/dist/session/transcript.js +119 -4
  81. package/dist/tool-system/builtin/agent.js +5 -1
  82. package/dist/tool-system/builtin/arena.js +1 -0
  83. package/dist/tool-system/builtin/background-jobs.d.ts +8 -5
  84. package/dist/tool-system/builtin/background-jobs.js +15 -11
  85. package/dist/tool-system/builtin/background-work.d.ts +6 -1
  86. package/dist/tool-system/builtin/background-work.js +5 -1
  87. package/dist/tool-system/builtin/bash.d.ts +3 -5
  88. package/dist/tool-system/builtin/bash.js +10 -5
  89. package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
  90. package/dist/tool-system/builtin/edit.d.ts +2 -1
  91. package/dist/tool-system/builtin/edit.js +12 -4
  92. package/dist/tool-system/builtin/generate-video.d.ts +4 -0
  93. package/dist/tool-system/builtin/generate-video.js +138 -21
  94. package/dist/tool-system/builtin/glob.d.ts +2 -1
  95. package/dist/tool-system/builtin/glob.js +28 -3
  96. package/dist/tool-system/builtin/grep.d.ts +1 -0
  97. package/dist/tool-system/builtin/grep.js +82 -17
  98. package/dist/tool-system/builtin/index.d.ts +25 -11
  99. package/dist/tool-system/builtin/index.js +45 -2
  100. package/dist/tool-system/builtin/video-providers.d.ts +12 -15
  101. package/dist/tool-system/builtin/video-providers.js +1 -0
  102. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  103. package/dist/tool-system/builtin/web-fetch.js +44 -3
  104. package/dist/tool-system/builtin/worktree.js +25 -7
  105. package/dist/tool-system/builtin/write.d.ts +2 -1
  106. package/dist/tool-system/builtin/write.js +14 -4
  107. package/dist/tool-system/context.d.ts +17 -5
  108. package/dist/tool-system/executor.js +24 -8
  109. package/dist/tool-system/mcp-manager.d.ts +6 -0
  110. package/dist/tool-system/mcp-manager.js +55 -5
  111. package/dist/tool-system/path-policy.d.ts +19 -0
  112. package/dist/tool-system/path-policy.js +62 -1
  113. package/dist/tool-system/permission.d.ts +43 -3
  114. package/dist/tool-system/permission.js +383 -30
  115. package/dist/tool-system/registry.d.ts +3 -2
  116. package/dist/tool-system/registry.js +52 -34
  117. package/dist/types.d.ts +15 -0
  118. package/package.json +1 -1
@@ -19,6 +19,8 @@ export declare class Arena {
19
19
  private config;
20
20
  private strategy;
21
21
  private limits;
22
+ private usage;
23
+ private readonly recordUsage;
22
24
  constructor(config: ArenaConfig);
23
25
  /**
24
26
  * Run a full arena session.
@@ -28,6 +28,7 @@ import { runDetailExpansion } from "./phases/planning-detail-expansion.js";
28
28
  import { withLanguage } from "./strategies/language-wrapper.js";
29
29
  import { ArenaLedger } from "./ledger.js";
30
30
  import { registerClaims } from "./phases/claim-registry.js";
31
+ import { addTokenUsage } from "../engine/session-usage.js";
31
32
  /** Bucket claims by status. Shared by the two consensus phases. */
32
33
  function summarizeClaims(all) {
33
34
  return {
@@ -41,6 +42,17 @@ export class Arena {
41
42
  config;
42
43
  strategy;
43
44
  limits;
45
+ usage = {
46
+ promptTokens: 0,
47
+ completionTokens: 0,
48
+ totalTokens: 0,
49
+ cacheReadTokens: 0,
50
+ cacheCreationTokens: 0,
51
+ };
52
+ recordUsage = (usage) => {
53
+ if (usage)
54
+ this.usage = addTokenUsage(this.usage, usage);
55
+ };
44
56
  constructor(config) {
45
57
  if (config.participants.length < 2) {
46
58
  throw new Error("Arena requires at least 2 participants");
@@ -63,13 +75,20 @@ export class Arena {
63
75
  * @param flags - Optional explicit overrides (mode, base, head)
64
76
  */
65
77
  async run(topic, flags) {
78
+ this.usage = {
79
+ promptTokens: 0,
80
+ completionTokens: 0,
81
+ totalTokens: 0,
82
+ cacheReadTokens: 0,
83
+ cacheCreationTokens: 0,
84
+ };
66
85
  const participantNames = this.config.participants.map((p) => p.name);
67
86
  const signal = this.config.signal;
68
87
  logger.info("arena.start", { mode: this.config.mode, participants: participantNames });
69
88
  signal?.throwIfAborted();
70
89
  // ─── Phase 0: Plan ────────────────────────────────────────
71
90
  const llmConfig = this.config.participants[0].llm;
72
- const plan = await planArena(topic, llmConfig, flags, signal);
91
+ const plan = await planArena(topic, llmConfig, flags, signal, this.recordUsage);
73
92
  this.config.onProgress?.({ type: "plan_resolved", plan });
74
93
  logger.info("arena.plan", {
75
94
  mode: plan.mode,
@@ -129,6 +148,7 @@ export class Arena {
129
148
  contextTools,
130
149
  signal,
131
150
  onProgress: this.config.onProgress,
151
+ onUsage: this.recordUsage,
132
152
  });
133
153
  const reports = researchResults.map((r) => r.report);
134
154
  const dossiers = researchResults.map((r) => r.dossier);
@@ -197,6 +217,7 @@ export class Arena {
197
217
  mode: "planning",
198
218
  signal,
199
219
  onProgress: this.config.onProgress,
220
+ onUsage: this.recordUsage,
200
221
  });
201
222
  signal?.throwIfAborted();
202
223
  // ─── Roadmap Consensus ───────────────────────────────────
@@ -212,6 +233,7 @@ export class Arena {
212
233
  claimSummary,
213
234
  signal,
214
235
  onProgress: this.config.onProgress,
236
+ onUsage: this.recordUsage,
215
237
  });
216
238
  signal?.throwIfAborted();
217
239
  // ─── Detail Expansion ────────────────────────────────────
@@ -229,6 +251,7 @@ export class Arena {
229
251
  contextTools,
230
252
  signal,
231
253
  onProgress: this.config.onProgress,
254
+ onUsage: this.recordUsage,
232
255
  });
233
256
  consensus.roadmapDetails = details;
234
257
  }
@@ -251,6 +274,7 @@ export class Arena {
251
274
  claims: allClaims,
252
275
  reviews,
253
276
  consensus,
277
+ usage: { ...this.usage },
254
278
  };
255
279
  }
256
280
  /**
@@ -271,6 +295,7 @@ export class Arena {
271
295
  limits: this.limits,
272
296
  signal,
273
297
  onProgress: this.config.onProgress,
298
+ onUsage: this.recordUsage,
274
299
  });
275
300
  signal?.throwIfAborted();
276
301
  // ─── Debate Rounds ───────────────────────────────────────
@@ -284,6 +309,7 @@ export class Arena {
284
309
  maxRounds: this.config.maxDiscussionRounds,
285
310
  signal,
286
311
  onProgress: this.config.onProgress,
312
+ onUsage: this.recordUsage,
287
313
  });
288
314
  signal?.throwIfAborted();
289
315
  // ─── Adjudication ────────────────────────────────────────
@@ -295,6 +321,7 @@ export class Arena {
295
321
  ledger,
296
322
  signal,
297
323
  onProgress: this.config.onProgress,
324
+ onUsage: this.recordUsage,
298
325
  });
299
326
  signal?.throwIfAborted();
300
327
  // ─── Consensus (claim-aware) ─────────────────────────────
@@ -314,6 +341,7 @@ export class Arena {
314
341
  claimSummary,
315
342
  signal,
316
343
  onProgress: this.config.onProgress,
344
+ onUsage: this.recordUsage,
317
345
  });
318
346
  return {
319
347
  topic,
@@ -328,6 +356,7 @@ export class Arena {
328
356
  debateRounds,
329
357
  adjudications,
330
358
  consensus,
359
+ usage: { ...this.usage },
331
360
  };
332
361
  }
333
362
  }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Claims that were verified without contest get synthetic adjudication records.
8
8
  */
9
- import type { ArenaParticipant, ArenaStrategy, ClaimAdjudication, ArenaProgressEvent } from "../types.js";
9
+ import type { ArenaParticipant, ArenaStrategy, ClaimAdjudication, ArenaProgressEvent, ArenaUsageRecorder } from "../types.js";
10
10
  import type { ArenaLedger } from "../ledger.js";
11
11
  interface AdjudicationOptions {
12
12
  concluder: ArenaParticipant;
@@ -15,6 +15,7 @@ interface AdjudicationOptions {
15
15
  ledger: ArenaLedger;
16
16
  signal?: AbortSignal;
17
17
  onProgress?: (event: ArenaProgressEvent) => void;
18
+ onUsage?: ArenaUsageRecorder;
18
19
  }
19
20
  /**
20
21
  * Run adjudication phase. The moderator rules on contested claims.
@@ -17,7 +17,7 @@ import { logger } from "../../logging/logger.js";
17
17
  * Verified claims get synthetic adjudications.
18
18
  */
19
19
  export async function runAdjudication(options) {
20
- const { concluder, strategy, topic, ledger, signal, onProgress } = options;
20
+ const { concluder, strategy, topic, ledger, signal, onProgress, onUsage } = options;
21
21
  const allAdjudications = [];
22
22
  const v2 = isStrategyV2(strategy);
23
23
  // Claims that need moderator adjudication: contested or those with debate rounds
@@ -54,6 +54,7 @@ export async function runAdjudication(options) {
54
54
  messages: [{ role: "user", content: userContent }],
55
55
  signal,
56
56
  });
57
+ onUsage?.(response.usage);
57
58
  logger.info("arena.adjudication_response", {
58
59
  claimId: claim.claimId,
59
60
  stopReason: response.stopReason,
@@ -8,7 +8,7 @@
8
8
  * In both paths, the moderator must faithfully reflect aggregated results
9
9
  * (no new unsourced conclusions).
10
10
  */
11
- import type { ArenaParticipant, ArenaStrategy, ParticipantReport, FindingReview, ArenaConsensus, ClaimStatusSummary, ArenaProgressEvent } from "../types.js";
11
+ import type { ArenaParticipant, ArenaStrategy, ParticipantReport, FindingReview, ArenaConsensus, ClaimStatusSummary, ArenaProgressEvent, ArenaUsageRecorder } from "../types.js";
12
12
  interface ConsensusOptions {
13
13
  /** The participant that acts as moderator/concluder */
14
14
  concluder: ArenaParticipant;
@@ -20,6 +20,7 @@ interface ConsensusOptions {
20
20
  claimSummary?: ClaimStatusSummary;
21
21
  signal?: AbortSignal;
22
22
  onProgress?: (event: ArenaProgressEvent) => void;
23
+ onUsage?: ArenaUsageRecorder;
23
24
  }
24
25
  /**
25
26
  * Build structured consensus from participant reports, cross-reviews,
@@ -16,7 +16,7 @@ import { isStrategyV2 } from "../types.js";
16
16
  * and optionally adjudicated claim data.
17
17
  */
18
18
  export async function buildConsensus(options) {
19
- const { concluder, strategy, topic, reports, reviews, claimSummary, signal, onProgress } = options;
19
+ const { concluder, strategy, topic, reports, reviews, claimSummary, signal, onProgress, onUsage } = options;
20
20
  onProgress?.({ type: "consensus_start" });
21
21
  const client = await createLLMClient(concluder.llm, concluder.clientDefaults);
22
22
  const systemPrompt = strategy.consensusSystemPrompt();
@@ -41,6 +41,7 @@ export async function buildConsensus(options) {
41
41
  messages: [{ role: "user", content: userContent }],
42
42
  signal,
43
43
  });
44
+ onUsage?.(response.usage);
44
45
  logger.info("arena.consensus_raw_response", {
45
46
  text: response.text,
46
47
  stopReason: response.stopReason,
@@ -64,6 +65,7 @@ export async function buildConsensus(options) {
64
65
  ],
65
66
  signal,
66
67
  });
68
+ onUsage?.(retryResponse.usage);
67
69
  logger.info("arena.consensus_retry_response", {
68
70
  textLen: retryResponse.text?.length ?? 0,
69
71
  stopReason: retryResponse.stopReason,
@@ -7,7 +7,7 @@
7
7
  * V2 (VerificationReview): Reviewers get claim + evidence packet + digest data,
8
8
  * and produce ClaimChallenge records that feed into the claim state machine.
9
9
  */
10
- import type { ArenaParticipant, ArenaStrategy, ParticipantReport, FindingReview, ClaimChallenge, ArenaProgressEvent, ArenaExecutionLimits } from "../types.js";
10
+ import type { ArenaParticipant, ArenaStrategy, ParticipantReport, FindingReview, ClaimChallenge, ArenaProgressEvent, ArenaExecutionLimits, ArenaUsageRecorder } from "../types.js";
11
11
  import type { ArenaLedger } from "../ledger.js";
12
12
  interface CrossReviewOptions {
13
13
  participants: ArenaParticipant[];
@@ -16,6 +16,7 @@ interface CrossReviewOptions {
16
16
  reports: ParticipantReport[];
17
17
  signal?: AbortSignal;
18
18
  onProgress?: (event: ArenaProgressEvent) => void;
19
+ onUsage?: ArenaUsageRecorder;
19
20
  }
20
21
  /**
21
22
  * Run cross-review phase (V1). Each participant reviews the others' findings.
@@ -32,6 +33,7 @@ interface VerificationReviewOptions {
32
33
  mode?: "review" | "discussion" | "planning";
33
34
  signal?: AbortSignal;
34
35
  onProgress?: (event: ArenaProgressEvent) => void;
36
+ onUsage?: ArenaUsageRecorder;
35
37
  }
36
38
  interface VerificationReviewResult {
37
39
  reviews: FindingReview[];
@@ -17,7 +17,7 @@ import { logger } from "../../logging/logger.js";
17
17
  * Run cross-review phase (V1). Each participant reviews the others' findings.
18
18
  */
19
19
  export async function runCrossReview(options) {
20
- const { participants, strategy, topic, reports, signal, onProgress } = options;
20
+ const { participants, strategy, topic, reports, signal, onProgress, onUsage } = options;
21
21
  onProgress?.({ type: "cross_review_start", round: 1 });
22
22
  const allReviews = [];
23
23
  const tasks = participants.map(async (p) => {
@@ -36,6 +36,7 @@ export async function runCrossReview(options) {
36
36
  messages: [{ role: "user", content: userContent }],
37
37
  signal,
38
38
  });
39
+ onUsage?.(response.usage);
39
40
  logger.info("arena.cross_review_raw_response", {
40
41
  participant: p.name,
41
42
  text: response.text,
@@ -56,6 +57,7 @@ export async function runCrossReview(options) {
56
57
  ],
57
58
  signal,
58
59
  });
60
+ onUsage?.(retryResponse.usage);
59
61
  logger.info("arena.cross_review_retry", {
60
62
  participant: p.name,
61
63
  stopReason: retryResponse.stopReason,
@@ -76,7 +78,7 @@ export async function runCrossReview(options) {
76
78
  * Falls back to V1 cross-review if strategy doesn't implement V2.
77
79
  */
78
80
  export async function runVerificationReview(options) {
79
- const { participants, strategy, topic, reports, ledger, limits, mode, signal, onProgress } = options;
81
+ const { participants, strategy, topic, reports, ledger, limits, mode, signal, onProgress, onUsage } = options;
80
82
  // Select claims to review, prioritized by severity + confidence
81
83
  const allClaims = ledger.getAllClaims();
82
84
  const claimsToReview = selectClaimsForReview(allClaims, limits.maxClaimsForReview);
@@ -120,6 +122,7 @@ export async function runVerificationReview(options) {
120
122
  messages: [{ role: "user", content: userContent }],
121
123
  signal,
122
124
  });
125
+ onUsage?.(response.usage);
123
126
  logger.info("arena.verification_review_raw", {
124
127
  participant: p.name,
125
128
  stopReason: response.stopReason,
@@ -134,6 +137,7 @@ export async function runVerificationReview(options) {
134
137
  ],
135
138
  signal,
136
139
  });
140
+ onUsage?.(retryResponse.usage);
137
141
  response = retryResponse;
138
142
  }
139
143
  const challenges = isPlanningMerge
@@ -163,6 +167,7 @@ export async function runVerificationReview(options) {
163
167
  messages: [{ role: "user", content: userContent }],
164
168
  signal,
165
169
  });
170
+ onUsage?.(response.usage);
166
171
  if (response.stopReason === "length") {
167
172
  const retryResponse = await client.createMessage({
168
173
  systemPrompt,
@@ -173,6 +178,7 @@ export async function runVerificationReview(options) {
173
178
  ],
174
179
  signal,
175
180
  });
181
+ onUsage?.(retryResponse.usage);
176
182
  response = retryResponse;
177
183
  }
178
184
  const reviews = strategy.parseCrossReviewResponse(p.name, response.text);
@@ -7,7 +7,7 @@
7
7
  * 2. Convergence check: all stances are "support" or "narrow" → resolved
8
8
  * 3. If max rounds exhausted → stays contested for adjudication
9
9
  */
10
- import type { ArenaParticipant, ArenaStrategy, DebateRound, ArenaExecutionLimits, ArenaProgressEvent } from "../types.js";
10
+ import type { ArenaParticipant, ArenaStrategy, DebateRound, ArenaExecutionLimits, ArenaProgressEvent, ArenaUsageRecorder } from "../types.js";
11
11
  import type { ArenaLedger } from "../ledger.js";
12
12
  interface DebateOptions {
13
13
  participants: ArenaParticipant[];
@@ -18,6 +18,7 @@ interface DebateOptions {
18
18
  maxRounds: number;
19
19
  signal?: AbortSignal;
20
20
  onProgress?: (event: ArenaProgressEvent) => void;
21
+ onUsage?: ArenaUsageRecorder;
21
22
  }
22
23
  /**
23
24
  * Run debate rounds for all contested claims.
@@ -17,7 +17,7 @@ import { logger } from "../../logging/logger.js";
17
17
  * Returns empty array if no contested claims exist.
18
18
  */
19
19
  export async function runDebateRounds(options) {
20
- const { participants, strategy, topic, ledger, limits, maxRounds, signal, onProgress } = options;
20
+ const { participants, strategy, topic, ledger, limits, maxRounds, signal, onProgress, onUsage } = options;
21
21
  // Select contested claims, capped
22
22
  const contested = ledger.getClaimsByStatus("contested")
23
23
  .slice(0, limits.maxContestedClaimsForDebate);
@@ -44,6 +44,7 @@ export async function runDebateRounds(options) {
44
44
  maxRounds,
45
45
  signal,
46
46
  onProgress,
47
+ onUsage,
47
48
  });
48
49
  allDebateRounds.push(...claimDebateRounds);
49
50
  // Attach debate rounds to claim record
@@ -52,7 +53,7 @@ export async function runDebateRounds(options) {
52
53
  return allDebateRounds;
53
54
  }
54
55
  async function debateClaim(options) {
55
- const { claim, participants, strategy, v2, topic, ledger, maxRounds, signal, onProgress } = options;
56
+ const { claim, participants, strategy, v2, topic, ledger, maxRounds, signal, onProgress, onUsage } = options;
56
57
  // Identify debaters: claim owner + primary challenger
57
58
  const owner = participants.find((p) => p.name === claim.owner);
58
59
  const primaryChallenger = findPrimaryChallenger(claim, participants);
@@ -91,6 +92,7 @@ async function debateClaim(options) {
91
92
  messages: [{ role: "user", content: userContent }],
92
93
  signal,
93
94
  });
95
+ onUsage?.(response.usage);
94
96
  logger.info("arena.debate_turn", {
95
97
  participant: p.name,
96
98
  claimId: claim.claimId,
@@ -5,7 +5,7 @@
5
5
  * Phase 3 upgrade: now captures ToolTrace and builds EvidencePackets,
6
6
  * outputting a full ResearchDossier alongside the ParticipantReport.
7
7
  */
8
- import type { ArenaBaseContext, ArenaParticipant, ArenaStrategy, ParticipantReport, ResearchDossier, ArenaProgressEvent } from "../types.js";
8
+ import type { ArenaBaseContext, ArenaParticipant, ArenaStrategy, ParticipantReport, ResearchDossier, ArenaProgressEvent, ArenaUsageRecorder } from "../types.js";
9
9
  import type { ToolDefinition } from "../../types.js";
10
10
  /** Result of a single participant's research — includes both report and dossier */
11
11
  export interface ResearchResult {
@@ -23,6 +23,7 @@ interface ResearchOptions {
23
23
  /** AbortSignal — cancels in-flight LLM calls */
24
24
  signal?: AbortSignal;
25
25
  onProgress?: (event: ArenaProgressEvent) => void;
26
+ onUsage?: ArenaUsageRecorder;
26
27
  }
27
28
  /**
28
29
  * Run participant research phase in parallel.
@@ -23,7 +23,7 @@ export async function runParticipantResearch(options) {
23
23
  * Returns both reports (backward compat) and dossiers (evidence trail).
24
24
  */
25
25
  export async function runParticipantResearchWithDossiers(options) {
26
- const { participants, strategy, topic, baseContext, enableContextTools, contextTools, signal, onProgress } = options;
26
+ const { participants, strategy, topic, baseContext, enableContextTools, contextTools, signal, onProgress, onUsage } = options;
27
27
  const tools = enableContextTools ? (contextTools ?? CONTEXT_TOOLS) : undefined;
28
28
  const tasks = participants.map(async (p) => {
29
29
  onProgress?.({ type: "research_start", participant: p.name });
@@ -57,6 +57,7 @@ export async function runParticipantResearchWithDossiers(options) {
57
57
  tools,
58
58
  signal,
59
59
  });
60
+ onUsage?.(response.usage);
60
61
  const toolNames = (response.toolCalls ?? []).map((tc) => tc.toolName);
61
62
  const hasTools = response.toolCalls && response.toolCalls.length > 0;
62
63
  logger.info("arena.research_round", {
@@ -158,6 +159,7 @@ export async function runParticipantResearchWithDossiers(options) {
158
159
  },
159
160
  ],
160
161
  });
162
+ onUsage?.(forceResponse.usage);
161
163
  logger.info("arena.research_force_conclude_response", {
162
164
  participant: p.name,
163
165
  textLen: forceResponse.text?.length ?? 0,
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Tool access is available so the LLM can verify file paths and interfaces.
11
11
  */
12
- import type { ArenaParticipant, ArenaRoadmapPhase, ArenaRoadmapPhaseDetail, ArenaProgressEvent, ArenaExecutionLimits, ArenaStrategyPlanning } from "../types.js";
12
+ import type { ArenaParticipant, ArenaRoadmapPhase, ArenaRoadmapPhaseDetail, ArenaProgressEvent, ArenaExecutionLimits, ArenaStrategyPlanning, ArenaUsageRecorder } from "../types.js";
13
13
  import type { ToolDefinition } from "../../types.js";
14
14
  import type { ArenaLedger } from "../ledger.js";
15
15
  interface DetailExpansionOptions {
@@ -27,6 +27,7 @@ interface DetailExpansionOptions {
27
27
  contextTools?: ToolDefinition[];
28
28
  signal?: AbortSignal;
29
29
  onProgress?: (event: ArenaProgressEvent) => void;
30
+ onUsage?: ArenaUsageRecorder;
30
31
  }
31
32
  /**
32
33
  * Expand each roadmap phase into a repo-level implementation plan.
@@ -22,7 +22,7 @@ const MAX_EXPANSION_TOOL_ROUNDS = 3;
22
22
  * The number of phases expanded is capped by limits.maxExpandedPhasesPerRun.
23
23
  */
24
24
  export async function runDetailExpansion(options) {
25
- const { concluder, strategy, topic, phases, ledger, limits, signal, onProgress } = options;
25
+ const { concluder, strategy, topic, phases, ledger, limits, signal, onProgress, onUsage } = options;
26
26
  const tools = options.enableContextTools !== false
27
27
  ? (options.contextTools ?? CONTEXT_TOOLS)
28
28
  : undefined;
@@ -56,6 +56,7 @@ export async function runDetailExpansion(options) {
56
56
  tools: offerTools,
57
57
  signal,
58
58
  });
59
+ onUsage?.(response.usage);
59
60
  const hasTools = response.toolCalls && response.toolCalls.length > 0;
60
61
  logger.info("arena.detail_expansion_round", {
61
62
  phase: phase.title,
@@ -12,6 +12,7 @@
12
12
  * LLM call that outputs a holistic execution plan.
13
13
  */
14
14
  import type { LLMConfig } from "../types.js";
15
+ import type { ArenaUsageRecorder } from "./types.js";
15
16
  import type { ArenaPlan, ArenaMode } from "./types.js";
16
17
  /** Explicit flags the user may pass via CLI or tool args */
17
18
  export interface PlannerFlags {
@@ -24,4 +25,4 @@ export interface PlannerFlags {
24
25
  *
25
26
  * Priority: explicit flags > LLM plan > safe defaults.
26
27
  */
27
- export declare function planArena(topic: string, llmConfig: LLMConfig, flags?: PlannerFlags, signal?: AbortSignal): Promise<ArenaPlan>;
28
+ export declare function planArena(topic: string, llmConfig: LLMConfig, flags?: PlannerFlags, signal?: AbortSignal, onUsage?: ArenaUsageRecorder): Promise<ArenaPlan>;
@@ -58,7 +58,7 @@ Respond ONLY with JSON, no markdown fences:
58
58
  *
59
59
  * Priority: explicit flags > LLM plan > safe defaults.
60
60
  */
61
- export async function planArena(topic, llmConfig, flags, signal) {
61
+ export async function planArena(topic, llmConfig, flags, signal, onUsage) {
62
62
  // If mode and base are both explicit, we can skip the LLM call
63
63
  if (flags?.mode && flags?.base) {
64
64
  return buildExplicitPlan(topic, flags);
@@ -75,6 +75,7 @@ export async function planArena(topic, llmConfig, flags, signal) {
75
75
  maxTokens: 1024,
76
76
  signal,
77
77
  });
78
+ onUsage?.(response.usage);
78
79
  const plan = parsePlanResponse(response.text, topic);
79
80
  // Explicit flags override LLM output
80
81
  if (flags?.mode)
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * V2 architecture: shared facts → independent research → findings → cross-review → consensus.
5
5
  */
6
- import type { ClientDefaults, LLMConfig } from "../types.js";
6
+ import type { ClientDefaults, LLMConfig, TokenUsage } from "../types.js";
7
+ /** Receives usage for each completed Arena provider request. */
8
+ export type ArenaUsageRecorder = (usage: TokenUsage | undefined) => void;
7
9
  /** Arena operating mode — determines prompts, output format, and default rounds */
8
10
  export type ArenaMode = "review" | "discussion" | "planning";
9
11
  /** Analysis lens — the perspective from which participants analyze */
@@ -337,6 +339,8 @@ export interface ArenaResultV2 {
337
339
  /** Claim adjudications (Phase 3+) */
338
340
  adjudications?: ClaimAdjudication[];
339
341
  consensus: ArenaConsensus;
342
+ /** Aggregate of every provider request made by this Arena run. */
343
+ usage: TokenUsage;
340
344
  }
341
345
  /**
342
346
  * ArenaStrategy — encapsulates mode-specific behavior for prompts and parsing.
@@ -33,6 +33,8 @@ import { createRunManager } from "../run/factory.js";
33
33
  import { startAutomation } from "../automation/index.js";
34
34
  import { CronStore, defaultCronStorePath } from "../automation/store.js";
35
35
  import { resolveLLMConfigForTag } from "../engine/resolve-llm-config.js";
36
+ import { randomUUID } from "node:crypto";
37
+ import { getApprovalRouter } from "../tool-system/permission.js";
36
38
  const cwd = process.env.AGENT_CWD ?? process.cwd();
37
39
  const port = Number(process.env.AGENT_TCP_PORT ?? "4321");
38
40
  const host = process.env.AGENT_TCP_HOST ?? "127.0.0.1";
@@ -113,9 +115,18 @@ const automation = startAutomation({
113
115
  // ── Serve over TCP ──────────────────────────────────────────────
114
116
  // One AgentServer per accepted connection, all sharing the same chatManager.
115
117
  const servers = new Set();
116
- listenTcp({ port, host }, (transport) => {
117
- const server = new AgentServer({ chatManager, transport });
118
+ listenTcp({ port, host }, (transport, socket) => {
119
+ const server = new AgentServer({
120
+ chatManager,
121
+ transport,
122
+ connectionId: randomUUID(),
123
+ approvalRouter: getApprovalRouter(),
124
+ });
118
125
  servers.add(server);
126
+ socket.once("close", () => {
127
+ server.disconnect();
128
+ servers.delete(server);
129
+ });
119
130
  })
120
131
  .then((listener) => {
121
132
  process.stderr.write(`[code-shell] automation server listening on ${host}:${listener.port}\n`);
@@ -30,7 +30,7 @@ export interface ContextManagerConfig {
30
30
  * Async function type for LLM summarization calls.
31
31
  * Injected by the Engine so the ContextManager doesn't depend on LLM directly.
32
32
  */
33
- export type SummarizeFn = (prompt: string) => Promise<string>;
33
+ export type SummarizeFn = (prompt: string, signal?: AbortSignal) => Promise<string>;
34
34
  export type CompactStrategy = "micro" | "summary" | "window" | "snip" | "emergency";
35
35
  export type OnCompactFn = (info: {
36
36
  strategy: CompactStrategy;
@@ -111,7 +111,7 @@ export declare class ContextManager {
111
111
  * Async context management — attempts LLM summarization before falling back.
112
112
  * Call this when you have access to the LLM (between turns).
113
113
  */
114
- manageAsync(messages: Message[]): Promise<Message[]>;
114
+ manageAsync(messages: Message[], signal?: AbortSignal): Promise<Message[]>;
115
115
  /**
116
116
  * Force maximum compaction, ignoring the ratio gates. This is what a manual
117
117
  * `/compact` invokes: the user explicitly asked to shrink NOW, so we don't
@@ -126,7 +126,7 @@ export declare class ContextManager {
126
126
  * a long-but-under-threshold text-only conversation (the /compact bug) still
127
127
  * gets summarized here.
128
128
  */
129
- forceSummarize(messages: Message[]): Promise<Message[]>;
129
+ forceSummarize(messages: Message[], signal?: AbortSignal): Promise<Message[]>;
130
130
  /**
131
131
  * Persist large tool_result blocks to disk and replace them with a
132
132
  * preview + filepath. No-op when transcript path hasn't been set
@@ -162,7 +162,7 @@ export class ContextManager {
162
162
  setSummarizeFn(fn) {
163
163
  this.summarizeFn = fn;
164
164
  }
165
- async trySummaryCompact(messages, before, logEvent) {
165
+ async trySummaryCompact(messages, before, logEvent, signal) {
166
166
  if (!this.summarizeFn || this.consecutiveSummaryFailures >= 3) {
167
167
  return { messages, tokens: before, compacted: false, noProgress: false };
168
168
  }
@@ -178,7 +178,7 @@ export class ContextManager {
178
178
  // re-summarizes from scratch.
179
179
  const priorSummary = extractAnchoredSummary(messages) ?? this.lastSummary;
180
180
  const prompt = buildSummarizationPrompt(messagesToSummarize, priorSummary);
181
- const summary = await this.summarizeFn(prompt);
181
+ const summary = await this.summarizeFn(prompt, signal);
182
182
  if (!summary || summary.length <= 50) {
183
183
  this.consecutiveSummaryFailures++;
184
184
  logger.warn("context.summary_failed", {
@@ -358,7 +358,7 @@ export class ContextManager {
358
358
  * Async context management — attempts LLM summarization before falling back.
359
359
  * Call this when you have access to the LLM (between turns).
360
360
  */
361
- async manageAsync(messages) {
361
+ async manageAsync(messages, signal) {
362
362
  let result = messages;
363
363
  // Tier 0a: Persist large tool_results to disk + replace with preview.
364
364
  result = this.persistLargeToolResults(result);
@@ -428,7 +428,7 @@ export class ContextManager {
428
428
  // Tier 2: LLM summary if approaching limit, or if micro was the only tier
429
429
  // available in the 0.70-0.85 band and it freed nothing.
430
430
  if (ratio >= this.config.compactAtRatio || shouldEscalateNoOpMicro) {
431
- const summarized = await this.trySummaryCompact(result, tokens, "context.summary_compact");
431
+ const summarized = await this.trySummaryCompact(result, tokens, "context.summary_compact", signal);
432
432
  result = summarized.messages;
433
433
  tokens = summarized.tokens;
434
434
  if (summarized.compacted) {
@@ -488,7 +488,7 @@ export class ContextManager {
488
488
  * a long-but-under-threshold text-only conversation (the /compact bug) still
489
489
  * gets summarized here.
490
490
  */
491
- async forceSummarize(messages) {
491
+ async forceSummarize(messages, signal) {
492
492
  let result = messages;
493
493
  // Tier 0: same waste-removal + micro as the automatic path.
494
494
  result = this.persistLargeToolResults(result);
@@ -505,7 +505,7 @@ export class ContextManager {
505
505
  let tokens = this.estimateTokensHybrid(result);
506
506
  // Unconditional LLM summary (no ratio gate).
507
507
  {
508
- const summarized = await this.trySummaryCompact(result, tokens, "context.force_summary_compact");
508
+ const summarized = await this.trySummaryCompact(result, tokens, "context.force_summary_compact", signal);
509
509
  result = summarized.messages;
510
510
  tokens = summarized.tokens;
511
511
  if (summarized.compacted)
@@ -15,6 +15,9 @@ const RATIOS = {
15
15
  mixed: 3.6, // Mixed content (typical for tool results)
16
16
  cjk: 1.5, // CJK characters: ~1.5 chars per token
17
17
  };
18
+ // Base64 is transport data, not text. A fixed vision estimate prevents large
19
+ // images from spuriously exhausting the text context window.
20
+ const IMAGE_TOKEN_ESTIMATE = 170;
18
21
  /**
19
22
  * Estimate token count for a string, detecting content type.
20
23
  */
@@ -80,12 +83,11 @@ function estimateBlockTokens(block) {
80
83
  if (block.name)
81
84
  tokens += estimateStringTokens(block.name);
82
85
  if (block.source?.data) {
83
- tokens += estimateStringTokens(block.source.media_type);
84
- tokens += estimateStringTokens(block.source.data);
86
+ tokens += IMAGE_TOKEN_ESTIMATE;
85
87
  }
86
88
  const maybeOpenAI = block;
87
89
  if (typeof maybeOpenAI.image_url?.url === "string") {
88
- tokens += estimateStringTokens(maybeOpenAI.image_url.url);
90
+ tokens += IMAGE_TOKEN_ESTIMATE;
89
91
  }
90
92
  return tokens;
91
93
  }
@@ -55,6 +55,12 @@ export declare function reconstructContentReplacementState(messages: Message[]):
55
55
  * transcript path. Layout: <transcriptDir>/tool-results/
56
56
  */
57
57
  export declare function resolveToolResultsDir(transcriptPath: string): string;
58
+ /**
59
+ * Convert an externally supplied tool_use_id to a bounded internal filename.
60
+ * The original id remains the key in ContentReplacementState/onPersist; it is
61
+ * never used as a path component.
62
+ */
63
+ export declare function safeToolResultFilename(toolUseId: string): string;
58
64
  interface PersistOptions {
59
65
  /** Per-result threshold; default DEFAULT_PERSIST_THRESHOLD. */
60
66
  perResultThreshold?: number;