@robota-sdk/agent-command 3.0.0-beta.100

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 (251) hide show
  1. package/CHANGELOG.md +1351 -0
  2. package/LICENSE +661 -0
  3. package/README.md +100 -0
  4. package/dist/node/index.cjs +121 -0
  5. package/dist/node/index.d.cts +1053 -0
  6. package/dist/node/index.d.cts.map +1 -0
  7. package/dist/node/index.d.ts +1053 -0
  8. package/dist/node/index.d.ts.map +1 -0
  9. package/dist/node/index.js +122 -0
  10. package/dist/node/index.js.map +1 -0
  11. package/package.json +83 -0
  12. package/src/advisor/advisor-command-module.ts +47 -0
  13. package/src/advisor/advisor-command.test.ts +102 -0
  14. package/src/advisor/advisor-command.ts +55 -0
  15. package/src/advisor/advisor-cost-functional.test.ts +92 -0
  16. package/src/advisor/index.ts +6 -0
  17. package/src/agent/__tests__/agent-command.test.ts +597 -0
  18. package/src/agent/agent-command-module.ts +89 -0
  19. package/src/agent/agent-command-parser.ts +180 -0
  20. package/src/agent/agent-command.ts +255 -0
  21. package/src/agent/index.ts +7 -0
  22. package/src/background/__tests__/background-command-module.test.ts +261 -0
  23. package/src/background/background-command-module.ts +52 -0
  24. package/src/background/background-command.ts +65 -0
  25. package/src/background/index.ts +6 -0
  26. package/src/command-module-utils.test.ts +37 -0
  27. package/src/command-module-utils.ts +35 -0
  28. package/src/compact/__tests__/compact-command-module.test.ts +181 -0
  29. package/src/compact/compact-command-module.ts +59 -0
  30. package/src/compact/compact-command.ts +33 -0
  31. package/src/compact/index.ts +6 -0
  32. package/src/context/__tests__/context-command-module.test.ts +502 -0
  33. package/src/context/auto-compact-format.ts +25 -0
  34. package/src/context/context-breakdown.ts +295 -0
  35. package/src/context/context-command-module.ts +82 -0
  36. package/src/context/context-command.ts +291 -0
  37. package/src/context/index.ts +6 -0
  38. package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +121 -0
  39. package/src/default/__tests__/default-command-modules.test.ts +192 -0
  40. package/src/default/__tests__/model-exposure.test.ts +269 -0
  41. package/src/default/__tests__/org-policy-forwarding.test.ts +106 -0
  42. package/src/default/__tests__/provider-environment-forwarding.test.ts +98 -0
  43. package/src/default/default-command-modules.ts +247 -0
  44. package/src/default/index.ts +5 -0
  45. package/src/devices/__tests__/devices-command-module.test.ts +299 -0
  46. package/src/devices/devices-command-module.ts +308 -0
  47. package/src/devices/devices-command-port.ts +143 -0
  48. package/src/devices/index.ts +14 -0
  49. package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
  50. package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
  51. package/src/doctor/__tests__/doctor-fixture.ts +146 -0
  52. package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
  53. package/src/doctor/__tests__/doctor-runner.test.ts +484 -0
  54. package/src/doctor/doctor-command-module.ts +132 -0
  55. package/src/doctor/doctor-extensions-probe.ts +323 -0
  56. package/src/doctor/doctor-node-deps.ts +88 -0
  57. package/src/doctor/doctor-redaction.ts +99 -0
  58. package/src/doctor/doctor-render.ts +70 -0
  59. package/src/doctor/doctor-repair.ts +136 -0
  60. package/src/doctor/doctor-runner.ts +122 -0
  61. package/src/doctor/doctor-settings-probe.ts +239 -0
  62. package/src/doctor/doctor-storage-probe.ts +187 -0
  63. package/src/doctor/doctor-types.ts +109 -0
  64. package/src/doctor/index.ts +32 -0
  65. package/src/editor/__tests__/editor-command-functional.test.ts +124 -0
  66. package/src/editor/editor-command-module.ts +53 -0
  67. package/src/editor/editor-command.ts +58 -0
  68. package/src/editor/index.ts +7 -0
  69. package/src/editor/resolve-editor.ts +21 -0
  70. package/src/effort/effort-command-module.ts +51 -0
  71. package/src/effort/effort-command.test.ts +203 -0
  72. package/src/effort/effort-command.ts +136 -0
  73. package/src/effort/index.ts +6 -0
  74. package/src/events/__tests__/events-command.test.ts +73 -0
  75. package/src/events/events-command-module.ts +49 -0
  76. package/src/events/events-command.ts +69 -0
  77. package/src/events/index.ts +2 -0
  78. package/src/exit/__tests__/exit-command-module.test.ts +61 -0
  79. package/src/exit/exit-command-module.ts +47 -0
  80. package/src/exit/exit-command.ts +26 -0
  81. package/src/exit/index.ts +6 -0
  82. package/src/fork/__tests__/fork-command.test.ts +184 -0
  83. package/src/fork/fork-command-module.ts +61 -0
  84. package/src/fork/fork-command.ts +162 -0
  85. package/src/fork/index.ts +8 -0
  86. package/src/git/__tests__/fake-git-port.ts +35 -0
  87. package/src/git/__tests__/git-command-module.test.ts +111 -0
  88. package/src/git/__tests__/git-commit.test.ts +185 -0
  89. package/src/git/git-command-module.ts +138 -0
  90. package/src/git/git-commit.ts +254 -0
  91. package/src/git/index.ts +20 -0
  92. package/src/goal/__tests__/goal-command.test.ts +80 -0
  93. package/src/goal/goal-command-module.ts +49 -0
  94. package/src/goal/goal-command.ts +69 -0
  95. package/src/goal/index.ts +6 -0
  96. package/src/handoff/__tests__/handoff-command-module.test.ts +25 -0
  97. package/src/handoff/__tests__/handoff-command.test.ts +218 -0
  98. package/src/handoff/handoff-command-module.ts +49 -0
  99. package/src/handoff/handoff-command.ts +187 -0
  100. package/src/handoff/index.ts +6 -0
  101. package/src/help/__tests__/help-command-module.test.ts +134 -0
  102. package/src/help/__tests__/help-command.test.ts +130 -0
  103. package/src/help/help-command-module.ts +47 -0
  104. package/src/help/help-command.ts +11 -0
  105. package/src/help/index.ts +6 -0
  106. package/src/index.ts +163 -0
  107. package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
  108. package/src/keybindings/index.ts +6 -0
  109. package/src/keybindings/keybindings-command-module.ts +88 -0
  110. package/src/language/__tests__/language-command-module.test.ts +126 -0
  111. package/src/language/index.ts +6 -0
  112. package/src/language/language-command-module.ts +55 -0
  113. package/src/language/language-command.ts +52 -0
  114. package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +895 -0
  115. package/src/mcp-activation/__tests__/mcp-model-view.test.ts +242 -0
  116. package/src/mcp-activation/__tests__/mcp-skill-command.test.ts +141 -0
  117. package/src/mcp-activation/index.ts +12 -0
  118. package/src/mcp-activation/mcp-activation-command-module.ts +123 -0
  119. package/src/mcp-activation/mcp-activation-command.ts +662 -0
  120. package/src/mcp-activation/mcp-model-notice.ts +99 -0
  121. package/src/mcp-activation/mcp-model-status.ts +104 -0
  122. package/src/memory/__tests__/memory-command-module.test.ts +476 -0
  123. package/src/memory/__tests__/memory-command-unavailable.test.ts +63 -0
  124. package/src/memory/index.ts +6 -0
  125. package/src/memory/memory-command-module.ts +77 -0
  126. package/src/memory/memory-command.ts +326 -0
  127. package/src/mode/__tests__/mode-command-module.test.ts +230 -0
  128. package/src/mode/index.ts +6 -0
  129. package/src/mode/mode-command-module.ts +55 -0
  130. package/src/mode/mode-command.ts +67 -0
  131. package/src/model/__tests__/model-command-module.test.ts +80 -0
  132. package/src/model/__tests__/model-command.test.ts +265 -0
  133. package/src/model/index.ts +6 -0
  134. package/src/model/model-command-module.ts +56 -0
  135. package/src/model/model-command.ts +249 -0
  136. package/src/output-style/__tests__/output-style-command.test.ts +96 -0
  137. package/src/output-style/index.ts +6 -0
  138. package/src/output-style/output-style-command-module.ts +143 -0
  139. package/src/peers/__tests__/peers-command-module.test.ts +24 -0
  140. package/src/peers/__tests__/peers-command.test.ts +399 -0
  141. package/src/peers/index.ts +9 -0
  142. package/src/peers/peers-command-module.ts +49 -0
  143. package/src/peers/peers-command.ts +220 -0
  144. package/src/permissions/__tests__/permissions-command-module.test.ts +276 -0
  145. package/src/permissions/index.ts +6 -0
  146. package/src/permissions/permissions-command-module.ts +55 -0
  147. package/src/permissions/permissions-command.ts +89 -0
  148. package/src/plan/__tests__/plan-command.test.ts +129 -0
  149. package/src/plan/index.ts +9 -0
  150. package/src/plan/plan-command-module.ts +49 -0
  151. package/src/plan/plan-command.ts +92 -0
  152. package/src/plugin/__tests__/plugin-active-turn.test.ts +100 -0
  153. package/src/plugin/__tests__/plugin-command-module.test.ts +300 -0
  154. package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
  155. package/src/plugin/index.ts +7 -0
  156. package/src/plugin/plugin-command-module.ts +85 -0
  157. package/src/plugin/plugin-command.ts +271 -0
  158. package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
  159. package/src/preset/__tests__/preset-command-module.test.ts +220 -0
  160. package/src/preset/index.ts +6 -0
  161. package/src/preset/preset-command-module.ts +80 -0
  162. package/src/preset/preset-command.ts +148 -0
  163. package/src/provider/__tests__/optional-provider-credentials.test.ts +130 -0
  164. package/src/provider/__tests__/org-policy.test.ts +174 -0
  165. package/src/provider/__tests__/provider-command-module.test.ts +884 -0
  166. package/src/provider/__tests__/provider-connection-setup.test.ts +567 -0
  167. package/src/provider/__tests__/provider-reference-switch.test.ts +113 -0
  168. package/src/provider/__tests__/provider-setup-flow.test.ts +244 -0
  169. package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
  170. package/src/provider/__tests__/scripted-interaction.ts +40 -0
  171. package/src/provider/index.ts +32 -0
  172. package/src/provider/provider-command-execution.ts +235 -0
  173. package/src/provider/provider-command-module.ts +82 -0
  174. package/src/provider/provider-command-profile-lifecycle.ts +239 -0
  175. package/src/provider/provider-command-profile-operations.ts +218 -0
  176. package/src/provider/provider-command-profile.ts +116 -0
  177. package/src/provider/provider-command-setup.ts +79 -0
  178. package/src/provider/provider-connection-setup.ts +287 -0
  179. package/src/provider/provider-connection-validation.ts +38 -0
  180. package/src/provider/provider-onboarding.ts +73 -0
  181. package/src/provider/provider-prompt-interaction.ts +28 -0
  182. package/src/provider/provider-setup-ask.ts +67 -0
  183. package/src/provider/provider-setup-flow.ts +326 -0
  184. package/src/provider/provider-startup.ts +225 -0
  185. package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
  186. package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
  187. package/src/remote-control/index.ts +8 -0
  188. package/src/remote-control/remote-control-command-module.ts +61 -0
  189. package/src/remote-control/remote-control-command.ts +138 -0
  190. package/src/reset/__tests__/reset-command-module.test.ts +63 -0
  191. package/src/reset/index.ts +2 -0
  192. package/src/reset/reset-command-module.ts +47 -0
  193. package/src/reset/reset-command.ts +10 -0
  194. package/src/rewind/__tests__/rewind-command-module.test.ts +287 -0
  195. package/src/rewind/index.ts +2 -0
  196. package/src/rewind/rewind-command-module.ts +56 -0
  197. package/src/rewind/rewind-command.ts +279 -0
  198. package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
  199. package/src/sandbox/index.ts +2 -0
  200. package/src/sandbox/sandbox-command-module.ts +56 -0
  201. package/src/sandbox/sandbox-command.ts +94 -0
  202. package/src/schedule/__tests__/loop-command-functional.test.ts +399 -0
  203. package/src/schedule/__tests__/loop-command.test.ts +594 -0
  204. package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
  205. package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
  206. package/src/schedule/__tests__/schedule-command.test.ts +247 -0
  207. package/src/schedule/__tests__/schedule-redos.test.ts +114 -0
  208. package/src/schedule/index.ts +12 -0
  209. package/src/schedule/loop-command.ts +427 -0
  210. package/src/schedule/loop-jitter.ts +41 -0
  211. package/src/schedule/schedule-command-module.ts +184 -0
  212. package/src/schedule/schedule-command.ts +161 -0
  213. package/src/schedule/schedule-spec-parser.ts +70 -0
  214. package/src/session/__tests__/model-pricing.test.ts +66 -0
  215. package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
  216. package/src/session/__tests__/session-command-module.test.ts +589 -0
  217. package/src/session/index.ts +17 -0
  218. package/src/session/model-pricing.ts +26 -0
  219. package/src/session/session-command-module.ts +197 -0
  220. package/src/session/session-command.ts +272 -0
  221. package/src/settings/index.ts +7 -0
  222. package/src/settings/settings-command-module.ts +47 -0
  223. package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
  224. package/src/shell/__tests__/shell-command-functional.test.ts +118 -0
  225. package/src/shell/index.ts +8 -0
  226. package/src/shell/resolve-shell.ts +25 -0
  227. package/src/shell/shell-command-module.ts +53 -0
  228. package/src/shell/shell-command.ts +48 -0
  229. package/src/shell/spawn-inherited.ts +32 -0
  230. package/src/skills/__tests__/skill-read-command.test.ts +64 -0
  231. package/src/skills/__tests__/skills-command-module.test.ts +200 -0
  232. package/src/skills/index.ts +7 -0
  233. package/src/skills/skill-read-command.ts +60 -0
  234. package/src/skills/skills-command-module.ts +76 -0
  235. package/src/skills/skills-command.ts +108 -0
  236. package/src/statusline/__tests__/statusline-command-module.test.ts +96 -0
  237. package/src/statusline/index.ts +6 -0
  238. package/src/statusline/statusline-command-module.ts +55 -0
  239. package/src/statusline/statusline-command.ts +80 -0
  240. package/src/terminal-client/__tests__/terminal-client-commands.test.ts +216 -0
  241. package/src/terminal-client/index.ts +5 -0
  242. package/src/terminal-client/terminal-client-commands.ts +109 -0
  243. package/src/theme/__tests__/theme-command.test.ts +204 -0
  244. package/src/theme/index.ts +12 -0
  245. package/src/theme/theme-command-module.ts +221 -0
  246. package/src/user-local/__tests__/user-local-command.test.ts +180 -0
  247. package/src/user-local/index.ts +13 -0
  248. package/src/user-local/user-local-command-constants.ts +5 -0
  249. package/src/user-local/user-local-command-module.ts +65 -0
  250. package/src/user-local/user-local-command.ts +214 -0
  251. package/src/user-local/user-local-memory-command.ts +160 -0
