@gajae-code/coding-agent 0.2.1 → 0.2.3

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 (153) hide show
  1. package/CHANGELOG.md +59 -1
  2. package/dist/types/cli/setup-cli.d.ts +1 -0
  3. package/dist/types/commands/contribution-prep.d.ts +18 -0
  4. package/dist/types/commands/deep-interview.d.ts +41 -0
  5. package/dist/types/commands/session.d.ts +24 -0
  6. package/dist/types/commands/setup.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +2 -2
  8. package/dist/types/config/models-config-schema.d.ts +17 -9
  9. package/dist/types/config/settings-schema.d.ts +37 -24
  10. package/dist/types/discovery/helpers.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +6 -0
  12. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +33 -0
  13. package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +12 -11
  15. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +25 -0
  16. package/dist/types/gjc-runtime/state-runtime.d.ts +13 -0
  17. package/dist/types/gjc-runtime/team-runtime.d.ts +37 -5
  18. package/dist/types/gjc-runtime/tmux-common.d.ts +41 -0
  19. package/dist/types/gjc-runtime/tmux-sessions.d.ts +17 -0
  20. package/dist/types/goals/runtime.d.ts +3 -9
  21. package/dist/types/goals/state.d.ts +3 -6
  22. package/dist/types/goals/tools/goal-tool.d.ts +1 -69
  23. package/dist/types/hooks/skill-state.d.ts +5 -0
  24. package/dist/types/memories/index.d.ts +1 -1
  25. package/dist/types/memory-backend/local-backend.d.ts +3 -3
  26. package/dist/types/modes/components/hook-selector.d.ts +7 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +0 -2
  28. package/dist/types/modes/components/status-line/types.d.ts +0 -3
  29. package/dist/types/modes/components/status-line.d.ts +0 -3
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  31. package/dist/types/modes/interactive-mode.d.ts +1 -12
  32. package/dist/types/modes/theme/defaults/index.d.ts +0 -2
  33. package/dist/types/modes/theme/theme.d.ts +1 -2
  34. package/dist/types/modes/types.d.ts +1 -7
  35. package/dist/types/modes/utils/context-usage.d.ts +6 -2
  36. package/dist/types/sdk.d.ts +6 -2
  37. package/dist/types/session/agent-session.d.ts +47 -1
  38. package/dist/types/session/contribution-prep.d.ts +47 -0
  39. package/dist/types/session/session-manager.d.ts +3 -0
  40. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
  41. package/dist/types/setup/provider-onboarding.d.ts +29 -5
  42. package/dist/types/skill-state/active-state.d.ts +30 -1
  43. package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +6 -1
  44. package/dist/types/skill-state/initial-phase.d.ts +12 -0
  45. package/dist/types/skill-state/workflow-hud.d.ts +9 -4
  46. package/dist/types/skill-state/workflow-state-contract.d.ts +34 -0
  47. package/dist/types/task/executor.d.ts +2 -0
  48. package/dist/types/task/types.d.ts +11 -0
  49. package/dist/types/tools/index.d.ts +20 -1
  50. package/dist/types/tools/skill.d.ts +47 -0
  51. package/dist/types/utils/changelog.d.ts +18 -2
  52. package/package.json +7 -7
  53. package/src/cli/args.ts +3 -2
  54. package/src/cli/setup-cli.ts +26 -12
  55. package/src/cli.ts +7 -1
  56. package/src/commands/contribution-prep.ts +41 -0
  57. package/src/commands/deep-interview.ts +30 -23
  58. package/src/commands/launch.ts +10 -1
  59. package/src/commands/ralplan.ts +10 -22
  60. package/src/commands/session.ts +150 -0
  61. package/src/commands/setup.ts +2 -0
  62. package/src/commands/state.ts +15 -4
  63. package/src/commands/team.ts +23 -3
  64. package/src/config/model-registry.ts +10 -2
  65. package/src/config/models-config-schema.ts +120 -102
  66. package/src/config/settings-schema.ts +42 -25
  67. package/src/config.ts +1 -1
  68. package/src/defaults/gjc/skills/deep-interview/SKILL.md +32 -13
  69. package/src/defaults/gjc/skills/ralplan/SKILL.md +22 -2
  70. package/src/defaults/gjc/skills/team/SKILL.md +39 -7
  71. package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -25
  72. package/src/discovery/helpers.ts +24 -1
  73. package/src/eval/py/prelude.py +1 -1
  74. package/src/extensibility/extensions/types.ts +6 -0
  75. package/src/gjc-runtime/deep-interview-runtime.ts +546 -0
  76. package/src/gjc-runtime/goal-mode-request.ts +2 -19
  77. package/src/gjc-runtime/launch-tmux.ts +83 -43
  78. package/src/gjc-runtime/ralplan-runtime.ts +460 -0
  79. package/src/gjc-runtime/state-runtime.ts +731 -0
  80. package/src/gjc-runtime/team-runtime.ts +708 -52
  81. package/src/gjc-runtime/tmux-common.ts +119 -0
  82. package/src/gjc-runtime/tmux-sessions.ts +165 -0
  83. package/src/gjc-runtime/ultragoal-guard.ts +6 -3
  84. package/src/gjc-runtime/ultragoal-runtime.ts +5 -4
  85. package/src/goals/runtime.ts +38 -144
  86. package/src/goals/state.ts +36 -7
  87. package/src/goals/tools/goal-tool.ts +15 -172
  88. package/src/hooks/skill-state.ts +39 -18
  89. package/src/internal-urls/docs-index.generated.ts +5 -4
  90. package/src/internal-urls/memory-protocol.ts +3 -2
  91. package/src/main.ts +2 -3
  92. package/src/memories/index.ts +2 -1
  93. package/src/memory-backend/local-backend.ts +14 -6
  94. package/src/modes/components/hook-selector.ts +156 -1
  95. package/src/modes/components/settings-selector.ts +5 -12
  96. package/src/modes/components/skill-hud/render.ts +4 -0
  97. package/src/modes/components/status-line/segments.ts +5 -16
  98. package/src/modes/components/status-line/types.ts +0 -3
  99. package/src/modes/components/status-line.ts +0 -6
  100. package/src/modes/controllers/command-controller.ts +27 -4
  101. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  102. package/src/modes/controllers/input-controller.ts +0 -15
  103. package/src/modes/controllers/selector-controller.ts +4 -11
  104. package/src/modes/interactive-mode.ts +18 -219
  105. package/src/modes/theme/defaults/dark-poimandres.json +0 -1
  106. package/src/modes/theme/defaults/light-poimandres.json +0 -1
  107. package/src/modes/theme/theme.ts +0 -6
  108. package/src/modes/types.ts +1 -7
  109. package/src/modes/utils/context-usage.ts +66 -17
  110. package/src/prompts/agents/architect.md +3 -0
  111. package/src/prompts/agents/executor.md +2 -0
  112. package/src/prompts/agents/frontmatter.md +1 -0
  113. package/src/prompts/goals/goal-continuation.md +1 -4
  114. package/src/prompts/goals/goal-mode-active.md +3 -5
  115. package/src/prompts/system/subagent-system-prompt.md +6 -0
  116. package/src/prompts/system/system-prompt.md +5 -7
  117. package/src/prompts/tools/goal.md +4 -4
  118. package/src/prompts/tools/skill.md +28 -0
  119. package/src/prompts/tools/task.md +3 -0
  120. package/src/sdk.ts +51 -11
  121. package/src/session/agent-session.ts +222 -21
  122. package/src/session/contribution-prep.ts +320 -0
  123. package/src/session/session-manager.ts +9 -1
  124. package/src/setup/model-onboarding-guidance.ts +6 -3
  125. package/src/setup/provider-onboarding.ts +177 -16
  126. package/src/skill-state/active-state.ts +188 -25
  127. package/src/skill-state/deep-interview-mutation-guard.ts +72 -21
  128. package/src/skill-state/initial-phase.ts +17 -0
  129. package/src/skill-state/workflow-hud.ts +23 -5
  130. package/src/skill-state/workflow-state-contract.ts +121 -0
  131. package/src/slash-commands/builtin-registry.ts +75 -25
  132. package/src/slash-commands/helpers/context-report.ts +123 -13
  133. package/src/task/agents.ts +1 -0
  134. package/src/task/commands.ts +1 -5
  135. package/src/task/executor.ts +9 -1
  136. package/src/task/index.ts +91 -4
  137. package/src/task/types.ts +6 -0
  138. package/src/tools/ask.ts +2 -0
  139. package/src/tools/gh.ts +212 -2
  140. package/src/tools/index.ts +25 -6
  141. package/src/tools/skill.ts +153 -0
  142. package/src/utils/changelog.ts +67 -44
  143. package/dist/types/commands/gjc-runtime-bridge.d.ts +0 -30
  144. package/dist/types/commands/question.d.ts +0 -7
  145. package/dist/types/modes/loop-limit.d.ts +0 -22
  146. package/src/commands/gjc-runtime-bridge.ts +0 -227
  147. package/src/commands/question.ts +0 -12
  148. package/src/modes/loop-limit.ts +0 -140
  149. package/src/prompts/commands/orchestrate.md +0 -49
  150. package/src/prompts/goals/goal-budget-limit.md +0 -16
  151. package/src/prompts/tools/create-goal.md +0 -3
  152. package/src/prompts/tools/get-goal.md +0 -3
  153. package/src/prompts/tools/update-goal.md +0 -3
