@gajae-code/coding-agent 0.10.0 → 0.10.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 (47) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/types/cli.d.ts +1 -0
  3. package/dist/types/config/settings.d.ts +2 -0
  4. package/dist/types/coordinator/contract.d.ts +1 -1
  5. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  6. package/dist/types/coordinator-mcp/server.d.ts +17 -5
  7. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  9. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  10. package/dist/types/modes/components/status-line.d.ts +0 -10
  11. package/dist/types/modes/print-mode.d.ts +3 -4
  12. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  13. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  14. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  15. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  16. package/dist/types/session/agent-session.d.ts +14 -1
  17. package/dist/types/session/context-estimation.d.ts +31 -0
  18. package/package.json +7 -7
  19. package/src/cli.ts +1 -0
  20. package/src/config/settings.ts +96 -45
  21. package/src/coordinator/contract.ts +1 -0
  22. package/src/coordinator-mcp/policy.ts +30 -0
  23. package/src/coordinator-mcp/server.ts +294 -66
  24. package/src/coordinator-mcp/session-reaper.ts +129 -0
  25. package/src/extensibility/extensions/runner.ts +1 -1
  26. package/src/extensibility/extensions/types.ts +3 -1
  27. package/src/internal-urls/docs-index.generated.ts +4 -4
  28. package/src/main.ts +3 -5
  29. package/src/modes/acp/acp-agent.ts +2 -2
  30. package/src/modes/components/footer.ts +3 -3
  31. package/src/modes/components/status-line/segments.ts +13 -6
  32. package/src/modes/components/status-line/types.ts +1 -1
  33. package/src/modes/components/status-line.ts +4 -171
  34. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  35. package/src/modes/interactive-mode.ts +16 -0
  36. package/src/modes/print-mode.ts +228 -92
  37. package/src/modes/rpc/rpc-client.ts +58 -1
  38. package/src/modes/rpc/rpc-mode.ts +286 -85
  39. package/src/modes/rpc/rpc-types.ts +15 -1
  40. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  41. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  42. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  43. package/src/modes/utils/context-usage.ts +56 -92
  44. package/src/session/agent-session.ts +287 -85
  45. package/src/session/context-estimation.ts +91 -0
  46. package/src/slash-commands/builtin-registry.ts +12 -2
  47. package/src/slash-commands/helpers/context-report.ts +26 -4
@@ -3,16 +3,20 @@ import type { CompactionSettings } from "@gajae-code/agent-core/compaction";
3
3
  import {
4
4
  effectiveReserveTokens,
5
5
  estimateMessageTokensHeuristic,
6
- estimateTextTokensHeuristic,
7
6
  resolveThresholdTokens,
8
7
  } from "@gajae-code/agent-core/compaction";
9
8
  import type { Model } from "@gajae-code/ai";
10
9
  import { formatNumber } from "@gajae-code/utils";
11
- import type { Skill } from "../../extensibility/skills";
12
10
  import type { AgentSession } from "../../session/agent-session";
13
- import type { Tool } from "../../tools";
11
+ import { computeNonMessageBreakdown } from "../../session/context-estimation";
14
12
  import type { theme as Theme } from "../theme/theme";
15
13
 
14
+ export {
15
+ computeNonMessageTokens,
16
+ estimateSkillsTokens,
17
+ estimateToolSchemaTokens,
18
+ } from "../../session/context-estimation";
19
+
16
20
  const GRID_COLS = 20;
17
21
  const GRID_ROWS = 10;
18
22
  const GRID_CELLS = GRID_COLS * GRID_ROWS;
@@ -38,90 +42,13 @@ export interface ContextBreakdown {
38
42
  contextWindow: number;
39
43
  categories: CategoryInfo[];
40
44
  lastUserTurnTokens: number;
41
- usedTokens: number;
45
+ estimatedCategoryTotal: number;
46
+ usedTokens: number | null;
47
+ source: "provider_anchor" | "heuristic" | "unknown";
42
48
  autoCompactBufferTokens: number;
43
49
  freeTokens: number;
44
50
  }
45
51
 
