@gajae-code/coding-agent 0.10.0 → 0.10.2
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.
- package/CHANGELOG.md +33 -0
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +17 -5
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/modes/components/gajae-pet-widget.d.ts +1 -0
- package/dist/types/modes/components/pet-capability.d.ts +37 -0
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/types.d.ts +11 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/session/agent-session.d.ts +14 -1
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/package.json +7 -7
- package/src/cli.ts +1 -0
- package/src/config/model-profiles.ts +19 -19
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +294 -66
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/extensibility/slash-commands.ts +3 -3
- package/src/gjc-runtime/tmux-owner-isolation.ts +4 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +3 -5
- package/src/modes/acp/acp-agent.ts +2 -2
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/gajae-pet-widget.ts +169 -20
- package/src/modes/components/pet-capability.ts +115 -0
- package/src/modes/components/pet-selector.ts +4 -6
- package/src/modes/components/settings-selector.ts +28 -2
- package/src/modes/components/status-line/segments.ts +13 -6
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/controllers/extension-ui-controller.ts +10 -9
- package/src/modes/controllers/input-controller.ts +43 -13
- package/src/modes/controllers/selector-controller.ts +7 -18
- package/src/modes/interactive-mode.ts +65 -4
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +11 -1
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/session/agent-session.ts +337 -90
- package/src/session/context-estimation.ts +91 -0
- package/src/session/session-manager.ts +9 -2
- package/src/slash-commands/builtin-registry.ts +31 -19
- package/src/slash-commands/helpers/context-report.ts +26 -4
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-neutral fixed-context (non-message) token estimation.
|
|
3
|
+
*
|
|
4
|
+
* Lives in the session layer so load-bearing session/compaction behavior
|
|
5
|
+
* (`AgentSession` context estimates) never depends on a presentation module.
|
|
6
|
+
* UI surfaces (`modes/utils/context-usage.ts`, status line) import from here
|
|
7
|
+
* as well, so every surface reports the same numbers.
|
|
8
|
+
*/
|
|
9
|
+
import { estimateTextTokensHeuristic } from "@gajae-code/agent-core/compaction";
|
|
10
|
+
import type { Skill } from "../extensibility/skills";
|
|
11
|
+
import type { Tool } from "../tools";
|
|
12
|
+
import type { AgentSession } from "./agent-session";
|
|
13
|
+
|
|
14
|
+
export function estimateSkillsTokens(skills: readonly Skill[]): number {
|
|
15
|
+
const fragments: string[] = [];
|
|
16
|
+
for (const skill of skills) {
|
|
17
|
+
// "- name: description\n" wire framing tokenizes ~identically to the
|
|
18
|
+
// concatenated form, so encode each piece separately and sum.
|
|
19
|
+
fragments.push(skill.name, skill.description);
|
|
20
|
+
}
|
|
21
|
+
return estimateTextTokensHeuristic(fragments);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function estimateToolSchemaTokens(
|
|
25
|
+
tools: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>,
|
|
26
|
+
): number {
|
|
27
|
+
const fragments: string[] = [];
|
|
28
|
+
for (const tool of tools) {
|
|
29
|
+
fragments.push(tool.name, tool.description);
|
|
30
|
+
try {
|
|
31
|
+
fragments.push(JSON.stringify(tool.parameters ?? {}));
|
|
32
|
+
} catch {
|
|
33
|
+
// Schema may contain functions or cycles; ignore.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return estimateTextTokensHeuristic(fragments);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compute just the NON-MESSAGE token total: system prompt (with its skills
|
|
41
|
+
* section subtracted, since skills are tokenized separately) + system context
|
|
42
|
+
* (the rest of the system-prompt array) + tools + skills.
|
|
43
|
+
*
|
|
44
|
+
* Exposed so callers like `StatusLineComponent` can cache the non-message
|
|
45
|
+
* total separately from the message total. Non-message inputs (skills,
|
|
46
|
+
* tools, system prompt) change rarely; the message list grows on every
|
|
47
|
+
* streaming turn. Splitting the two lets the caller refresh each on its own
|
|
48
|
+
* cadence — non-message recomputed only when the inputs identity changes,
|
|
49
|
+
* messages walked incrementally as new entries append.
|
|
50
|
+
*/
|
|
51
|
+
export function computeNonMessageTokens(session: AgentSession): number {
|
|
52
|
+
const parts = computeNonMessageBreakdown(session);
|
|
53
|
+
return (
|
|
54
|
+
parts.systemPromptTokens + parts.systemContextTokens + parts.rulesTokens + parts.toolsTokens + parts.skillsTokens
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Shared helper for the four non-message token totals. Single source of truth
|
|
60
|
+
* for `computeNonMessageTokens` (session estimates + status-line incremental
|
|
61
|
+
* cache) and `computeContextBreakdown` (/context panel). The split avoids
|
|
62
|
+
* drift between the surfaces — they MUST report the same numbers.
|
|
63
|
+
*/
|
|
64
|
+
export function computeNonMessageBreakdown(session: AgentSession): {
|
|
65
|
+
rulesTokens: number;
|
|
66
|
+
skillsTokens: number;
|
|
67
|
+
toolsTokens: number;
|
|
68
|
+
systemContextTokens: number;
|
|
69
|
+
systemPromptTokens: number;
|
|
70
|
+
} {
|
|
71
|
+
const skillsTokens = estimateSkillsTokens(session.skills ?? []);
|
|
72
|
+
const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? []);
|
|
73
|
+
const systemPromptParts = session.systemPrompt ?? [];
|
|
74
|
+
const rulesTokens = estimateRulesTokens(systemPromptParts);
|
|
75
|
+
const systemContextTokens = estimateTextTokensHeuristic(systemPromptParts.slice(1));
|
|
76
|
+
const systemPromptTokens = Math.max(
|
|
77
|
+
0,
|
|
78
|
+
estimateTextTokensHeuristic(systemPromptParts[0] ?? "") - skillsTokens - rulesTokens,
|
|
79
|
+
);
|
|
80
|
+
return { rulesTokens, skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function estimateRulesTokens(systemPromptParts: readonly string[]): number {
|
|
84
|
+
const fragments: string[] = [];
|
|
85
|
+
for (const part of systemPromptParts) {
|
|
86
|
+
for (const match of part.matchAll(/<rules>[\s\S]*?<\/rules>/g)) {
|
|
87
|
+
fragments.push(match[0]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return fragments.length === 0 ? 0 : estimateTextTokensHeuristic(fragments);
|
|
91
|
+
}
|
|
@@ -3335,6 +3335,11 @@ export class SessionManager {
|
|
|
3335
3335
|
|
|
3336
3336
|
restoreState(snapshot: SessionManagerStateSnapshot): void {
|
|
3337
3337
|
const restoredFileEntries = [...snapshot.materializedFileEntries];
|
|
3338
|
+
const retainsPersistWriter =
|
|
3339
|
+
this.#persistWriter?.isOpen() === true &&
|
|
3340
|
+
this.#sessionId === snapshot.sessionId &&
|
|
3341
|
+
this.#sessionFile === snapshot.sessionFile &&
|
|
3342
|
+
this.#persistWriterPath === snapshot.sessionFile;
|
|
3338
3343
|
this.#sessionId = snapshot.sessionId;
|
|
3339
3344
|
this.#sessionName = snapshot.sessionName;
|
|
3340
3345
|
this.#titleSource = snapshot.titleSource;
|
|
@@ -3342,8 +3347,10 @@ export class SessionManager {
|
|
|
3342
3347
|
this.#flushed = snapshot.flushed;
|
|
3343
3348
|
this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
|
|
3344
3349
|
this.#fileEntries = restoredFileEntries;
|
|
3345
|
-
|
|
3346
|
-
|
|
3350
|
+
if (!retainsPersistWriter) {
|
|
3351
|
+
this.#persistWriter = undefined;
|
|
3352
|
+
this.#persistWriterPath = undefined;
|
|
3353
|
+
}
|
|
3347
3354
|
this.#persistChain = Promise.resolve();
|
|
3348
3355
|
this.#persistError = undefined;
|
|
3349
3356
|
this.#persistErrorReported = false;
|
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
4
4
|
import { type Model, modelsAreEqual } from "@gajae-code/ai";
|
|
5
5
|
import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
|
|
6
|
-
import {
|
|
6
|
+
import { PET_SKINS, type PetMode, Spacer, Text } from "@gajae-code/tui";
|
|
7
7
|
import { setProjectDir } from "@gajae-code/utils";
|
|
8
8
|
import { jobElapsedMs } from "../async";
|
|
9
9
|
import { materializeActiveModelProfileAssignments } from "../config/model-profile-activation";
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../discovery/helpers.js";
|
|
23
23
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
24
24
|
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
25
|
-
import { GajaePetWidget } from "../modes/components/gajae-pet-widget";
|
|
26
25
|
import { theme } from "../modes/theme/theme";
|
|
27
26
|
import type { InteractiveModeContext } from "../modes/types";
|
|
28
27
|
import {
|
|
@@ -65,6 +64,22 @@ export type { BuiltinSlashCommand, SubcommandDef } from "./types";
|
|
|
65
64
|
/** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
|
|
66
65
|
export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
|
|
67
66
|
|
|
67
|
+
const PET_COMMAND_OPTIONS: ReadonlyArray<{ name: string; mode: PetMode; description: string }> = [
|
|
68
|
+
{ name: "off", mode: "off", description: "Hide the pet" },
|
|
69
|
+
{ name: "RedGajae", mode: "red", description: PET_SKINS.red.description },
|
|
70
|
+
{ name: "BlueGajae", mode: "blue", description: PET_SKINS.blue.description },
|
|
71
|
+
];
|
|
72
|
+
const PET_COMMAND_HINT = `[${PET_COMMAND_OPTIONS.map(option => option.name).join("|")}]`;
|
|
73
|
+
/**
|
|
74
|
+
* Deprecated inputs kept accepted for compatibility (`/pet on|red|blue`).
|
|
75
|
+
* Display, completion, and inline hints stay canonical (`PET_COMMAND_OPTIONS`).
|
|
76
|
+
*/
|
|
77
|
+
const PET_COMMAND_DEPRECATED_INPUTS: Readonly<Record<string, PetMode>> = {
|
|
78
|
+
on: "red",
|
|
79
|
+
red: "red",
|
|
80
|
+
blue: "blue",
|
|
81
|
+
};
|
|
82
|
+
|
|
68
83
|
type GjcModelBatchAssignmentTargetId = "all-role-agents" | "all-targets";
|
|
69
84
|
type ParsedModelCommandArgs =
|
|
70
85
|
| { kind: "summary" }
|
|
@@ -562,32 +577,29 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
562
577
|
{
|
|
563
578
|
name: "pet",
|
|
564
579
|
description: "Gajae pet living beside the composer",
|
|
565
|
-
subcommands:
|
|
566
|
-
|
|
567
|
-
...PET_SKIN_IDS.map(id => ({ name: id, description: PET_SKINS[id].description })),
|
|
568
|
-
],
|
|
569
|
-
inlineHint: `[${PET_MODE_IDS.join("|")}]`,
|
|
580
|
+
subcommands: PET_COMMAND_OPTIONS.map(option => ({ name: option.name, description: option.description })),
|
|
581
|
+
inlineHint: PET_COMMAND_HINT,
|
|
570
582
|
allowArgs: true,
|
|
571
583
|
handleTui: (command, runtime) => {
|
|
572
584
|
const ctx = runtime.ctx;
|
|
573
585
|
const raw = command.args?.trim().toLowerCase() ?? "";
|
|
574
|
-
const arg =
|
|
575
|
-
|
|
586
|
+
const arg =
|
|
587
|
+
PET_COMMAND_OPTIONS.find(option => option.name.toLowerCase() === raw)?.mode ??
|
|
588
|
+
PET_COMMAND_DEPRECATED_INPUTS[raw];
|
|
589
|
+
if (!raw) {
|
|
576
590
|
ctx.showPetSelector();
|
|
577
591
|
ctx.editor.setText("");
|
|
578
592
|
return;
|
|
579
593
|
}
|
|
580
|
-
if (arg
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const name = arg === "off" ? "Gajae pet hidden" : `${PET_SKINS[arg].label} is here`;
|
|
588
|
-
ctx.showStatus(name);
|
|
594
|
+
if (arg) {
|
|
595
|
+
// The shared commit policy rechecks capability, persists only on
|
|
596
|
+
// acceptance, and surfaces the actionable warning on rejection.
|
|
597
|
+
if (ctx.setPetMode(arg)) {
|
|
598
|
+
const name = arg === "off" ? "Gajae pet hidden" : `${PET_SKINS[arg].label} is here`;
|
|
599
|
+
ctx.showStatus(name);
|
|
600
|
+
}
|
|
589
601
|
} else {
|
|
590
|
-
ctx.showStatus(`Usage: /pet
|
|
602
|
+
ctx.showStatus(`Usage: /pet ${PET_COMMAND_HINT}`, { dim: true });
|
|
591
603
|
}
|
|
592
604
|
ctx.editor.setText("");
|
|
593
605
|
},
|
|
@@ -83,6 +83,12 @@ function formatReserveText(runtime: SlashCommandRuntime, contextWindow: number,
|
|
|
83
83
|
return "unknown";
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function contextUsageSourceLabel(source: "provider_anchor" | "heuristic" | "unknown"): string {
|
|
87
|
+
if (source === "provider_anchor") return "provider-reported";
|
|
88
|
+
if (source === "unknown") return "estimated; exact count unknown until next response";
|
|
89
|
+
return "estimated";
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
/**
|
|
87
93
|
* Build the `/context` ACP-mode text. Tries the rich breakdown first
|
|
88
94
|
* (categories + auto-compact buffer + free slack) and falls back to the
|
|
@@ -96,11 +102,21 @@ export function buildContextReportText(runtime: SlashCommandRuntime): string {
|
|
|
96
102
|
return "Context usage is unavailable: no model is selected for this session.";
|
|
97
103
|
}
|
|
98
104
|
const promptUsage = findLastAssistantUsage(history.activeMessages);
|
|
99
|
-
const
|
|
105
|
+
const activeContext =
|
|
106
|
+
breakdown.usedTokens === null
|
|
107
|
+
? `Active context: unknown / ${breakdown.contextWindow.toLocaleString()} tokens (exact count unknown until next response) (${contextUsageSourceLabel(breakdown.source)})`
|
|
108
|
+
: `Active context: ${breakdown.usedTokens.toLocaleString()} / ${breakdown.contextWindow.toLocaleString()} tokens (${((breakdown.usedTokens / breakdown.contextWindow) * 100).toFixed(1)}% used) (${contextUsageSourceLabel(breakdown.source)})`;
|
|
100
109
|
const lines = [
|
|
101
110
|
"Context usage",
|
|
102
111
|
`Model: ${breakdown.model?.provider ?? "unknown"}/${breakdown.model?.id ?? "unknown"}`,
|
|
103
|
-
|
|
112
|
+
activeContext,
|
|
113
|
+
...(breakdown.source !== "unknown" &&
|
|
114
|
+
breakdown.usedTokens !== null &&
|
|
115
|
+
breakdown.estimatedCategoryTotal !== breakdown.usedTokens
|
|
116
|
+
? [
|
|
117
|
+
`Estimated category total: ${breakdown.estimatedCategoryTotal.toLocaleString()} tokens (composition below is estimated)`,
|
|
118
|
+
]
|
|
119
|
+
: []),
|
|
104
120
|
`Reserve: ${formatReserveText(runtime, breakdown.contextWindow, breakdown.autoCompactBufferTokens)}`,
|
|
105
121
|
"",
|
|
106
122
|
"Active context breakdown (estimated)",
|
|
@@ -111,7 +127,13 @@ export function buildContextReportText(runtime: SlashCommandRuntime): string {
|
|
|
111
127
|
if (breakdown.autoCompactBufferTokens > 0) {
|
|
112
128
|
lines.push(formatTokenLine("Reserve", breakdown.autoCompactBufferTokens, breakdown.contextWindow));
|
|
113
129
|
}
|
|
114
|
-
lines.push(
|
|
130
|
+
lines.push(
|
|
131
|
+
formatTokenLine(
|
|
132
|
+
breakdown.usedTokens === null ? "Free (estimated)" : "Free",
|
|
133
|
+
breakdown.freeTokens,
|
|
134
|
+
breakdown.contextWindow,
|
|
135
|
+
),
|
|
136
|
+
);
|
|
115
137
|
lines.push(
|
|
116
138
|
"",
|
|
117
139
|
"History",
|
|
@@ -141,7 +163,7 @@ export function buildContextReportText(runtime: SlashCommandRuntime): string {
|
|
|
141
163
|
if (!fallback) return "Context usage is unavailable.";
|
|
142
164
|
return [
|
|
143
165
|
"Context usage",
|
|
144
|
-
`Active context: ${fallback.tokens === null
|
|
166
|
+
`Active context: ${fallback.tokens === null ? "unknown" : fallback.tokens.toLocaleString()} (${contextUsageSourceLabel(fallback.source)})`,
|
|
145
167
|
`Context window: ${fallback.contextWindow.toLocaleString()}`,
|
|
146
168
|
"Breakdown: unknown",
|
|
147
169
|
].join("\n");
|