@gajae-code/coding-agent 0.2.2 → 0.2.4
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 +45 -8600
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/cli/update-cli.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +41 -0
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +56 -0
- package/dist/types/defaults/gjc-defaults.d.ts +19 -6
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +18 -0
- package/dist/types/hooks/skill-state.d.ts +5 -0
- package/dist/types/memories/index.d.ts +1 -1
- package/dist/types/memory-backend/local-backend.d.ts +3 -3
- package/dist/types/modes/components/hook-selector.d.ts +7 -0
- package/dist/types/modes/components/settings-selector.d.ts +3 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/theme/defaults/index.d.ts +126 -0
- package/dist/types/modes/theme/theme.d.ts +5 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/context-usage.d.ts +6 -2
- package/dist/types/sdk.d.ts +6 -2
- package/dist/types/session/agent-session.d.ts +45 -1
- package/dist/types/session/session-manager.d.ts +3 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
- package/dist/types/setup/provider-onboarding.d.ts +29 -5
- package/dist/types/skill-state/active-state.d.ts +26 -1
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/initial-phase.d.ts +12 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/types.d.ts +11 -0
- package/dist/types/tools/index.d.ts +20 -1
- package/dist/types/tools/skill.d.ts +47 -0
- package/dist/types/utils/changelog.d.ts +18 -2
- package/package.json +7 -7
- package/src/cli/setup-cli.ts +26 -12
- package/src/cli/update-cli.ts +67 -16
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +25 -2
- package/src/commands/setup.ts +2 -0
- package/src/commands/state.ts +1 -0
- package/src/config/settings-schema.ts +63 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +58 -5
- package/src/defaults/gjc/skills/deep-interview/auto-answer-uncertain.md +37 -0
- package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +42 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -0
- package/src/defaults/gjc/skills/team/SKILL.md +10 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +19 -6
- package/src/defaults/gjc-defaults.ts +68 -16
- package/src/discovery/helpers.ts +24 -1
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +312 -1
- package/src/gjc-runtime/state-runtime.ts +175 -5
- package/src/goals/tools/goal-tool.ts +5 -1
- package/src/hooks/skill-state.ts +8 -6
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/internal-urls/memory-protocol.ts +3 -2
- package/src/main.ts +2 -3
- package/src/memories/index.ts +6 -4
- package/src/memory-backend/local-backend.ts +14 -6
- package/src/modes/components/hook-selector.ts +156 -1
- package/src/modes/components/settings-selector.ts +16 -12
- package/src/modes/controllers/command-controller.ts +3 -4
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +69 -9
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/theme/defaults/blue-crab.json +126 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/theme.ts +40 -1
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/context-usage.ts +66 -17
- package/src/prompts/agents/architect.md +3 -0
- package/src/prompts/agents/executor.md +2 -0
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/memories/unavailable.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +6 -0
- package/src/prompts/tools/skill.md +28 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/sdk.ts +54 -10
- package/src/session/agent-session.ts +204 -21
- package/src/session/session-manager.ts +9 -1
- package/src/setup/model-onboarding-guidance.ts +6 -3
- package/src/setup/provider-onboarding.ts +177 -16
- package/src/skill-state/active-state.ts +150 -25
- package/src/skill-state/deep-interview-mutation-guard.ts +11 -24
- package/src/skill-state/initial-phase.ts +17 -0
- package/src/slash-commands/builtin-registry.ts +62 -14
- package/src/slash-commands/helpers/context-report.ts +123 -13
- package/src/task/agents.ts +1 -0
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +91 -4
- package/src/task/types.ts +6 -0
- package/src/tools/ask.ts +2 -0
- package/src/tools/index.ts +23 -1
- package/src/tools/skill.ts +153 -0
- package/src/utils/changelog.ts +67 -44
|
@@ -16,10 +16,12 @@ import type { InteractiveModeContext } from "../modes/types";
|
|
|
16
16
|
import { formatModelOnboardingGuidance } from "../setup/model-onboarding-guidance";
|
|
17
17
|
import {
|
|
18
18
|
addApiCompatibleProvider,
|
|
19
|
+
formatProviderPresetList,
|
|
19
20
|
formatProviderSetupResult,
|
|
20
21
|
parseProviderCompatibility,
|
|
21
22
|
} from "../setup/provider-onboarding";
|
|
22
23
|
import { parseThinkingLevel } from "../thinking";
|
|
24
|
+
import { buildContextReportText } from "./helpers/context-report";
|
|
23
25
|
import { formatDuration } from "./helpers/format";
|
|
24
26
|
import { commandConsumed, errorMessage, parseSlashCommand, usage } from "./helpers/parse";
|
|
25
27
|
import { handleSshAcp } from "./helpers/ssh";
|
|
@@ -39,6 +41,7 @@ export type { BuiltinSlashCommand, SubcommandDef } from "./types";
|
|
|
39
41
|
export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
|
|
40
42
|
|
|
41
43
|
function parseProviderSetupSlashArgs(args: string): {
|
|
44
|
+
preset?: string;
|
|
42
45
|
compat?: string;
|
|
43
46
|
provider?: string;
|
|
44
47
|
baseUrl?: string;
|
|
@@ -49,6 +52,7 @@ function parseProviderSetupSlashArgs(args: string): {
|
|
|
49
52
|
} {
|
|
50
53
|
const tokens = args.split(/\s+/).filter(Boolean);
|
|
51
54
|
const result: {
|
|
55
|
+
preset?: string;
|
|
52
56
|
compat?: string;
|
|
53
57
|
provider?: string;
|
|
54
58
|
baseUrl?: string;
|
|
@@ -67,9 +71,16 @@ function parseProviderSetupSlashArgs(args: string): {
|
|
|
67
71
|
result.force = true;
|
|
68
72
|
continue;
|
|
69
73
|
}
|
|
74
|
+
if (!token.startsWith("-") && !result.preset) {
|
|
75
|
+
result.preset = token;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
70
78
|
const value = tokens[i + 1];
|
|
71
79
|
if (!value) continue;
|
|
72
|
-
if (token === "--
|
|
80
|
+
if (token === "--preset") {
|
|
81
|
+
result.preset = value;
|
|
82
|
+
i += 1;
|
|
83
|
+
} else if (token === "--compat") {
|
|
73
84
|
result.compat = value;
|
|
74
85
|
i += 1;
|
|
75
86
|
} else if (token === "--provider") {
|
|
@@ -95,7 +106,10 @@ function parseProviderSetupSlashArgs(args: string): {
|
|
|
95
106
|
function providerSetupUsage(): string {
|
|
96
107
|
return [
|
|
97
108
|
"Provider onboarding",
|
|
109
|
+
"Presets: /provider add --preset <minimax|minimax-cn|glm> [--force]",
|
|
110
|
+
"Aliases: /provider add minimax, /provider add minimax-cn, /provider add glm, /provider add zai (writes glm-proxy)",
|
|
98
111
|
"API providers: /provider add --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <model> [--force]",
|
|
112
|
+
`Available presets:\n${formatProviderPresetList()}`,
|
|
99
113
|
"OAuth/subscription providers: /provider login [provider-id] or /login [provider-id]",
|
|
100
114
|
"Headless OAuth callbacks can be pasted with /login <redirect URL or code>.",
|
|
101
115
|
].join("\n");
|
|
@@ -202,6 +216,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
202
216
|
runtime.ctx.editor.setText("");
|
|
203
217
|
},
|
|
204
218
|
},
|
|
219
|
+
{
|
|
220
|
+
name: "theme",
|
|
221
|
+
description: "Open theme selector",
|
|
222
|
+
handleTui: (_command, runtime) => {
|
|
223
|
+
runtime.ctx.showThemeSelector();
|
|
224
|
+
runtime.ctx.editor.setText("");
|
|
225
|
+
},
|
|
226
|
+
},
|
|
205
227
|
{
|
|
206
228
|
name: "goal",
|
|
207
229
|
description: "Toggle goal mode (persistent autonomous objective for this session)",
|
|
@@ -503,6 +525,19 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
503
525
|
runtime.ctx.editor.setText("");
|
|
504
526
|
},
|
|
505
527
|
},
|
|
528
|
+
{
|
|
529
|
+
name: "context",
|
|
530
|
+
description: "Show active context token usage breakdown",
|
|
531
|
+
acpDescription: "Show active context token usage breakdown",
|
|
532
|
+
handle: async (_command, runtime) => {
|
|
533
|
+
await runtime.output(buildContextReportText(runtime));
|
|
534
|
+
return commandConsumed();
|
|
535
|
+
},
|
|
536
|
+
handleTui: (_command, runtime) => {
|
|
537
|
+
runtime.ctx.handleContextCommand();
|
|
538
|
+
runtime.ctx.editor.setText("");
|
|
539
|
+
},
|
|
540
|
+
},
|
|
506
541
|
{
|
|
507
542
|
name: "usage",
|
|
508
543
|
description: "Show provider usage and limits",
|
|
@@ -580,20 +615,30 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
580
615
|
if (!args.startsWith("add ")) return usage(providerSetupUsage(), runtime);
|
|
581
616
|
const parsed = parseProviderSetupSlashArgs(args.slice(4));
|
|
582
617
|
const missing: string[] = [];
|
|
583
|
-
if (!parsed.
|
|
584
|
-
|
|
585
|
-
|
|
618
|
+
if (!parsed.preset) {
|
|
619
|
+
if (!parsed.compat) missing.push("--compat");
|
|
620
|
+
if (!parsed.provider) missing.push("--provider");
|
|
621
|
+
if (!parsed.baseUrl) missing.push("--base-url");
|
|
622
|
+
}
|
|
586
623
|
if (parsed.rejectedRawApiKey) {
|
|
587
624
|
return usage("Provider setup rejects raw --api-key values; use --api-key-env <ENV> instead.", runtime);
|
|
588
625
|
}
|
|
589
|
-
if (!parsed.
|
|
590
|
-
|
|
591
|
-
|
|
626
|
+
if (!parsed.preset) {
|
|
627
|
+
if (!parsed.apiKeyEnv) missing.push("--api-key-env");
|
|
628
|
+
if (parsed.models.length === 0) missing.push("--model");
|
|
629
|
+
}
|
|
630
|
+
if (missing.length > 0) {
|
|
631
|
+
return usage(
|
|
632
|
+
`Missing required option(s): ${missing.join(", ")}. Or use /provider add --preset <preset>.`,
|
|
633
|
+
runtime,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
592
636
|
try {
|
|
593
637
|
const result = await addApiCompatibleProvider({
|
|
594
|
-
compatibility: parseProviderCompatibility(parsed.compat
|
|
595
|
-
|
|
596
|
-
|
|
638
|
+
compatibility: parsed.compat ? parseProviderCompatibility(parsed.compat) : undefined,
|
|
639
|
+
preset: parsed.preset,
|
|
640
|
+
providerId: parsed.provider,
|
|
641
|
+
baseUrl: parsed.baseUrl,
|
|
597
642
|
apiKeyEnv: parsed.apiKeyEnv,
|
|
598
643
|
models: parsed.models,
|
|
599
644
|
force: parsed.force,
|
|
@@ -631,9 +676,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
631
676
|
throw new Error("Provider setup rejects raw --api-key values; use --api-key-env <ENV> instead.");
|
|
632
677
|
}
|
|
633
678
|
const result = await addApiCompatibleProvider({
|
|
634
|
-
compatibility: parseProviderCompatibility(parsed.compat
|
|
635
|
-
|
|
636
|
-
|
|
679
|
+
compatibility: parsed.compat ? parseProviderCompatibility(parsed.compat) : undefined,
|
|
680
|
+
preset: parsed.preset,
|
|
681
|
+
providerId: parsed.provider,
|
|
682
|
+
baseUrl: parsed.baseUrl,
|
|
637
683
|
apiKeyEnv: parsed.apiKeyEnv,
|
|
638
684
|
models: parsed.models,
|
|
639
685
|
force: parsed.force,
|
|
@@ -885,7 +931,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
885
931
|
runtime.settings,
|
|
886
932
|
runtime.session,
|
|
887
933
|
);
|
|
888
|
-
await runtime.output(
|
|
934
|
+
await runtime.output(
|
|
935
|
+
payload || "Memory payload is empty; durable memory is unavailable or unconfirmed.",
|
|
936
|
+
);
|
|
889
937
|
return commandConsumed();
|
|
890
938
|
}
|
|
891
939
|
case "clear":
|
|
@@ -1,7 +1,88 @@
|
|
|
1
|
+
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
2
|
+
import { type CompactionSettings, calculatePromptTokens } from "@gajae-code/agent-core/compaction";
|
|
3
|
+
import type { AssistantMessage, Usage } from "@gajae-code/ai";
|
|
1
4
|
import { computeContextBreakdown } from "../../modes/utils/context-usage";
|
|
5
|
+
import type { CompactionEntry, SessionEntry } from "../../session/session-manager";
|
|
2
6
|
import type { SlashCommandRuntime } from "../types";
|
|
3
7
|
import { renderAsciiBar } from "./format";
|
|
4
8
|
|
|
9
|
+
interface ActiveHistorySummary {
|
|
10
|
+
activeMessages: readonly AgentMessage[];
|
|
11
|
+
rawBranchMessages: number;
|
|
12
|
+
rawBranchEntries: number;
|
|
13
|
+
compaction: CompactionEntry | undefined;
|
|
14
|
+
compactedRawMessages: number | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isMessageEntry(entry: SessionEntry): boolean {
|
|
18
|
+
return entry.type === "message" || entry.type === "custom_message" || entry.type === "branch_summary";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function summarizeActiveHistory(runtime: SlashCommandRuntime): ActiveHistorySummary {
|
|
22
|
+
const activeContext = runtime.sessionManager.buildSessionContext();
|
|
23
|
+
const branch = runtime.sessionManager.getBranch();
|
|
24
|
+
let compaction: CompactionEntry | undefined;
|
|
25
|
+
let compactionIndex = -1;
|
|
26
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
27
|
+
const entry = branch[i];
|
|
28
|
+
if (entry.type === "compaction") {
|
|
29
|
+
compaction = entry;
|
|
30
|
+
compactionIndex = i;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const rawBranchMessages = branch.filter(isMessageEntry).length;
|
|
36
|
+
let compactedRawMessages: number | undefined;
|
|
37
|
+
if (compaction) {
|
|
38
|
+
const firstKeptIndex = branch.findIndex(entry => entry.id === compaction?.firstKeptEntryId);
|
|
39
|
+
if (firstKeptIndex >= 0 && compactionIndex >= 0) {
|
|
40
|
+
compactedRawMessages = branch.slice(0, firstKeptIndex).filter(isMessageEntry).length;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
activeMessages: activeContext.messages,
|
|
46
|
+
rawBranchMessages,
|
|
47
|
+
rawBranchEntries: branch.length,
|
|
48
|
+
compaction,
|
|
49
|
+
compactedRawMessages,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findLastAssistantUsage(messages: readonly AgentMessage[]):
|
|
54
|
+
| {
|
|
55
|
+
message: AssistantMessage;
|
|
56
|
+
usage: Usage;
|
|
57
|
+
}
|
|
58
|
+
| undefined {
|
|
59
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
60
|
+
const message = messages[i];
|
|
61
|
+
if (message.role === "assistant") {
|
|
62
|
+
const assistant = message as AssistantMessage;
|
|
63
|
+
return { message: assistant, usage: assistant.usage };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatUnknownNumber(value: number | undefined): string {
|
|
70
|
+
return value === undefined ? "unknown" : value.toLocaleString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function formatTokenLine(label: string, tokens: number, contextWindow: number): string {
|
|
74
|
+
const fraction = contextWindow > 0 ? tokens / contextWindow : 0;
|
|
75
|
+
return ` ${label.padEnd(16)} ${renderAsciiBar(fraction)} ${tokens.toLocaleString()} tokens`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatReserveText(runtime: SlashCommandRuntime, contextWindow: number, reserveTokens: number): string {
|
|
79
|
+
if (contextWindow <= 0) return "unknown";
|
|
80
|
+
if (reserveTokens > 0) return `${reserveTokens.toLocaleString()} tokens`;
|
|
81
|
+
const compaction = runtime.settings.getGroup("compaction") as CompactionSettings;
|
|
82
|
+
if (!compaction.enabled || compaction.strategy === "off") return "none configured";
|
|
83
|
+
return "unknown";
|
|
84
|
+
}
|
|
85
|
+
|
|
5
86
|
/**
|
|
6
87
|
* Build the `/context` ACP-mode text. Tries the rich breakdown first
|
|
7
88
|
* (categories + auto-compact buffer + free slack) and falls back to the
|
|
@@ -9,31 +90,60 @@ import { renderAsciiBar } from "./format";
|
|
|
9
90
|
*/
|
|
10
91
|
export function buildContextReportText(runtime: SlashCommandRuntime): string {
|
|
11
92
|
try {
|
|
12
|
-
const
|
|
93
|
+
const history = summarizeActiveHistory(runtime);
|
|
94
|
+
const breakdown = computeContextBreakdown(runtime.session, { messages: history.activeMessages });
|
|
13
95
|
if (breakdown.contextWindow <= 0) {
|
|
14
96
|
return "Context usage is unavailable: no model is selected for this session.";
|
|
15
97
|
}
|
|
16
|
-
const
|
|
17
|
-
const
|
|
98
|
+
const promptUsage = findLastAssistantUsage(history.activeMessages);
|
|
99
|
+
const usedPct = (breakdown.usedTokens / breakdown.contextWindow) * 100;
|
|
100
|
+
const lines = [
|
|
101
|
+
"Context usage",
|
|
102
|
+
`Model: ${breakdown.model?.provider ?? "unknown"}/${breakdown.model?.id ?? "unknown"}`,
|
|
103
|
+
`Active context: ${breakdown.usedTokens.toLocaleString()} / ${breakdown.contextWindow.toLocaleString()} tokens (${usedPct.toFixed(1)}% used)`,
|
|
104
|
+
`Reserve: ${formatReserveText(runtime, breakdown.contextWindow, breakdown.autoCompactBufferTokens)}`,
|
|
105
|
+
"",
|
|
106
|
+
"Active context breakdown (estimated)",
|
|
107
|
+
];
|
|
18
108
|
for (const category of breakdown.categories) {
|
|
19
|
-
|
|
20
|
-
const fraction = category.tokens / breakdown.contextWindow;
|
|
21
|
-
lines.push(` ${category.label.padEnd(16)} ${renderAsciiBar(fraction)} ${category.tokens} tokens`);
|
|
109
|
+
lines.push(formatTokenLine(category.label, category.tokens, breakdown.contextWindow));
|
|
22
110
|
}
|
|
23
111
|
if (breakdown.autoCompactBufferTokens > 0) {
|
|
24
|
-
|
|
112
|
+
lines.push(formatTokenLine("Reserve", breakdown.autoCompactBufferTokens, breakdown.contextWindow));
|
|
113
|
+
}
|
|
114
|
+
lines.push(formatTokenLine("Free", breakdown.freeTokens, breakdown.contextWindow));
|
|
115
|
+
lines.push(
|
|
116
|
+
"",
|
|
117
|
+
"History",
|
|
118
|
+
`Active messages sent next turn: ${history.activeMessages.length.toLocaleString()}`,
|
|
119
|
+
`Raw branch history: ${history.rawBranchMessages.toLocaleString()} message entries / ${history.rawBranchEntries.toLocaleString()} total entries`,
|
|
120
|
+
history.compaction
|
|
121
|
+
? `Compacted history: summary active; compacted raw messages: ${formatUnknownNumber(history.compactedRawMessages)}; tokens before compaction: ${history.compaction.tokensBefore.toLocaleString()}`
|
|
122
|
+
: "Compacted history: none on active branch",
|
|
123
|
+
"",
|
|
124
|
+
"Last recorded provider turn",
|
|
125
|
+
);
|
|
126
|
+
if (promptUsage) {
|
|
25
127
|
lines.push(
|
|
26
|
-
`
|
|
128
|
+
`Model: ${promptUsage.message.provider}/${promptUsage.message.model}`,
|
|
129
|
+
`Prompt tokens: ${calculatePromptTokens(promptUsage.usage).toLocaleString()}`,
|
|
130
|
+
`Input/output/cache: ${promptUsage.usage.input.toLocaleString()} / ${promptUsage.usage.output.toLocaleString()} / ${(
|
|
131
|
+
promptUsage.usage.cacheRead + promptUsage.usage.cacheWrite
|
|
132
|
+
).toLocaleString()}`,
|
|
133
|
+
`Cost: $${promptUsage.usage.cost.total.toFixed(6)}`,
|
|
27
134
|
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const fraction = breakdown.freeTokens / breakdown.contextWindow;
|
|
31
|
-
lines.push(` ${"Free".padEnd(16)} ${renderAsciiBar(fraction)} ${breakdown.freeTokens} tokens`);
|
|
135
|
+
} else {
|
|
136
|
+
lines.push("Usage/cost: unknown (no assistant response with recorded provider usage yet)");
|
|
32
137
|
}
|
|
33
138
|
return lines.join("\n");
|
|
34
139
|
} catch {
|
|
35
140
|
const fallback = runtime.session.getContextUsage();
|
|
36
141
|
if (!fallback) return "Context usage is unavailable.";
|
|
37
|
-
return [
|
|
142
|
+
return [
|
|
143
|
+
"Context usage",
|
|
144
|
+
`Active context: ${fallback.tokens === null || fallback.tokens === undefined ? "unknown" : fallback.tokens.toLocaleString()}`,
|
|
145
|
+
`Context window: ${fallback.contextWindow.toLocaleString()}`,
|
|
146
|
+
"Breakdown: unknown",
|
|
147
|
+
].join("\n");
|
|
38
148
|
}
|
|
39
149
|
}
|
package/src/task/agents.ts
CHANGED
package/src/task/executor.ts
CHANGED
|
@@ -23,7 +23,7 @@ import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prom
|
|
|
23
23
|
import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md" with { type: "text" };
|
|
24
24
|
import { AgentRegistry } from "../registry/agent-registry";
|
|
25
25
|
import { createAgentSession, discoverAuthStorage } from "../sdk";
|
|
26
|
-
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
26
|
+
import type { AgentSession, AgentSessionEvent, ForkContextSeed } from "../session/agent-session";
|
|
27
27
|
import type { ArtifactManager } from "../session/artifacts";
|
|
28
28
|
import type { AuthStorage } from "../session/auth-storage";
|
|
29
29
|
import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
|
|
@@ -157,6 +157,7 @@ export interface ExecutorOptions {
|
|
|
157
157
|
parentTelemetry?: AgentTelemetryConfig;
|
|
158
158
|
/** Skills to autoload via sendCustomMessage before the first prompt */
|
|
159
159
|
autoloadSkills?: Skill[];
|
|
160
|
+
forkContextSeed?: ForkContextSeed;
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -1143,6 +1144,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1143
1144
|
|
|
1144
1145
|
const { normalized: normalizedOutputSchema } = normalizeSchema(outputSchema);
|
|
1145
1146
|
|
|
1147
|
+
const forkContextNotice = options.forkContextSeed
|
|
1148
|
+
? `This subagent was started with a forked snapshot of the parent conversation. Included ${options.forkContextSeed.metadata.includedMessages} message(s), skipped ${options.forkContextSeed.metadata.skippedMessages}, approximately ${options.forkContextSeed.metadata.approximateTokens} tokens. The snapshot is not live; use IRC for live coordination when enabled.`
|
|
1149
|
+
: "";
|
|
1150
|
+
|
|
1146
1151
|
const { session } = await awaitAbortable(
|
|
1147
1152
|
createAgentSession({
|
|
1148
1153
|
cwd: worktree ?? cwd,
|
|
@@ -1167,6 +1172,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1167
1172
|
contextFile: contextFileForPrompt,
|
|
1168
1173
|
ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
|
|
1169
1174
|
ircSelfId: ircEnabled ? id : "",
|
|
1175
|
+
forkContext: forkContextNotice,
|
|
1170
1176
|
});
|
|
1171
1177
|
return defaultPrompt.length === 0
|
|
1172
1178
|
? [subagentPrompt]
|
|
@@ -1185,6 +1191,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1185
1191
|
enableMCP,
|
|
1186
1192
|
localProtocolOptions: options.localProtocolOptions,
|
|
1187
1193
|
telemetry: subagentTelemetry,
|
|
1194
|
+
forkContextSeed: options.forkContextSeed,
|
|
1188
1195
|
}),
|
|
1189
1196
|
);
|
|
1190
1197
|
|
|
@@ -1215,6 +1222,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1215
1222
|
task,
|
|
1216
1223
|
tools: session.getActiveToolNames(),
|
|
1217
1224
|
outputSchema,
|
|
1225
|
+
forkContext: options.forkContextSeed?.metadata,
|
|
1218
1226
|
});
|
|
1219
1227
|
|
|
1220
1228
|
abortSignal.addEventListener(
|
package/src/task/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ import * as fs from "node:fs/promises";
|
|
|
16
16
|
import * as os from "node:os";
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
19
|
-
import type { Usage } from "@gajae-code/ai";
|
|
19
|
+
import type { Model, Usage } from "@gajae-code/ai";
|
|
20
20
|
import { $env, prompt, Snowflake } from "@gajae-code/utils";
|
|
21
21
|
import type { ToolSession } from "..";
|
|
22
22
|
import { AsyncJobManager } from "../async";
|
|
@@ -26,12 +26,15 @@ import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" wit
|
|
|
26
26
|
import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
|
|
27
27
|
import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
|
|
28
28
|
import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
|
|
29
|
+
import type { ForkContextSeed } from "../session/agent-session";
|
|
29
30
|
import { formatBytes, formatDuration } from "../tools/render-utils";
|
|
30
31
|
import {
|
|
31
32
|
type AgentDefinition,
|
|
32
33
|
type AgentProgress,
|
|
34
|
+
type ForkContextPolicy,
|
|
33
35
|
getTaskSchema,
|
|
34
36
|
type SingleResult,
|
|
37
|
+
type TaskItem,
|
|
35
38
|
type TaskParams,
|
|
36
39
|
type TaskToolDetails,
|
|
37
40
|
type TaskToolSchemaInstance,
|
|
@@ -203,6 +206,33 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
|
|
|
203
206
|
return "task.simple is set to independent, so the task tool does not accept `context` or `schema`. Put all required background and output expectations inside each task assignment or the selected agent definition.";
|
|
204
207
|
}
|
|
205
208
|
|
|
209
|
+
function getForkContextPolicy(agent: AgentDefinition): ForkContextPolicy {
|
|
210
|
+
return agent.forkContext ?? "forbidden";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function validateForkContextRequests(
|
|
214
|
+
tasks: readonly TaskItem[],
|
|
215
|
+
agent: AgentDefinition,
|
|
216
|
+
forkContextEnabled: boolean,
|
|
217
|
+
): string | undefined {
|
|
218
|
+
const requested = tasks.filter(task => task.inheritContext === true);
|
|
219
|
+
if (requested.length === 0) return undefined;
|
|
220
|
+
const taskIds = requested.map(task => task.id).join(", ");
|
|
221
|
+
if (!forkContextEnabled) {
|
|
222
|
+
return `Cannot inherit parent context for task(s) ${taskIds}: task.forkContext.enabled is false.`;
|
|
223
|
+
}
|
|
224
|
+
if (getForkContextPolicy(agent) !== "allowed") {
|
|
225
|
+
return `Cannot inherit parent context for task(s) ${taskIds}: agent '${agent.name}' does not declare forkContext: allowed.`;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveForkContextMaxTokens(configured: number, model: Model | undefined): number {
|
|
231
|
+
if (configured > 0) return Math.trunc(configured);
|
|
232
|
+
const contextWindow = model?.contextWindow ?? 0;
|
|
233
|
+
return contextWindow > 0 ? Math.max(1, Math.floor(contextWindow * 0.25)) : 25_000;
|
|
234
|
+
}
|
|
235
|
+
|
|
206
236
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
207
237
|
// Tool Class
|
|
208
238
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -314,6 +344,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
314
344
|
};
|
|
315
345
|
}
|
|
316
346
|
|
|
347
|
+
const forkContextValidationError = validateForkContextRequests(
|
|
348
|
+
taskItems,
|
|
349
|
+
agent,
|
|
350
|
+
this.session.settings.get("task.forkContext.enabled"),
|
|
351
|
+
);
|
|
352
|
+
if (forkContextValidationError) {
|
|
353
|
+
return createTaskModeError(forkContextValidationError);
|
|
354
|
+
}
|
|
355
|
+
|
|
317
356
|
const manager = AsyncJobManager.instance();
|
|
318
357
|
if (!manager) {
|
|
319
358
|
return {
|
|
@@ -378,6 +417,20 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
378
417
|
|
|
379
418
|
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
|
|
380
419
|
const semaphore = new Semaphore(maxConcurrency);
|
|
420
|
+
const buildForkContextSeedForTask = async (task: TaskItem): Promise<ForkContextSeed | undefined> => {
|
|
421
|
+
if (task.inheritContext !== true) return undefined;
|
|
422
|
+
if (!this.session.buildForkContextSeed) {
|
|
423
|
+
throw new Error("Current session cannot build fork-context seeds.");
|
|
424
|
+
}
|
|
425
|
+
const maxMessages = this.session.settings.get("task.forkContext.maxMessages");
|
|
426
|
+
const configuredMaxTokens = this.session.settings.get("task.forkContext.maxTokens");
|
|
427
|
+
return await this.session.buildForkContextSeed({
|
|
428
|
+
maxMessages,
|
|
429
|
+
maxTokens: resolveForkContextMaxTokens(configuredMaxTokens, this.session.model),
|
|
430
|
+
signal,
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
const frozenForkSeeds = new Map<string, ForkContextSeed>();
|
|
381
434
|
|
|
382
435
|
for (let i = 0; i < taskItems.length; i++) {
|
|
383
436
|
const taskItem = taskItems[i];
|
|
@@ -391,6 +444,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
391
444
|
}
|
|
392
445
|
|
|
393
446
|
const uniqueId = uniqueIds[i];
|
|
447
|
+
const frozenForkSeed = await buildForkContextSeedForTask(taskItem);
|
|
448
|
+
if (frozenForkSeed) frozenForkSeeds.set(uniqueId, frozenForkSeed);
|
|
394
449
|
const singleParams: TaskParams = { ...params, tasks: [taskItem] };
|
|
395
450
|
const label = uniqueId;
|
|
396
451
|
try {
|
|
@@ -416,9 +471,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
416
471
|
buildAsyncDetails("running", startedJobs[0]?.jobId ?? label) as unknown as Record<string, unknown>,
|
|
417
472
|
);
|
|
418
473
|
try {
|
|
419
|
-
const result = await this.#executeSync(
|
|
420
|
-
|
|
421
|
-
|
|
474
|
+
const result = await this.#executeSync(
|
|
475
|
+
_toolCallId,
|
|
476
|
+
singleParams,
|
|
477
|
+
runSignal,
|
|
478
|
+
undefined,
|
|
479
|
+
[uniqueId],
|
|
480
|
+
frozenForkSeeds,
|
|
481
|
+
);
|
|
422
482
|
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
423
483
|
const singleResult = result.details?.results[0];
|
|
424
484
|
if (progress) {
|
|
@@ -576,6 +636,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
576
636
|
signal?: AbortSignal,
|
|
577
637
|
onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
|
|
578
638
|
preAllocatedIds?: string[],
|
|
639
|
+
prebuiltForkContextSeeds?: ReadonlyMap<string, ForkContextSeed>,
|
|
579
640
|
): Promise<AgentToolResult<TaskToolDetails>> {
|
|
580
641
|
const startTime = Date.now();
|
|
581
642
|
const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
|
|
@@ -651,6 +712,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
651
712
|
};
|
|
652
713
|
}
|
|
653
714
|
|
|
715
|
+
const forkContextValidationError = validateForkContextRequests(
|
|
716
|
+
params.tasks ?? [],
|
|
717
|
+
agent,
|
|
718
|
+
this.session.settings.get("task.forkContext.enabled"),
|
|
719
|
+
);
|
|
720
|
+
if (forkContextValidationError) {
|
|
721
|
+
return createTaskModeError(forkContextValidationError);
|
|
722
|
+
}
|
|
723
|
+
|
|
654
724
|
const planModeState = this.session.getPlanModeState?.();
|
|
655
725
|
const planModeTools = ["read", "search", "find", "lsp", "web_search"];
|
|
656
726
|
const effectiveAgent: typeof agent = planModeState?.enabled
|
|
@@ -912,7 +982,22 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
912
982
|
}
|
|
913
983
|
emitProgress();
|
|
914
984
|
|
|
985
|
+
const buildForkContextSeed = async (task: (typeof tasksWithUniqueIds)[number]) => {
|
|
986
|
+
if (task.inheritContext !== true) return undefined;
|
|
987
|
+
if (!this.session.buildForkContextSeed) {
|
|
988
|
+
throw new Error("Current session cannot build fork-context seeds.");
|
|
989
|
+
}
|
|
990
|
+
const maxMessages = this.session.settings.get("task.forkContext.maxMessages");
|
|
991
|
+
const configuredMaxTokens = this.session.settings.get("task.forkContext.maxTokens");
|
|
992
|
+
return await this.session.buildForkContextSeed({
|
|
993
|
+
maxMessages,
|
|
994
|
+
maxTokens: resolveForkContextMaxTokens(configuredMaxTokens, this.session.model),
|
|
995
|
+
signal,
|
|
996
|
+
});
|
|
997
|
+
};
|
|
998
|
+
|
|
915
999
|
const runTask = async (task: (typeof tasksWithUniqueIds)[number], index: number) => {
|
|
1000
|
+
const forkContextSeed = prebuiltForkContextSeeds?.get(task.id) ?? (await buildForkContextSeed(task));
|
|
916
1001
|
if (!isIsolated) {
|
|
917
1002
|
return runSubprocess({
|
|
918
1003
|
cwd: this.session.cwd,
|
|
@@ -953,6 +1038,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
953
1038
|
parentArtifactManager,
|
|
954
1039
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
955
1040
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
1041
|
+
forkContextSeed,
|
|
956
1042
|
});
|
|
957
1043
|
}
|
|
958
1044
|
|
|
@@ -1007,6 +1093,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1007
1093
|
parentArtifactManager,
|
|
1008
1094
|
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
1009
1095
|
parentTelemetry: this.session.getTelemetry?.(),
|
|
1096
|
+
forkContextSeed,
|
|
1010
1097
|
});
|
|
1011
1098
|
if (mergeMode === "branch" && result.exitCode === 0) {
|
|
1012
1099
|
try {
|
package/src/task/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { NestedRepoPatch } from "./worktree";
|
|
|
7
7
|
|
|
8
8
|
/** Source of an agent definition */
|
|
9
9
|
export type AgentSource = "bundled" | "user" | "project";
|
|
10
|
+
export type ForkContextPolicy = "forbidden" | "allowed";
|
|
10
11
|
|
|
11
12
|
const parseNumber = (value: string | undefined, defaultValue: number): number => {
|
|
12
13
|
if (value) {
|
|
@@ -64,6 +65,10 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
|
|
|
64
65
|
id: z.string().max(48).describe("camelcase identifier"),
|
|
65
66
|
description: z.string().describe("ui label, not seen by subagent"),
|
|
66
67
|
assignment: z.string().describe(assignmentDescription),
|
|
68
|
+
inheritContext: z
|
|
69
|
+
.boolean()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("explicit request to seed a subagent with sanitized parent context"),
|
|
67
72
|
});
|
|
68
73
|
|
|
69
74
|
/** Single task item for parallel execution (default shape with context enabled). */
|
|
@@ -175,6 +180,7 @@ export interface AgentDefinition {
|
|
|
175
180
|
blocking?: boolean;
|
|
176
181
|
autoloadSkills?: string[];
|
|
177
182
|
hide?: boolean;
|
|
183
|
+
forkContext?: ForkContextPolicy;
|
|
178
184
|
source: AgentSource;
|
|
179
185
|
filePath?: string;
|
|
180
186
|
}
|
package/src/tools/ask.ts
CHANGED
|
@@ -149,6 +149,7 @@ interface UIContext {
|
|
|
149
149
|
timeout?: number;
|
|
150
150
|
signal?: AbortSignal;
|
|
151
151
|
outline?: boolean;
|
|
152
|
+
wrapFocused?: boolean;
|
|
152
153
|
onTimeout?: () => void;
|
|
153
154
|
onLeft?: () => void;
|
|
154
155
|
onRight?: () => void;
|
|
@@ -194,6 +195,7 @@ async function askSingleQuestion(
|
|
|
194
195
|
timeout,
|
|
195
196
|
signal,
|
|
196
197
|
outline: true,
|
|
198
|
+
wrapFocused: true,
|
|
197
199
|
onTimeout,
|
|
198
200
|
helpText,
|
|
199
201
|
onLeft: navigation?.allowBack
|