46
- export function estimateSkillsTokens(skills: readonly Skill[]): number {
47
- const fragments: string[] = [];
48
- for (const skill of skills) {
49
- // "- name: description\n" wire framing tokenizes ~identically to the
50
- // concatenated form, so encode each piece separately and sum.
51
- fragments.push(skill.name, skill.description);
52
- }
53
- return estimateTextTokensHeuristic(fragments);
54
- }
55
-
56
- export function estimateToolSchemaTokens(
57
- tools: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>,
58
- ): number {
59
- const fragments: string[] = [];
60
- for (const tool of tools) {
61
- fragments.push(tool.name, tool.description);
62
- try {
63
- fragments.push(JSON.stringify(tool.parameters ?? {}));
64
- } catch {
65
- // Schema may contain functions or cycles; ignore.
66
- }
67
- }
68
- return estimateTextTokensHeuristic(fragments);
69
- }
70
-
71
- /**
72
- * Compute just the NON-MESSAGE token total: system prompt (with its skills
73
- * section subtracted, since skills are tokenized separately) + system context
74
- * (the rest of the system-prompt array) + tools + skills.
75
- *
76
- * Exposed so callers like `StatusLineComponent` can cache the non-message
77
- * total separately from the message total. Non-message inputs (skills,
78
- * tools, system prompt) change rarely; the message list grows on every
79
- * streaming turn. Splitting the two lets the caller refresh each on its own
80
- * cadence — non-message recomputed only when the inputs identity changes,
81
- * messages walked incrementally as new entries append.
82
- */
83
- export function computeNonMessageTokens(session: AgentSession): number {
84
- const parts = computeNonMessageBreakdown(session);
85
- return (
86
- parts.systemPromptTokens + parts.systemContextTokens + parts.rulesTokens + parts.toolsTokens + parts.skillsTokens
87
- );
88
- }
89
-
90
- /**
91
- * Shared helper for the four non-message token totals. Single source of truth
92
- * for both `computeNonMessageTokens` (status-line incremental cache) and
93
- * `computeContextBreakdown` (/context panel). The split avoids drift between
94
- * the two surfaces — they MUST report the same numbers.
95
- */
96
- function computeNonMessageBreakdown(session: AgentSession): {
97
- rulesTokens: number;
98
- skillsTokens: number;
99
- toolsTokens: number;
100
- systemContextTokens: number;
101
- systemPromptTokens: number;
102
- } {
103
- const skillsTokens = estimateSkillsTokens(session.skills ?? []);
104
- const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? []);
105
- const systemPromptParts = session.systemPrompt ?? [];
106
- const rulesTokens = estimateRulesTokens(systemPromptParts);
107
- const systemContextTokens = estimateTextTokensHeuristic(systemPromptParts.slice(1));
108
- const systemPromptTokens = Math.max(
109
- 0,
110
- estimateTextTokensHeuristic(systemPromptParts[0] ?? "") - skillsTokens - rulesTokens,
111
- );
112
- return { rulesTokens, skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens };
113
- }
114
-
115
- function estimateRulesTokens(systemPromptParts: readonly string[]): number {
116
- const fragments: string[] = [];
117
- for (const part of systemPromptParts) {
118
- for (const match of part.matchAll(/<rules>[\s\S]*?<\/rules>/g)) {
119
- fragments.push(match[0]);
120
- }
121
- }
122
- return fragments.length === 0 ? 0 : estimateTextTokensHeuristic(fragments);
123
- }
124
-
125
52
  function splitLastUserTurn(messages: readonly AgentMessage[]): {
126
53
  regularMessagesTokens: number;
127
54
  lastUserTurnTokens: number;
@@ -198,7 +125,11 @@ export function computeContextBreakdown(
198
125
  },
199
126
  ];
200
127
 
201
- const usedTokens = categories.reduce((sum, c) => sum + c.tokens, 0);
128
+ const estimatedCategoryTotal = categories.reduce((sum, c) => sum + c.tokens, 0);
129
+ const contextUsage = session.getContextUsage?.();
130
+ const source = contextUsage?.source ?? "heuristic";
131
+ const usedTokens = source === "unknown" ? null : (contextUsage?.tokens ?? estimatedCategoryTotal);
132
+ const tokensForFreeSpace = usedTokens ?? estimatedCategoryTotal;
202
133
 
203
134
  let autoCompactBufferTokens = 0;
204
135
  if (contextWindow > 0) {
@@ -214,16 +145,18 @@ export function computeContextBreakdown(
214
145
  autoCompactBufferTokens = effectiveReserveTokens(contextWindow, compactionSettings);
215
146
  }
216
147
  }