@@ -0,0 +1,295 @@
1
+ /**
2
+ * The `/context` transcript breakdown: token accounting over the history plus its rendering.
3
+ *
4
+ * Split out of `context-command.ts` when that file passed its size floor. The seam is a real one and
5
+ * the role ports named it: this half only READS (`TContextReadHost`), while the command surface it
6
+ * came from dispatches subcommands and writes thresholds and references.
7
+ */
8
+
9
+ import { CONTEXT_ESTIMATE_CHARS_PER_TOKEN, estimateToolSchemaTokens } from '@robota-sdk/agent-core';
10
+ import {
11
+ listCommandContextReferences,
12
+ readAutoCompactThreshold,
13
+ readAutoCompactThresholdSource,
14
+ readCommandContextState,
15
+ } from '@robota-sdk/agent-framework';
16
+
17
+ import { formatAutoCompactLine } from './auto-compact-format.js';
18
+
19
+ import type { IHistoryEntry, TUniversalMessage } from '@robota-sdk/agent-core';
20
+ import type {
21
+ ICommandHostContextReferences,
22
+ ICommandHostContextWindow,
23
+ ICommandHostSessionAccess,
24
+ } from '@robota-sdk/agent-framework';
25
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
26
+ import type { IContextReferenceItem } from '@robota-sdk/agent-interface-session';
27
+
28
+ /** What the breakdown reads — no adapter role, no writes. */
29
+ type TContextReadHost = ICommandHostContextReferences &
30
+ ICommandHostContextWindow &
31
+ ICommandHostSessionAccess;
32
+
33
+ const TOOL_ARG_MAX_LEN = 60;
34
+
35
+ function estimateTokens(charLength: number): number {
36
+ return Math.ceil(charLength / CONTEXT_ESTIMATE_CHARS_PER_TOKEN);
37
+ }
38
+
39
+ export function formatContextReferenceLine(reference: IContextReferenceItem): string {
40
+ return [
41
+ reference.relativePath,
42
+ `[${reference.loadType}, ${reference.status}]`,
43
+ `~${estimateTokens(reference.byteLength).toLocaleString()} tokens`,
44
+ ].join(' ');
45
+ }
46
+
47
+ // ── History analysis ────────────────────────────────────────────────────────
48
+
49
+ interface IToolResultSummary {
50
+ toolName: string;
51
+ displayArg: string;
52
+ }
53
+
54
+ interface IMessageTokensByRole {
55
+ systemTokens: number;
56
+ userTokens: number;
57
+ userCount: number;
58
+ assistantTokens: number;
59
+ assistantCount: number;
60
+ toolTokens: number;
61
+ toolCallCount: number;
62
+ totalTokens: number;
63
+ }
64
+
65
+ interface IToolDisplayList {
66
+ turnCount: number;
67
+ toolResults: IToolResultSummary[];
68
+ totalToolCallCount: number;
69
+ }
70
+
71
+ function parseToolCallArgs(argsJson: string): Record<string, unknown> | null {
72
+ try {
73
+ return JSON.parse(argsJson) as Record<string, unknown>;
74
+ } catch {
75
+ // allow-fallback: tool call arguments JSON may be malformed; display-only degradation, not a terminal path
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function extractFirstToolArg(argsJson: string): string {
81
+ const parsed = parseToolCallArgs(argsJson);
82
+ if (parsed === null) return '';
83
+ const first = Object.values(parsed)[0];
84
+ const raw = typeof first === 'string' ? first : JSON.stringify(first);
85
+ return raw.length > TOOL_ARG_MAX_LEN ? `${raw.slice(0, TOOL_ARG_MAX_LEN)}…` : raw;
86
+ }
87
+
88
+ function computeMessageTokensByRole(rawMessages: TUniversalMessage[]): IMessageTokensByRole {
89
+ let systemTokens = 0;
90
+ let userTokens = 0;
91
+ let userCount = 0;
92
+ let assistantTokens = 0;
93
+ let assistantCount = 0;
94
+ let toolTokens = 0;
95
+ let toolCallCount = 0;
96
+
97
+ for (const msg of rawMessages) {
98
+ const t = Math.ceil(JSON.stringify(msg).length / CONTEXT_ESTIMATE_CHARS_PER_TOKEN);
99
+ if (msg.role === 'system') {
100
+ systemTokens += t;
101
+ } else if (msg.role === 'user') {
102
+ userTokens += t;
103
+ userCount++;
104
+ } else if (msg.role === 'assistant') {
105
+ assistantTokens += t;
106
+ assistantCount++;
107
+ } else if (msg.role === 'tool') {
108
+ toolTokens += t;
109
+ toolCallCount++;
110
+ }
111
+ }
112
+
113
+ return {
114
+ systemTokens,
115
+ userTokens,
116
+ userCount,
117
+ assistantTokens,
118
+ assistantCount,
119
+ toolTokens,
120
+ toolCallCount,
121
+ totalTokens: systemTokens + userTokens + assistantTokens + toolTokens,
122
+ };
123
+ }
124
+
125
+ /** One conversation item as the display reads it: its role and its message payload. */
126
+ interface IChatItem {
127
+ readonly type: string;
128
+ readonly data: unknown;
129
+ }
130
+
131
+ /** Every chat entry of the full history, including turns compaction has since summarized away. */
132
+ export function buildToolDisplayList(history: IHistoryEntry[]): IToolDisplayList {
133
+ return buildDisplayList(
134
+ history
135
+ .filter((entry) => entry.category === 'chat')
136
+ .map((entry) => ({ type: entry.type, data: entry.data })),
137
+ );
138
+ }
139
+
140
+ /** Only what the model still has in context — what `/context list` accounts for. */
141
+ export function buildLiveToolDisplayList(messages: readonly TUniversalMessage[]): IToolDisplayList {
142
+ return buildDisplayList(messages.map((message) => ({ type: message.role, data: message })));
143
+ }
144
+
145
+ function buildDisplayList(items: readonly IChatItem[]): IToolDisplayList {
146
+ type TToolCallEntry = { name: string; firstArg: string };
147
+ const toolCallMap = new Map<string, TToolCallEntry>();
148
+
149
+ for (const entry of items) {
150
+ if (entry.type !== 'assistant') continue;
151
+ const data = entry.data as {
152
+ toolCalls?: Array<{ id: string; function: { name: string; arguments: string } }>;
153
+ };
154
+ for (const tc of data.toolCalls ?? []) {
155
+ toolCallMap.set(tc.id, {
156
+ name: tc.function.name,
157
+ firstArg: extractFirstToolArg(tc.function.arguments),
158
+ });
159
+ }
160
+ }
161
+
162
+ let turnCount = 0;
163
+ let totalToolCallCount = 0;
164
+ const toolDisplayMap = new Map<string, IToolResultSummary>();
165
+
166
+ for (const entry of items) {
167
+ if (entry.type === 'user') {
168
+ turnCount++;
169
+ } else if (entry.type === 'tool') {
170
+ const data = entry.data as { toolCallId?: string };
171
+ const tc = toolCallMap.get(data.toolCallId ?? '');
172
+ const toolName = tc?.name ?? 'tool';
173
+ const displayArg = tc?.firstArg ?? '';
174
+ totalToolCallCount++;
175
+ toolDisplayMap.set(`${toolName}:${displayArg}`, { toolName, displayArg });
176
+ }
177
+ }
178
+
179
+ return {
180
+ turnCount,
181
+ toolResults: [...toolDisplayMap.values()],
182
+ totalToolCallCount,
183
+ };
184
+ }
185
+
186
+ // ── Full context breakdown (for /context list) ──────────────────────────────
187
+
188
+ function formatSection(title: string, tokens: number, lines: string[]): string {
189
+ const tokenLabel = tokens > 0 ? ` — ~${tokens.toLocaleString()} tokens` : '';
190
+ const header = `${title}${tokenLabel}:`;
191
+ return lines.length === 0
192
+ ? `${header}\n (none)`
193
+ : [header, ...lines.map((l) => ` ${l}`)].join('\n');
194
+ }
195
+
196
+ export function formatFullContextBreakdown(context: TContextReadHost): ICommandResult {
197
+ const state = readCommandContextState(context);
198
+ const autoCompactThreshold = readAutoCompactThreshold(context);
199
+ const autoCompactThresholdSource = readAutoCompactThresholdSource(context);
200
+ const rawMessages = context.getSession().getHistory();
201
+ const msgTokens = computeMessageTokensByRole(rawMessages);
202
+ // CLI-1990: what the TOOL SCHEMAS cost on every request — the offered set, so a deferred tool that
203
+ // has not been loaded is not counted, because it is not sent. Until now nothing separated this
204
+ // from the system prompt it is billed alongside, so the saving deferral produces had no observable.
205
+ const offeredToolSchemas = context.getSession().getOfferedToolSchemas();
206
+ const toolSchemaTokens = estimateToolSchemaTokens(offeredToolSchemas);
207
+ // The live messages, like the token counts beside it: a turn compaction summarized away is no
208
+ // longer in context, so listing its tool results would describe a context the model does not have.
209
+ const display = buildLiveToolDisplayList(rawMessages);
210
+ const references = listCommandContextReferences(context);
211
+
212
+ const systemRefs = references.filter((r) => r.loadType === 'system');
213
+ const manualRefs = references.filter((r) => r.loadType === 'manual');
214
+ const promptRefs = references.filter((r) => r.loadType === 'prompt-reference');
215
+
216
+ const systemRefTokens = systemRefs.reduce((s, r) => s + estimateTokens(r.byteLength), 0);
217
+ const manualTokens = manualRefs.reduce((s, r) => s + estimateTokens(r.byteLength), 0);
218
+ const promptRefTokens = promptRefs.reduce((s, r) => s + estimateTokens(r.byteLength), 0);
219
+
220
+ const convTurnLabel =
221
+ display.turnCount === 0
222
+ ? 'Conversation history — 0 turns'
223
+ : `Conversation history — ${display.turnCount} turn${display.turnCount !== 1 ? 's' : ''} | ~${msgTokens.totalTokens.toLocaleString()} tokens`;
224
+
225
+ const toolResultLines = display.toolResults.map(
226
+ (t) => `${t.toolName}${t.displayArg ? `: ${t.displayArg}` : ''}`,
227
+ );
228
+
229
+ const uniqueToolCount = display.toolResults.length;
230
+ const totalToolCallCount = display.totalToolCallCount;
231
+ const toolResultsLabel =
232
+ totalToolCallCount === 0
233
+ ? 'Tool results (0): (none)'
234
+ : uniqueToolCount < totalToolCallCount
235
+ ? `Tool results (${uniqueToolCount} unique / ${totalToolCallCount} calls, ~${msgTokens.toolTokens.toLocaleString()} tokens):`
236
+ : `Tool results (${uniqueToolCount}, ~${msgTokens.toolTokens.toLocaleString()} tokens):`;
237
+
238
+ const conversationLines: string[] =
239
+ display.turnCount === 0
240
+ ? []
241
+ : [
242
+ `User (${msgTokens.userCount}): ~${msgTokens.userTokens.toLocaleString()} tokens`,
243
+ `Assistant (${msgTokens.assistantCount}): ~${msgTokens.assistantTokens.toLocaleString()} tokens`,
244
+ ...(msgTokens.systemTokens > 0
245
+ ? [`System messages: ~${msgTokens.systemTokens.toLocaleString()} tokens`]
246
+ : []),
247
+ totalToolCallCount > 0
248
+ ? [toolResultsLabel, ...toolResultLines.map((l) => ` ${l}`)].join('\n')
249
+ : toolResultsLabel,
250
+ ];
251
+
252
+ const convSection =
253
+ display.turnCount === 0
254
+ ? `${convTurnLabel}:\n (none)`
255
+ : [`${convTurnLabel}:`, ...conversationLines.map((l) => ` ${l}`)].join('\n');
256
+
257
+ const message = [
258
+ `Context: ${state.usedTokens.toLocaleString()} / ${state.maxTokens.toLocaleString()} tokens (${Math.round(state.usedPercentage)}%)`,
259
+ formatAutoCompactLine(autoCompactThreshold, autoCompactThresholdSource),
260
+ '',
261
+ formatSection(
262
+ 'System prompt (active every turn)',
263
+ systemRefTokens,
264
+ systemRefs.map(formatContextReferenceLine),
265
+ ),
266
+ '',
267
+ formatSection(
268
+ 'Tool schemas (sent every turn)',
269
+ toolSchemaTokens,
270
+ offeredToolSchemas.map((schema) => schema.name),
271
+ ),
272
+ '',
273
+ convSection,
274
+ '',
275
+ formatSection('Manually added', manualTokens, manualRefs.map(formatContextReferenceLine)),
276
+ '',
277
+ formatSection(
278
+ 'Prompt references (@-syntax)',
279
+ promptRefTokens,
280
+ promptRefs.map(formatContextReferenceLine),
281
+ ),
282
+ ].join('\n');
283
+
284
+ return {
285
+ success: true,
286
+ message,
287
+ data: {
288
+ references,
289
+ history: { turnCount: display.turnCount, toolResults: display.toolResults },
290
+ // CLI-1990: machine-readable beside the rendered line, so a caller can compare the figure
291
+ // across configurations rather than parsing the message back out.
292
+ toolSchemaTokens,
293
+ },
294
+ };
295
+ }
@@ -0,0 +1,82 @@
1
+ import { executeContextCommand } from './context-command.js';
2
+ import { createSystemCommandFromEntry } from '../command-module-utils.js';
3
+
4
+ import type { ICommandModule, ISystemCommand } from '@robota-sdk/agent-framework';
5
+ import type { ICommand, ICommandSource } from '@robota-sdk/agent-interface-command';
6
+
7
+ export function createContextCommandEntry(): ICommand {
8
+ return {
9
+ name: 'context',
10
+ displayName: 'Context References',
11
+ description: 'Context window info, reference inventory, and auto-compact controls',
12
+ // Model-invocable for the read-only views only: knowing how full the context is lets the model
13
+ // decide when to compact. Pinning, unpinning and clearing references and the auto-compact
14
+ // policy are the user's curation and preference, so they stay user-only.
15
+ modelDescription:
16
+ 'Inspect the context window. Use it before a large task or before deciding to compact, or to ' +
17
+ 'see which files the user pinned into context. Bare returns used/max tokens and percentage, ' +
18
+ 'the auto-compact threshold, the pinned-reference summary and the turn count; `list` returns ' +
19
+ 'the per-category token breakdown and every pinned reference.',
20
+ source: 'context',
21
+ modelInvocable: true,
22
+ userInvocable: true,
23
+ argumentHint: 'list | add <path> | remove <path> | clear | auto ...',
24
+ subcommands: [
25
+ {
26
+ name: 'list',
27
+ description: 'List loaded context references',
28
+ source: 'context',
29
+ modelInvocable: true,
30
+ },
31
+ {
32
+ name: 'add',
33
+ description: 'Add a file to active context references',
34
+ source: 'context',
35
+ modelInvocable: false,
36
+ },
37
+ {
38
+ name: 'remove',
39
+ description: 'Remove a context reference',
40
+ source: 'context',
41
+ modelInvocable: false,
42
+ },
43
+ {
44
+ name: 'clear',
45
+ description: 'Clear context references',
46
+ source: 'context',
47
+ modelInvocable: false,
48
+ },
49
+ {
50
+ name: 'auto',
51
+ description: 'Inspect or change auto-compact policy',
52
+ source: 'context',
53
+ modelInvocable: false,
54
+ },
55
+ ],
56
+ };
57
+ }
58
+
59
+ function createContextSystemCommand(): ISystemCommand {
60
+ const entry = createContextCommandEntry();
61
+ return createSystemCommandFromEntry(entry, {
62
+ requiresPermission: false,
63
+ lifecycle: 'inline',
64
+ execute: executeContextCommand,
65
+ });
66
+ }
67
+
68
+ export class ContextCommandSource implements ICommandSource {
69
+ readonly name = 'context';
70
+
71
+ getCommands(): ICommand[] {
72
+ return [createContextCommandEntry()];
73
+ }
74
+ }
75
+
76
+ export function createContextCommandModule(): ICommandModule {
77
+ return {
78
+ name: 'agent-command-context',
79
+ commandSources: [new ContextCommandSource()],
80
+ systemCommands: [createContextSystemCommand()],
81
+ };
82
+ }
@@ -0,0 +1,291 @@
1
+ import {
2
+ addCommandContextReference,
3
+ clearCommandContextReferences,
4
+ DEFAULT_AUTO_COMPACT_THRESHOLD,
5
+ listCommandContextReferences,
6
+ readAutoCompactThreshold,
7
+ readAutoCompactThresholdSource,
8
+ readCommandContextState,
9
+ removeCommandContextReference,
10
+ resetAutoCompactThresholdSetting,
11
+ setCommandAutoCompactThreshold,
12
+ writeAutoCompactThresholdSetting,
13
+ } from '@robota-sdk/agent-framework';
14
+
15
+ import {
16
+ buildToolDisplayList,
17
+ formatContextReferenceLine,
18
+ formatFullContextBreakdown,
19
+ } from './context-breakdown.js';
20
+
21
+ import type {
22
+ ICommandHostAdapterAccess,
23
+ ICommandHostContextReferences,
24
+ ICommandHostContextWindow,
25
+ ICommandHostSessionAccess,
26
+ TAutoCompactThreshold,
27
+ TAutoCompactThresholdSource,
28
+ } from '@robota-sdk/agent-framework';
29
+ import type { ICommandResult } from '@robota-sdk/agent-interface-command';
30
+ import type { IContextReferenceItem } from '@robota-sdk/agent-interface-session';
31
+
32
+ import { formatAutoCompactLine, formatThreshold } from './auto-compact-format.js';
33
+
34
+ export { formatAutoCompactLine } from './auto-compact-format.js';
35
+
36
+ const PERCENT = 100;
37
+ const USAGE = [
38
+ 'Usage: /context [list] | add <path> | remove <path> | clear | auto on | off | <percent> | reset',
39
+ 'Examples: /context list, /context add AGENTS.md, /context remove AGENTS.md, /context auto 85%',
40
+ ].join('\n');
41
+
42
+ function formatPersistenceSuffix(persisted: boolean): string {
43
+ return persisted ? 'settings' : 'current session only';
44
+ }
45
+
46
+ /** The roles the `/context` surface reads. Named once so five signatures do not restate it. */
47
+ type TContextCommandHost = ICommandHostAdapterAccess &
48
+ ICommandHostContextReferences &
49
+ ICommandHostContextWindow &
50
+ ICommandHostSessionAccess;
51
+
52
+ /** The subset the threshold operations need — no context-reference role. */
53
+ type TContextThresholdHost = ICommandHostAdapterAccess &
54
+ ICommandHostContextWindow &
55
+ ICommandHostSessionAccess;
56
+
57
+ export async function executeContextCommand(
58
+ context: TContextCommandHost,
59
+ args: string,
60
+ ): Promise<ICommandResult> {
61
+ const parts = args
62
+ .trim()
63
+ .split(/\s+/)
64
+ .filter((part) => part.length > 0);
65
+
66
+ if (parts.length > 0) {
67
+ return executeContextSubcommand(context, parts);
68
+ }
69
+
70
+ const state = readCommandContextState(context);
71
+ const autoCompactThreshold = readAutoCompactThreshold(context);
72
+ const autoCompactThresholdSource = readAutoCompactThresholdSource(context);
73
+ const history = context.getSession().getFullHistory();
74
+ const display = buildToolDisplayList(history);
75
+ const references = listCommandContextReferences(context);
76
+ return {
77
+ message: [
78
+ `Context: ${state.usedTokens.toLocaleString()} / ${state.maxTokens.toLocaleString()} tokens (${Math.round(state.usedPercentage)}%)`,
79
+ formatAutoCompactLine(autoCompactThreshold, autoCompactThresholdSource),
80
+ formatContextReferenceSummary(references),
81
+ `History: ${display.turnCount} turn${display.turnCount !== 1 ? 's' : ''}`,
82
+ ].join('\n'),
83
+ success: true,
84
+ data: {
85
+ usedTokens: state.usedTokens,
86
+ maxTokens: state.maxTokens,
87
+ percentage: state.usedPercentage,
88
+ autoCompactThreshold,
89
+ autoCompactThresholdSource,
90
+ references,
91
+ },
92
+ };
93
+ }
94
+
95
+ async function executeContextSubcommand(
96
+ context: TContextCommandHost,
97
+ parts: readonly string[],
98
+ ): Promise<ICommandResult> {
99
+ const [subcommand, ...rest] = parts;
100
+ if (subcommand === 'list') {
101
+ if (rest.length > 0) return { success: false, message: USAGE };
102
+ return formatFullContextBreakdown(context);
103
+ }
104
+ if (subcommand === 'add') {
105
+ return executeAddContextReference(context, rest);
106
+ }
107
+ if (subcommand === 'remove') {
108
+ return executeRemoveContextReference(context, rest);
109
+ }
110
+ if (subcommand === 'clear') {
111
+ if (rest.length > 0) return { success: false, message: USAGE };
112
+ const result = clearCommandContextReferences(context);
113
+ return {
114
+ success: true,
115
+ message: `Context references cleared: ${result.removed.length} removed.`,
116
+ data: { removed: result.removed },
117
+ };
118
+ }
119
+ if (subcommand !== 'auto') {
120
+ return { success: false, message: USAGE };
121
+ }
122
+ return executeAutoContextSubcommand(context, rest);
123
+ }
124
+
125
+ function executeAutoContextSubcommand(
126
+ context: TContextThresholdHost,
127
+ parts: readonly string[],
128
+ ): ICommandResult {
129
+ const [action, extra] = parts;
130
+ if (extra !== undefined) return { success: false, message: USAGE };
131
+ if (action === undefined) {
132
+ const threshold = readAutoCompactThreshold(context);
133
+ const source = readAutoCompactThresholdSource(context);
134
+ return {
135
+ success: true,
136
+ message: [formatAutoCompactLine(threshold, source), USAGE].join('\n'),
137
+ data: { autoCompactThreshold: threshold, autoCompactThresholdSource: source },
138
+ };
139
+ }
140
+
141
+ if (action === 'on') {
142
+ return applyAutoCompactThreshold(context, DEFAULT_AUTO_COMPACT_THRESHOLD, 'enabled');
143
+ }
144
+ if (action === 'off') {
145
+ return applyAutoCompactThreshold(context, false, 'disabled');
146
+ }
147
+ if (action === 'reset') {
148
+ const persisted = resetAutoCompactThresholdSetting(context);
149
+ setCommandAutoCompactThreshold(context, DEFAULT_AUTO_COMPACT_THRESHOLD, 'default');
150
+ return {
151
+ success: true,
152
+ message: `Auto compact reset to default: ${formatThreshold(DEFAULT_AUTO_COMPACT_THRESHOLD)} (${formatPersistenceSuffix(persisted)}).`,
153
+ data: {
154
+ autoCompactThreshold: DEFAULT_AUTO_COMPACT_THRESHOLD,
155
+ autoCompactThresholdSource: 'default',
156
+ persisted,
157
+ },
158
+ };
159
+ }
160
+
161
+ const parsed = parseThreshold(action);
162
+ if (!parsed.success) {
163
+ return { success: false, message: `${parsed.message}\n${USAGE}` };
164
+ }
165
+ return applyAutoCompactThreshold(context, parsed.threshold, 'threshold set');
166
+ }
167
+
168
+ async function executeAddContextReference(
169
+ context: ICommandHostContextReferences,
170
+ args: readonly string[],
171
+ ): Promise<ICommandResult> {
172
+ const path = args.join(' ').trim();
173
+ if (!path) return { success: false, message: USAGE };
174
+
175
+ const result = await addCommandContextReference(context, path);
176
+ if (!result.reference) {
177
+ return {
178
+ success: false,
179
+ message: result.diagnostics.join('\n') || `Context reference not found: ${path}`,
180
+ data: { diagnostics: result.diagnostics },
181
+ };
182
+ }
183
+
184
+ return {
185
+ success: true,
186
+ message: [
187
+ `Context reference added: ${formatContextReferenceLine(result.reference)}.`,
188
+ ...(result.evicted.length > 0
189
+ ? [`Evicted ${result.evicted.length} older context reference(s).`]
190
+ : []),
191
+ ].join('\n'),
192
+ data: { reference: result.reference, evicted: result.evicted },
193
+ };
194
+ }
195
+
196
+ function executeRemoveContextReference(
197
+ context: ICommandHostContextReferences,
198
+ args: readonly string[],
199
+ ): ICommandResult {
200
+ const path = args.join(' ').trim();
201
+ if (!path) return { success: false, message: USAGE };
202
+
203
+ const result = removeCommandContextReference(context, path);
204
+ if (!result.removed) {
205
+ return {
206
+ success: false,
207
+ message: `Context reference not found: ${path}`,
208
+ };
209
+ }
210
+
211
+ return {
212
+ success: true,
213
+ message: `Context reference removed: ${formatContextReferenceLine(result.removed)}.`,
214
+ data: { removed: result.removed },
215
+ };
216
+ }
217
+
218
+ function applyAutoCompactThreshold(
219
+ context: TContextThresholdHost,
220
+ threshold: TAutoCompactThreshold,
221
+ action: 'enabled' | 'disabled' | 'threshold set',
222
+ ): ICommandResult {
223
+ const persisted = writeAutoCompactThresholdSetting(context, threshold);
224
+ const source: TAutoCompactThresholdSource = persisted ? 'settings' : 'session';
225
+ setCommandAutoCompactThreshold(context, threshold, source);
226
+
227
+ return {
228
+ success: true,
229
+ message: formatApplyMessage(action, threshold, persisted),
230
+ data: {
231
+ autoCompactThreshold: threshold,
232
+ autoCompactThresholdSource: source,
233
+ persisted,
234
+ },
235
+ };
236
+ }
237
+
238
+ function formatApplyMessage(
239
+ action: 'enabled' | 'disabled' | 'threshold set',
240
+ threshold: TAutoCompactThreshold,
241
+ persisted: boolean,
242
+ ): string {
243
+ const suffix = formatPersistenceSuffix(persisted);
244
+ if (action === 'disabled') {
245
+ return `Auto compact disabled (${suffix}).`;
246
+ }
247
+ if (action === 'enabled') {
248
+ return `Auto compact enabled at ${formatThreshold(threshold)} (${suffix}).`;
249
+ }
250
+ return `Auto compact threshold set to ${formatThreshold(threshold)} (${suffix}).`;
251
+ }
252
+
253
+ type TParseThresholdResult =
254
+ { success: true; threshold: number } | { success: false; message: string };
255
+
256
+ function parseThreshold(raw: string): TParseThresholdResult {
257
+ if (raw.endsWith('%')) {
258
+ const percent = Number(raw.slice(0, -1));
259
+ if (!Number.isFinite(percent) || percent <= 0 || percent > PERCENT) {
260
+ return {
261
+ success: false,
262
+ message: 'Auto compact percentage must be greater than 0% and at most 100%.',
263
+ };
264
+ }
265
+ return { success: true, threshold: percent / PERCENT };
266
+ }
267
+
268
+ if (raw.includes('.')) {
269
+ const fraction = Number(raw);
270
+ if (!Number.isFinite(fraction) || fraction <= 0 || fraction > 1) {
271
+ return {
272
+ success: false,
273
+ message: 'Auto compact fraction must be greater than 0 and at most 1.',
274
+ };
275
+ }
276
+ return { success: true, threshold: fraction };
277
+ }
278
+
279
+ return {
280
+ success: false,
281
+ message: 'Use a percentage such as 85% or a fraction such as 0.85.',
282
+ };
283
+ }
284
+
285
+ function formatContextReferenceSummary(references: readonly IContextReferenceItem[]): string {
286
+ const active = references.filter((reference) => reference.status === 'active').length;
287
+ const observed = references.filter((reference) => reference.status === 'observed').length;
288
+ return `References: ${active} active, ${observed} observed`;
289
+ }
290
+
291
+ // 1 token ≈ 4 chars — sourced from the agent-core estimation SSOT.
@@ -0,0 +1,6 @@
1
+ export {
2
+ ContextCommandSource,
3
+ createContextCommandEntry,
4
+ createContextCommandModule,
5
+ } from './context-command-module.js';
6
+ export { executeContextCommand } from './context-command.js';