@@ -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 === "--compat") {
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,17 +216,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
202
216
  runtime.ctx.editor.setText("");
203
217
  },
204
218
  },
205
- {
206
- name: "loop",
207
- description:
208
- "Toggle loop mode. While enabled, the next prompt you send re-submits after every yield. Esc cancels the current iteration; /loop again to disable.",
209
- inlineHint: "[count|duration]",
210
- allowArgs: true,
211
- handleTui: async (command, runtime) => {
212
- await runtime.ctx.handleLoopCommand(command.args);
213
- runtime.ctx.editor.setText("");
214
- },
215
- },
216
219
  {
217
220
  name: "goal",
218
221
  description: "Toggle goal mode (persistent autonomous objective for this session)",
@@ -222,7 +225,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
222
225
  { name: "pause", description: "Pause the current goal" },
223
226
  { name: "resume", description: "Resume a paused goal" },
224
227
  { name: "drop", description: "Drop the current goal" },
225
- { name: "budget", description: "Adjust the token budget", usage: "<N|off>" },
226
228
  ],
227
229
  inlineHint: "[objective]",
228
230
  allowArgs: true,
@@ -515,6 +517,19 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
515
517
  runtime.ctx.editor.setText("");
516
518
  },
517
519
  },
520
+ {
521
+ name: "context",
522
+ description: "Show active context token usage breakdown",
523
+ acpDescription: "Show active context token usage breakdown",
524
+ handle: async (_command, runtime) => {
525
+ await runtime.output(buildContextReportText(runtime));
526
+ return commandConsumed();
527
+ },
528
+ handleTui: (_command, runtime) => {
529
+ runtime.ctx.handleContextCommand();
530
+ runtime.ctx.editor.setText("");
531
+ },
532
+ },
518
533
  {
519
534
  name: "usage",
520
535
  description: "Show provider usage and limits",
@@ -592,20 +607,30 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
592
607
  if (!args.startsWith("add ")) return usage(providerSetupUsage(), runtime);
593
608
  const parsed = parseProviderSetupSlashArgs(args.slice(4));
594
609
  const missing: string[] = [];
595
- if (!parsed.compat) missing.push("--compat");
596
- if (!parsed.provider) missing.push("--provider");
597
- if (!parsed.baseUrl) missing.push("--base-url");
610
+ if (!parsed.preset) {
611
+ if (!parsed.compat) missing.push("--compat");
612
+ if (!parsed.provider) missing.push("--provider");
613
+ if (!parsed.baseUrl) missing.push("--base-url");
614
+ }
598
615
  if (parsed.rejectedRawApiKey) {
599
616
  return usage("Provider setup rejects raw --api-key values; use --api-key-env <ENV> instead.", runtime);
600
617
  }
601
- if (!parsed.apiKeyEnv) missing.push("--api-key-env");
602
- if (parsed.models.length === 0) missing.push("--model");
603
- if (missing.length > 0) return usage(`Missing required option(s): ${missing.join(", ")}`, runtime);
618
+ if (!parsed.preset) {
619
+ if (!parsed.apiKeyEnv) missing.push("--api-key-env");
620
+ if (parsed.models.length === 0) missing.push("--model");
621
+ }
622
+ if (missing.length > 0) {
623
+ return usage(
624
+ `Missing required option(s): ${missing.join(", ")}. Or use /provider add --preset <preset>.`,
625
+ runtime,
626
+ );
627
+ }
604
628
  try {
605
629
  const result = await addApiCompatibleProvider({
606
- compatibility: parseProviderCompatibility(parsed.compat!),
607
- providerId: parsed.provider!,
608
- baseUrl: parsed.baseUrl!,
630
+ compatibility: parsed.compat ? parseProviderCompatibility(parsed.compat) : undefined,
631
+ preset: parsed.preset,
632
+ providerId: parsed.provider,
633
+ baseUrl: parsed.baseUrl,
609
634
  apiKeyEnv: parsed.apiKeyEnv,
610
635
  models: parsed.models,
611
636
  force: parsed.force,
@@ -643,9 +668,10 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
643
668
  throw new Error("Provider setup rejects raw --api-key values; use --api-key-env <ENV> instead.");
644
669
  }
645
670
  const result = await addApiCompatibleProvider({
646
- compatibility: parseProviderCompatibility(parsed.compat ?? ""),
647
- providerId: parsed.provider ?? "",
648
- baseUrl: parsed.baseUrl ?? "",
671
+ compatibility: parsed.compat ? parseProviderCompatibility(parsed.compat) : undefined,
672
+ preset: parsed.preset,
673
+ providerId: parsed.provider,
674
+ baseUrl: parsed.baseUrl,
649
675
  apiKeyEnv: parsed.apiKeyEnv,
650
676
  models: parsed.models,
651
677
  force: parsed.force,
@@ -793,6 +819,30 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
793
819
  await runtime.ctx.handleCompactCommand(customInstructions);
794
820
  },
795
821
  },
822
+ {
823
+ name: "contribute-pr",
824
+ aliases: ["contribution-prep"],
825
+ description: "Dump redacted session context and spawn a fresh contribute-pr worker",
826
+ inlineHint: "[focus instructions]",
827
+ allowArgs: true,
828
+ handle: async (command, runtime) => {
829
+ const result = await runtime.session.prepareContributionPrep({
830
+ customInstructions: command.args || undefined,
831
+ spawnWorker: true,
832
+ });
833
+ await runtime.output(
834
+ [
835
+ "Contribution prep artifacts written.",
836
+ `Manifest: ${result.manifestPath}`,
837
+ `Worker prompt: ${result.workerPromptPath}`,
838
+ ].join("\n"),
839
+ );
840
+ return commandConsumed();
841
+ },
842
+ handleTui: async (command, runtime) => {
843
+ await runtime.ctx.handleContributionPrepCommand(command.args || undefined);
844
+ },
845
+ },
796
846
  {
797
847
  name: "resume",
798
848
  description: "Resume a different session",
@@ -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 breakdown = computeContextBreakdown(runtime.session);
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 usedPct = Math.round((breakdown.usedTokens / breakdown.contextWindow) * 100);
17
- const lines = [`Context window: ${breakdown.contextWindow} tokens (${usedPct}% used)`];
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
- if (category.tokens === 0) continue;
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
- const fraction = breakdown.autoCompactBufferTokens / breakdown.contextWindow;
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
- ` ${"Auto-compact buf".padEnd(16)} ${renderAsciiBar(fraction)} ${breakdown.autoCompactBufferTokens} tokens`,
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
- if (breakdown.freeTokens > 0) {
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 ["Context", `Window: ${fallback.contextWindow}`, `Used: ${fallback.tokens ?? 0}`].join("\n");
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
  }
@@ -29,6 +29,7 @@ interface AgentFrontmatter {
29
29
  thinkingLevel?: string;
30
30
  blocking?: boolean;
31
31
  hide?: boolean;
32
+ forkContext?: "forbidden" | "allowed";
32
33
  }
33
34
 
34
35
  interface EmbeddedAgentDef {
@@ -9,12 +9,8 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
9
9
  import { loadCapability } from "../discovery";
10
10
  // Embed command markdown files at build time
11
11
  import initMd from "../prompts/agents/init.md" with { type: "text" };
12
- import orchestrateMd from "../prompts/commands/orchestrate.md" with { type: "text" };
13
12
 
14
- const EMBEDDED_COMMANDS: { name: string; content: string }[] = [
15
- { name: "init.md", content: prompt.render(initMd) },
16
- { name: "orchestrate.md", content: prompt.render(orchestrateMd) },
17
- ];
13
+ const EMBEDDED_COMMANDS: { name: string; content: string }[] = [{ name: "init.md", content: prompt.render(initMd) }];
18
14
 
19
15
  export const EMBEDDED_COMMAND_TEMPLATES: ReadonlyArray<{ name: string; content: string }> = EMBEDDED_COMMANDS;
20
16
 
@@ -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(_toolCallId, singleParams, runSignal, undefined, [
420
- uniqueId,
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