217
- autoCompactBufferTokens = Math.min(autoCompactBufferTokens, Math.max(0, contextWindow - usedTokens));
148
+ autoCompactBufferTokens = Math.min(autoCompactBufferTokens, Math.max(0, contextWindow - tokensForFreeSpace));
218
149
 
219
- const freeTokens = Math.max(0, contextWindow - usedTokens - autoCompactBufferTokens);
150
+ const freeTokens = Math.max(0, contextWindow - tokensForFreeSpace - autoCompactBufferTokens);
220
151
 
221
152
  return {
222
153
  model,
223
154
  contextWindow,
224
155
  categories,
225
156
  lastUserTurnTokens,
157
+ estimatedCategoryTotal,
226
158
  usedTokens,
159
+ source,
227
160
  autoCompactBufferTokens,
228
161
  freeTokens,
229
162
  };
@@ -310,18 +243,48 @@ function percentString(part: number, whole: number, fractionDigits = 1): string
310
243
 
311
244
  function buildLegendLines(breakdown: ContextBreakdown, theme: typeof Theme): string[] {
312
245
  const lines: string[] = [];
313
- const { model, contextWindow, categories, usedTokens, autoCompactBufferTokens, freeTokens } = breakdown;
246
+ const {
247
+ model,
248
+ contextWindow,
249
+ categories,
250
+ estimatedCategoryTotal,
251
+ usedTokens,
252
+ source,
253
+ autoCompactBufferTokens,
254
+ freeTokens,
255
+ } = breakdown;
314
256
 
315
257
  const modelName = model?.name ?? model?.id ?? "no model";
316
258
  const modelId = model?.id ?? "unknown";
317
259
  const windowLabel = formatNumber(contextWindow).toLowerCase();
260
+ const totalSourceLabel =
261
+ source === "provider_anchor"
262
+ ? "provider-reported"
263
+ : source === "unknown"
264
+ ? "estimated; exact count unknown until next response"
265
+ : "estimated";
318
266
 
319
267
  lines.push(theme.bold(`${modelName}`) + theme.fg("dim", ` (${windowLabel} context)`));
320
268
  lines.push(theme.fg("muted", `${modelId}[${windowLabel}]`));
321
- lines.push(
322
- `${theme.bold(formatNumber(usedTokens))}${theme.fg("dim", `/${windowLabel} tokens`)}` +
323
- theme.fg("muted", ` (${percentString(usedTokens, contextWindow)})`),
324
- );
269
+ if (usedTokens === null) {
270
+ lines.push(
271
+ `${theme.bold("unknown")}${theme.fg("dim", `/${windowLabel} tokens`)}` +
272
+ theme.fg("muted", " (exact count unknown until next response)"),
273
+ );
274
+ } else {
275
+ lines.push(
276
+ `${theme.bold(formatNumber(usedTokens))}${theme.fg("dim", `/${windowLabel} tokens`)}` +
277
+ theme.fg("muted", ` (${percentString(usedTokens, contextWindow)}) (${totalSourceLabel})`),
278
+ );
279
+ }
280
+ if (source !== "unknown" && usedTokens !== null && estimatedCategoryTotal !== usedTokens) {
281
+ lines.push(
282
+ theme.fg(
283
+ "muted",
284
+ `Estimated category total: ${formatNumber(estimatedCategoryTotal)} tokens (composition below is estimated)`,
285
+ ),
286
+ );
287
+ }
325
288
  lines.push("");
326
289
  lines.push(theme.fg("muted", "Estimated usage by category"));
327
290
 
@@ -334,8 +297,9 @@ function buildLegendLines(breakdown: ContextBreakdown, theme: typeof Theme): str
334
297
  }
335
298
 
336
299
  const freeDot = theme.fg("dim", CELL_FREE);
300
+ const freeLabel = usedTokens === null ? "Free space (estimated)" : "Free space";
337
301
  lines.push(
338
- `${freeDot} Free space: ${theme.bold(formatNumber(freeTokens))} ${theme.fg("dim", `(${percentString(freeTokens, contextWindow)})`)}`,
302
+ `${freeDot} ${freeLabel}: ${theme.bold(formatNumber(freeTokens))} ${theme.fg("dim", `(${percentString(freeTokens, contextWindow)})`)}`,
339
303
  );
340
304
 
341
305
  if (autoCompactBufferTokens > 0) {