@gajae-code/coding-agent 0.4.3 → 0.4.5
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 +42 -0
- package/dist/types/async/job-manager.d.ts +19 -1
- package/dist/types/cli/fast-help.d.ts +1 -0
- package/dist/types/cli/setup-cli.d.ts +16 -1
- package/dist/types/commands/coordinator.d.ts +19 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/commands/mcp-serve.d.ts +24 -0
- package/dist/types/commands/setup.d.ts +47 -0
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/coordinator/contract.d.ts +4 -0
- package/dist/types/coordinator-mcp/policy.d.ts +24 -0
- package/dist/types/coordinator-mcp/safety.d.ts +26 -0
- package/dist/types/coordinator-mcp/server.d.ts +58 -0
- package/dist/types/extensibility/extensions/types.d.ts +13 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +46 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/types.d.ts +9 -1
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/hook-selector.d.ts +11 -0
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +8 -0
- package/dist/types/setup/hermes-setup.d.ts +78 -0
- package/dist/types/task/fork-context-advisory.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/render.d.ts +7 -1
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +10 -0
- package/dist/types/tools/subagent-render.d.ts +25 -0
- package/dist/types/tools/subagent.d.ts +5 -1
- package/package.json +8 -7
- package/scripts/build-binary.ts +4 -0
- package/src/async/job-manager.ts +43 -1
- package/src/cli/fast-help.ts +80 -0
- package/src/cli/setup-cli.ts +95 -2
- package/src/cli.ts +109 -16
- package/src/commands/coordinator.ts +113 -0
- package/src/commands/harness.ts +92 -9
- package/src/commands/mcp-serve.ts +63 -0
- package/src/commands/setup.ts +34 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/coordinator/contract.ts +21 -0
- package/src/coordinator-mcp/policy.ts +160 -0
- package/src/coordinator-mcp/safety.ts +80 -0
- package/src/coordinator-mcp/server.ts +1519 -0
- package/src/cursor.ts +30 -2
- package/src/extensibility/extensions/types.ts +13 -0
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +117 -0
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +9 -1
- package/src/harness-control-plane/phase-rollup.ts +96 -0
- package/src/harness-control-plane/receipt-ingest.ts +127 -0
- package/src/harness-control-plane/receipts.ts +229 -1
- package/src/harness-control-plane/rpc-adapter.ts +8 -0
- package/src/harness-control-plane/types.ts +29 -1
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/main.ts +7 -3
- package/src/modes/components/hook-selector.ts +109 -5
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/controllers/extension-ui-controller.ts +16 -1
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/utils/abort-message.ts +41 -0
- package/src/modes/utils/context-usage.ts +15 -8
- package/src/modes/utils/ui-helpers.ts +5 -6
- package/src/prompts/agents/architect.md +6 -0
- package/src/prompts/agents/critic.md +6 -0
- package/src/prompts/agents/planner.md +8 -1
- package/src/sdk.ts +9 -4
- package/src/session/agent-session.ts +22 -5
- package/src/session/session-manager.ts +20 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +30 -0
- package/src/setup/hermes-setup.ts +484 -0
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +33 -2
- package/src/task/receipt.ts +2 -0
- package/src/task/render.ts +14 -0
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +7 -0
- package/src/tools/ask.ts +30 -10
- package/src/tools/index.ts +2 -2
- package/src/tools/renderers.ts +2 -0
- package/src/tools/subagent-render.ts +169 -0
- package/src/tools/subagent.ts +49 -7
- package/src/utils/title-generator.ts +16 -2
package/src/main.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { getDefault, type SettingPath, Settings, settings } from "./config/setti
|
|
|
33
33
|
import { initializeWithSettings } from "./discovery";
|
|
34
34
|
import { exportFromFile } from "./export/html";
|
|
35
35
|
import type { ExtensionUIContext } from "./extensibility/extensions/types";
|
|
36
|
-
import { InteractiveMode
|
|
36
|
+
import type { InteractiveMode } from "./modes/interactive-mode";
|
|
37
37
|
import { initTheme, stopThemeWatcher } from "./modes/theme/theme";
|
|
38
38
|
import type { SubmittedUserInput } from "./modes/types";
|
|
39
39
|
import type { MCPManager } from "./runtime-mcp";
|
|
@@ -304,6 +304,7 @@ async function runInteractiveMode(
|
|
|
304
304
|
initialMessage?: string,
|
|
305
305
|
initialImages?: ImageContent[],
|
|
306
306
|
): Promise<void> {
|
|
307
|
+
const { InteractiveMode } = await import("./modes/interactive-mode");
|
|
307
308
|
const mode = new InteractiveMode(
|
|
308
309
|
session,
|
|
309
310
|
version,
|
|
@@ -706,7 +707,7 @@ async function buildSessionOptions(
|
|
|
706
707
|
interface RunRootCommandDependencies {
|
|
707
708
|
createAgentSession?: typeof createAgentSession;
|
|
708
709
|
discoverAuthStorage?: typeof discoverAuthStorage;
|
|
709
|
-
runAcpMode?:
|
|
710
|
+
runAcpMode?: (createSession: AcpSessionFactory) => Promise<void>;
|
|
710
711
|
settings?: Settings;
|
|
711
712
|
}
|
|
712
713
|
|
|
@@ -927,7 +928,7 @@ export async function runRootCommand(
|
|
|
927
928
|
rawArgs,
|
|
928
929
|
createSession,
|
|
929
930
|
});
|
|
930
|
-
await (deps.runAcpMode ?? runAcpMode)(createAcpSession);
|
|
931
|
+
await (deps.runAcpMode ?? (await import("./modes/acp")).runAcpMode)(createAcpSession);
|
|
931
932
|
} else {
|
|
932
933
|
const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } =
|
|
933
934
|
await createSession(sessionOptions);
|
|
@@ -973,8 +974,10 @@ export async function runRootCommand(
|
|
|
973
974
|
}
|
|
974
975
|
|
|
975
976
|
if (mode === "rpc" || mode === "rpc-ui") {
|
|
977
|
+
const { runRpcMode } = await import("./modes/rpc/rpc-mode");
|
|
976
978
|
await runRpcMode(session, mode === "rpc-ui" ? setToolUIContext : undefined);
|
|
977
979
|
} else if (mode === "bridge") {
|
|
980
|
+
const { runBridgeMode } = await import("./modes/bridge/bridge-mode");
|
|
978
981
|
await runBridgeMode(session, setToolUIContext);
|
|
979
982
|
} else if (isInteractive) {
|
|
980
983
|
const versionCheckPromise = checkForNewVersion(VERSION).catch(() => undefined);
|
|
@@ -1014,6 +1017,7 @@ export async function runRootCommand(
|
|
|
1014
1017
|
initialImages,
|
|
1015
1018
|
);
|
|
1016
1019
|
} else {
|
|
1020
|
+
const { runPrintMode } = await import("./modes/print-mode");
|
|
1017
1021
|
await runPrintMode(session, {
|
|
1018
1022
|
mode,
|
|
1019
1023
|
messages: parsedArgs.messages,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import {
|
|
6
6
|
Container,
|
|
7
|
+
Editor,
|
|
7
8
|
Markdown,
|
|
8
9
|
matchesKey,
|
|
9
10
|
padding,
|
|
@@ -16,8 +17,13 @@ import {
|
|
|
16
17
|
visibleWidth,
|
|
17
18
|
wrapTextWithAnsi,
|
|
18
19
|
} from "@gajae-code/tui";
|
|
19
|
-
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
20
|
-
import {
|
|
20
|
+
import { getEditorTheme, getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
21
|
+
import {
|
|
22
|
+
matchesAppExternalEditor,
|
|
23
|
+
matchesAppInterrupt,
|
|
24
|
+
matchesSelectCancel,
|
|
25
|
+
} from "../../modes/utils/keybinding-matchers";
|
|
26
|
+
import { getEditorCommand, openInEditor } from "../../utils/external-editor";
|
|
21
27
|
import { CountdownTimer } from "./countdown-timer";
|
|
22
28
|
import { DynamicBorder } from "./dynamic-border";
|
|
23
29
|
|
|
@@ -56,6 +62,17 @@ export interface HookSelectorOptions {
|
|
|
56
62
|
*/
|
|
57
63
|
wrapFocused?: boolean;
|
|
58
64
|
scrollTitleRows?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Inline free-text entry for the option with this label (e.g. the ask
|
|
67
|
+
* tool's "Other (type your own)"). Selecting it keeps the title and option
|
|
68
|
+
* list on screen and opens a prompt-style editor below the list instead of
|
|
69
|
+
* replacing the whole selector. Enter submits via `onSubmit`; Escape
|
|
70
|
+
* returns to option selection.
|
|
71
|
+
*/
|
|
72
|
+
customInput?: {
|
|
73
|
+
optionLabel: string;
|
|
74
|
+
onSubmit: (text: string) => void;
|
|
75
|
+
};
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
class OutlinedList extends Container {
|
|
@@ -297,6 +314,12 @@ export class HookSelectorComponent extends Container {
|
|
|
297
314
|
#wrapFocused: boolean;
|
|
298
315
|
#outline: boolean;
|
|
299
316
|
#scrollTitleRows: number | undefined;
|
|
317
|
+
#customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
|
|
318
|
+
#inputArea: Container;
|
|
319
|
+
#inlineEditor: Editor | undefined;
|
|
320
|
+
#helpTextComponent: Text;
|
|
321
|
+
#baseHelpText: string;
|
|
322
|
+
#tui: TUI | undefined;
|
|
300
323
|
constructor(
|
|
301
324
|
title: string,
|
|
302
325
|
options: string[],
|
|
@@ -317,6 +340,8 @@ export class HookSelectorComponent extends Container {
|
|
|
317
340
|
this.#onExternalEditorCallback = opts?.onExternalEditor;
|
|
318
341
|
this.#wrapFocused = opts?.wrapFocused === true;
|
|
319
342
|
this.#outline = opts?.outline === true;
|
|
343
|
+
this.#customInput = opts?.customInput;
|
|
344
|
+
this.#tui = opts?.tui;
|
|
320
345
|
|
|
321
346
|
this.addChild(new DynamicBorder());
|
|
322
347
|
this.addChild(new Spacer(1));
|
|
@@ -364,9 +389,12 @@ export class HookSelectorComponent extends Container {
|
|
|
364
389
|
this.#listContainer = new Container();
|
|
365
390
|
this.addChild(this.#listContainer);
|
|
366
391
|
}
|
|
392
|
+
this.#inputArea = new Container();
|
|
393
|
+
this.addChild(this.#inputArea);
|
|
367
394
|
this.addChild(new Spacer(1));
|
|
368
|
-
|
|
369
|
-
this
|
|
395
|
+
this.#baseHelpText = opts?.helpText ?? "up/down navigate enter select esc cancel";
|
|
396
|
+
this.#helpTextComponent = new Text(theme.fg("dim", this.#baseHelpText), 1, 0);
|
|
397
|
+
this.addChild(this.#helpTextComponent);
|
|
370
398
|
this.addChild(new Spacer(1));
|
|
371
399
|
this.addChild(new DynamicBorder());
|
|
372
400
|
|
|
@@ -432,6 +460,10 @@ export class HookSelectorComponent extends Container {
|
|
|
432
460
|
this.#scrollableTitle?.scrollBy(this.#scrollTitleRows);
|
|
433
461
|
return;
|
|
434
462
|
}
|
|
463
|
+
if (this.#inlineEditor) {
|
|
464
|
+
this.#handleInputModeKey(keyData, this.#inlineEditor);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
435
467
|
if (matchesKey(keyData, "up") || keyData === "k") {
|
|
436
468
|
this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
|
|
437
469
|
this.#updateList();
|
|
@@ -440,7 +472,12 @@ export class HookSelectorComponent extends Container {
|
|
|
440
472
|
this.#updateList();
|
|
441
473
|
} else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
442
474
|
const selected = this.#options[this.#selectedIndex];
|
|
443
|
-
if (selected)
|
|
475
|
+
if (!selected) return;
|
|
476
|
+
if (this.#customInput && selected === this.#customInput.optionLabel) {
|
|
477
|
+
this.#enterInputMode();
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
this.#onSelectCallback(selected);
|
|
444
481
|
} else if (matchesKey(keyData, "left")) {
|
|
445
482
|
this.#onLeftCallback?.();
|
|
446
483
|
} else if (matchesKey(keyData, "right")) {
|
|
@@ -452,6 +489,73 @@ export class HookSelectorComponent extends Container {
|
|
|
452
489
|
}
|
|
453
490
|
}
|
|
454
491
|
|
|
492
|
+
/** Keys while the inline custom-input editor is open below the option list. */
|
|
493
|
+
#handleInputModeKey(keyData: string, editor: Editor): void {
|
|
494
|
+
// Escape backs out to option selection instead of cancelling the dialog,
|
|
495
|
+
// so a stray Esc never throws away the question context.
|
|
496
|
+
if (matchesKey(keyData, "escape") || matchesAppInterrupt(keyData)) {
|
|
497
|
+
this.#exitInputMode();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (matchesAppExternalEditor(keyData)) {
|
|
501
|
+
void this.#openExternalEditor(editor);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
|
|
505
|
+
this.#customInput?.onSubmit(editor.getText());
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
editor.handleInput(keyData);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
#enterInputMode(): void {
|
|
512
|
+
if (this.#inlineEditor) return;
|
|
513
|
+
// Stop the auto-select countdown for good: the user is actively typing,
|
|
514
|
+
// matching the old behavior where the separate editor had no timeout.
|
|
515
|
+
if (this.#countdown) {
|
|
516
|
+
this.#countdown.dispose();
|
|
517
|
+
this.#countdown = undefined;
|
|
518
|
+
this.#titleComponent.setText(this.#baseTitle);
|
|
519
|
+
}
|
|
520
|
+
const editor = new Editor(getEditorTheme());
|
|
521
|
+
editor.setBorderVisible(false);
|
|
522
|
+
editor.setPromptGutter("> ");
|
|
523
|
+
editor.disableSubmit = true;
|
|
524
|
+
this.#inlineEditor = editor;
|
|
525
|
+
this.#inputArea.addChild(new Spacer(1));
|
|
526
|
+
this.#inputArea.addChild(editor);
|
|
527
|
+
const scrollHint = this.#scrollTitleRows === undefined ? "" : " wheel/PgUp/PgDn scroll question";
|
|
528
|
+
this.#helpTextComponent.setText(
|
|
529
|
+
theme.fg("dim", `enter submit esc back to options ctrl+g external editor${scrollHint}`),
|
|
530
|
+
);
|
|
531
|
+
this.invalidate();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
#exitInputMode(): void {
|
|
535
|
+
if (!this.#inlineEditor) return;
|
|
536
|
+
this.#inlineEditor = undefined;
|
|
537
|
+
this.#inputArea.clear();
|
|
538
|
+
this.#helpTextComponent.setText(theme.fg("dim", this.#baseHelpText));
|
|
539
|
+
this.invalidate();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async #openExternalEditor(editor: Editor): Promise<void> {
|
|
543
|
+
const editorCmd = getEditorCommand();
|
|
544
|
+
if (!editorCmd || !this.#tui) return;
|
|
545
|
+
|
|
546
|
+
const currentText = editor.getExpandedText();
|
|
547
|
+
try {
|
|
548
|
+
this.#tui.stop();
|
|
549
|
+
const result = await openInEditor(editorCmd, currentText);
|
|
550
|
+
if (result !== null) {
|
|
551
|
+
editor.setText(result);
|
|
552
|
+
}
|
|
553
|
+
} finally {
|
|
554
|
+
this.#tui.start();
|
|
555
|
+
this.#tui.requestRender(true);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
455
559
|
dispose(): void {
|
|
456
560
|
this.#countdown?.dispose();
|
|
457
561
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
3
|
-
import {
|
|
3
|
+
import { estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
|
|
4
4
|
import { type Component, truncateToWidth, visibleWidth } from "@gajae-code/tui";
|
|
5
5
|
import { formatCount, getProjectDir } from "@gajae-code/utils";
|
|
6
6
|
import { $ } from "bun";
|
|
@@ -50,7 +50,7 @@ export interface StatusLineSettings {
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Symbol-keyed sidecar tagged onto each `AgentMessage` to memoize its
|
|
53
|
-
* `
|
|
53
|
+
* `estimateMessageTokensHeuristic` result. Keyed by message identity (the object itself);
|
|
54
54
|
* a cheap content fingerprint detects in-place mutations (post-hoc error
|
|
55
55
|
* attachment, retry-truncated branch rebuild, etc.) and forces recompute.
|
|
56
56
|
*
|
|
@@ -64,11 +64,11 @@ interface TaggedMessage {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
* Cheap structural fingerprint mirroring `
|
|
67
|
+
* Cheap structural fingerprint mirroring `estimateMessageTokensHeuristic`'s content walk.
|
|
68
68
|
* O(blocks) — only reads string `.length` and primitives, never copies or
|
|
69
69
|
* serializes content. Any in-place mutation that alters total tokenized
|
|
70
70
|
* content also alters one of the byte-length sums or block counts captured
|
|
71
|
-
* here, forcing the cached
|
|
71
|
+
* here, forcing the cached heuristic token value to be recomputed.
|
|
72
72
|
*/
|
|
73
73
|
function messageFingerprint(msg: AgentMessage): string {
|
|
74
74
|
const role = (msg as { role?: string }).role ?? "";
|
|
@@ -136,7 +136,7 @@ function tokensForMessage(msg: AgentMessage): number {
|
|
|
136
136
|
const tagged = msg as TaggedMessage;
|
|
137
137
|
const cached = tagged[kTokenCache];
|
|
138
138
|
if (cached && cached.fingerprint === fp) return cached.tokens;
|
|
139
|
-
const tokens =
|
|
139
|
+
const tokens = estimateMessageTokensHeuristic(msg);
|
|
140
140
|
tagged[kTokenCache] = { fingerprint: fp, tokens };
|
|
141
141
|
return tokens;
|
|
142
142
|
}
|
|
@@ -560,7 +560,7 @@ export class StatusLineComponent implements Component {
|
|
|
560
560
|
let messagesTokens = 0;
|
|
561
561
|
const lastIdx = messages.length - 1;
|
|
562
562
|
for (let i = 0; i < messages.length; i++) {
|
|
563
|
-
messagesTokens += i === lastIdx ?
|
|
563
|
+
messagesTokens += i === lastIdx ? estimateMessageTokensHeuristic(messages[i]) : tokensForMessage(messages[i]);
|
|
564
564
|
}
|
|
565
565
|
|
|
566
566
|
const usedTokens = this.#nonMessageTokensCache + messagesTokens;
|
|
@@ -20,6 +20,7 @@ import type { AgentSessionEvent } from "../../session/agent-session";
|
|
|
20
20
|
import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
|
|
21
21
|
import type { ResolveToolDetails } from "../../tools/resolve";
|
|
22
22
|
import { interruptHint } from "../shared";
|
|
23
|
+
import { buildAbortDisplayMessage } from "../utils/abort-message";
|
|
23
24
|
|
|
24
25
|
type AgentSessionEventKind = AgentSessionEvent["type"];
|
|
25
26
|
|
|
@@ -419,10 +420,10 @@ export class EventController {
|
|
|
419
420
|
// controller ran, so reaching this branch implies the abort was NOT a
|
|
420
421
|
// silent internal transition.
|
|
421
422
|
const retryAttempt = this.ctx.session.retryAttempt;
|
|
422
|
-
errorMessage =
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
423
|
+
errorMessage = buildAbortDisplayMessage({
|
|
424
|
+
errorMessage: this.ctx.streamingMessage.errorMessage,
|
|
425
|
+
retryAttempt,
|
|
426
|
+
});
|
|
426
427
|
this.ctx.streamingMessage.errorMessage = errorMessage;
|
|
427
428
|
}
|
|
428
429
|
if (silentlyAborted || ttsrSilenced) {
|
|
@@ -29,6 +29,7 @@ const HOOK_SELECTOR_MOUSE_REPORTING_ENABLE = "\x1b[?1006h\x1b[?1000h";
|
|
|
29
29
|
const HOOK_SELECTOR_MOUSE_REPORTING_DISABLE = "\x1b[?1000l\x1b[?1006l";
|
|
30
30
|
const HOOK_SELECTOR_CHROME_ROWS = 7;
|
|
31
31
|
const HOOK_SELECTOR_OUTLINE_ROWS = 2;
|
|
32
|
+
const HOOK_SELECTOR_INLINE_INPUT_ROWS = 2;
|
|
32
33
|
|
|
33
34
|
export class ExtensionUiController {
|
|
34
35
|
#extensionTerminalInputUnsubscribers = new Set<() => void>();
|
|
@@ -601,8 +602,11 @@ export class ExtensionUiController {
|
|
|
601
602
|
const maxVisible =
|
|
602
603
|
requestedTitleRows === undefined ? baseMaxVisible : Math.min(15, Math.max(3, scrollOptionRows + 1));
|
|
603
604
|
const listChromeRows = dialogOptions?.outline === true ? HOOK_SELECTOR_OUTLINE_ROWS : 0;
|
|
605
|
+
// Reserve rows for the inline custom-input editor so opening it doesn't
|
|
606
|
+
// push the scrollable title past the viewport into terminal scrollback.
|
|
607
|
+
const inlineInputRows = dialogOptions?.customInput ? HOOK_SELECTOR_INLINE_INPUT_ROWS : 0;
|
|
604
608
|
const availableTitleRows =
|
|
605
|
-
this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - HOOK_SELECTOR_CHROME_ROWS;
|
|
609
|
+
this.ctx.ui.terminal.rows - scrollOptionRows - listChromeRows - inlineInputRows - HOOK_SELECTOR_CHROME_ROWS;
|
|
606
610
|
const scrollTitleRows =
|
|
607
611
|
requestedTitleRows === undefined ? undefined : Math.max(1, Math.min(requestedTitleRows, availableTitleRows));
|
|
608
612
|
if (scrollTitleRows !== undefined) {
|
|
@@ -645,6 +649,17 @@ export class ExtensionUiController {
|
|
|
645
649
|
wrapFocused: dialogOptions?.wrapFocused,
|
|
646
650
|
scrollTitleRows,
|
|
647
651
|
maxVisible,
|
|
652
|
+
customInput: dialogOptions?.customInput
|
|
653
|
+
? {
|
|
654
|
+
optionLabel: dialogOptions.customInput.optionLabel,
|
|
655
|
+
onSubmit: text => {
|
|
656
|
+
const optionLabel = dialogOptions.customInput?.optionLabel;
|
|
657
|
+
this.hideHookSelector();
|
|
658
|
+
dialogOptions.customInput?.onSubmit(text);
|
|
659
|
+
finish(optionLabel);
|
|
660
|
+
},
|
|
661
|
+
}
|
|
662
|
+
: undefined,
|
|
648
663
|
},
|
|
649
664
|
);
|
|
650
665
|
this.ctx.editorContainer.clear();
|
|
@@ -1057,7 +1057,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1057
1057
|
return;
|
|
1058
1058
|
}
|
|
1059
1059
|
if (event.state?.enabled === true && !this.#goalModePreviousTools) {
|
|
1060
|
-
this.#goalModePreviousTools = this.session.getActiveToolNames()
|
|
1060
|
+
this.#goalModePreviousTools = this.session.getActiveToolNames();
|
|
1061
1061
|
}
|
|
1062
1062
|
this.goalModeEnabled = event.state?.enabled === true;
|
|
1063
1063
|
this.goalModePaused = event.state?.enabled !== true && event.state?.goal?.status === "paused";
|
|
@@ -1146,10 +1146,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1146
1146
|
const restored = await this.session.goalRuntime.onThreadResumed();
|
|
1147
1147
|
this.goalModeEnabled = restored?.enabled === true;
|
|
1148
1148
|
this.goalModePaused = restored?.enabled !== true && restored?.goal.status === "paused";
|
|
1149
|
-
//
|
|
1150
|
-
// Re-add it now so the agent can call resume, complete, or drop on this goal.
|
|
1149
|
+
// Keep `goal` armed on resumed threads; it is part of the default active tool set.
|
|
1151
1150
|
if (restored?.goal) {
|
|
1152
|
-
const previousTools = this.session.getActiveToolNames()
|
|
1151
|
+
const previousTools = this.session.getActiveToolNames();
|
|
1153
1152
|
this.#goalModePreviousTools = previousTools;
|
|
1154
1153
|
await this.session.setActiveToolsByName([...new Set([...previousTools, "goal"])]);
|
|
1155
1154
|
}
|
|
@@ -1318,7 +1317,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1318
1317
|
this.showWarning("Exit plan mode first.");
|
|
1319
1318
|
return;
|
|
1320
1319
|
}
|
|
1321
|
-
const previousTools = this.session.getActiveToolNames()
|
|
1320
|
+
const previousTools = this.session.getActiveToolNames();
|
|
1322
1321
|
const goalTools = [...new Set([...previousTools, "goal"])];
|
|
1323
1322
|
this.#goalModePreviousTools = previousTools;
|
|
1324
1323
|
this.goalModePaused = false;
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -72,7 +72,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
72
72
|
// In text mode, output final response
|
|
73
73
|
if (mode === "text") {
|
|
74
74
|
const state = session.state;
|
|
75
|
-
const lastMessage = state.messages
|
|
75
|
+
const lastMessage = state.messages.findLast(message => message.role === "assistant");
|
|
76
76
|
|
|
77
77
|
if (lastMessage?.role === "assistant") {
|
|
78
78
|
const assistantMsg = lastMessage as AssistantMessage;
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -264,7 +264,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
264
264
|
"icon.context": "◫",
|
|
265
265
|
"icon.cost": "💲",
|
|
266
266
|
"icon.time": "⏱",
|
|
267
|
-
"icon.pi": "
|
|
267
|
+
"icon.pi": "🦞",
|
|
268
268
|
"icon.agents": "👥",
|
|
269
269
|
"icon.cache": "💾",
|
|
270
270
|
"icon.input": "⤵",
|
|
@@ -686,7 +686,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
686
686
|
"icon.context": "ctx:",
|
|
687
687
|
"icon.cost": "$",
|
|
688
688
|
"icon.time": "t:",
|
|
689
|
-
"icon.pi": "
|
|
689
|
+
"icon.pi": "GJC",
|
|
690
690
|
"icon.agents": "AG",
|
|
691
691
|
"icon.cache": "cache",
|
|
692
692
|
"icon.input": "in:",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const STREAM_IDLE_TIMEOUT_PATTERN = /\bstream stalled while waiting for the next event\b/i;
|
|
2
|
+
const GENERIC_ABORT_PATTERN = /^Request was aborted\.?$/i;
|
|
3
|
+
const ABORT_DISPLAY_LABEL_PATTERN = /^(?:Operation aborted|Aborted after \d+ retry attempts?)(?::|$)/;
|
|
4
|
+
|
|
5
|
+
export function buildAbortDisplayMessage({
|
|
6
|
+
errorMessage,
|
|
7
|
+
retryAttempt,
|
|
8
|
+
}: {
|
|
9
|
+
errorMessage?: string;
|
|
10
|
+
retryAttempt: number;
|
|
11
|
+
}): string {
|
|
12
|
+
const existingDisplayMessage = normalizeExistingAbortDisplayMessage(errorMessage);
|
|
13
|
+
if (existingDisplayMessage) return existingDisplayMessage;
|
|
14
|
+
|
|
15
|
+
const baseMessage =
|
|
16
|
+
retryAttempt > 0
|
|
17
|
+
? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
|
|
18
|
+
: "Operation aborted";
|
|
19
|
+
const cause = normalizeAbortCause(errorMessage);
|
|
20
|
+
if (!cause) return baseMessage;
|
|
21
|
+
|
|
22
|
+
return `${baseMessage}: ${cause}${streamIdleTimeoutHint(cause)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeExistingAbortDisplayMessage(errorMessage: string | undefined): string {
|
|
26
|
+
const trimmed = errorMessage?.trim();
|
|
27
|
+
if (!trimmed || !ABORT_DISPLAY_LABEL_PATTERN.test(trimmed)) return "";
|
|
28
|
+
return trimmed;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeAbortCause(errorMessage: string | undefined): string {
|
|
32
|
+
const trimmed = errorMessage?.trim();
|
|
33
|
+
if (!trimmed || GENERIC_ABORT_PATTERN.test(trimmed)) return "";
|
|
34
|
+
return trimmed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function streamIdleTimeoutHint(cause: string): string {
|
|
38
|
+
if (!STREAM_IDLE_TIMEOUT_PATTERN.test(cause)) return "";
|
|
39
|
+
const separator = /[.!?]$/.test(cause) ? " " : ". ";
|
|
40
|
+
return `${separator}Hint: set PI_STREAM_IDLE_TIMEOUT_MS=300000 for slow reasoning/proxy streams, or PI_STREAM_IDLE_TIMEOUT_MS=0 to disable the watchdog.`;
|
|
41
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
2
2
|
import type { CompactionSettings } from "@gajae-code/agent-core/compaction";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
effectiveReserveTokens,
|
|
5
|
+
estimateMessageTokensHeuristic,
|
|
6
|
+
estimateTextTokensHeuristic,
|
|
7
|
+
resolveThresholdTokens,
|
|
8
|
+
} from "@gajae-code/agent-core/compaction";
|
|
4
9
|
import type { Model } from "@gajae-code/ai";
|
|
5
|
-
import { countTokens } from "@gajae-code/natives";
|
|
6
10
|
import { formatNumber } from "@gajae-code/utils";
|
|
7
11
|
import type { Skill } from "../../extensibility/skills";
|
|
8
12
|
import type { AgentSession } from "../../session/agent-session";
|
|
@@ -46,7 +50,7 @@ export function estimateSkillsTokens(skills: readonly Skill[]): number {
|
|
|
46
50
|
// concatenated form, so encode each piece separately and sum.
|
|
47
51
|
fragments.push(skill.name, skill.description);
|
|
48
52
|
}
|
|
49
|
-
return
|
|
53
|
+
return estimateTextTokensHeuristic(fragments);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
export function estimateToolSchemaTokens(
|
|
@@ -61,7 +65,7 @@ export function estimateToolSchemaTokens(
|
|
|
61
65
|
// Schema may contain functions or cycles; ignore.
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
|
-
return
|
|
68
|
+
return estimateTextTokensHeuristic(fragments);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
/**
|
|
@@ -100,8 +104,11 @@ function computeNonMessageBreakdown(session: AgentSession): {
|
|
|
100
104
|
const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? []);
|
|
101
105
|
const systemPromptParts = session.systemPrompt ?? [];
|
|
102
106
|
const rulesTokens = estimateRulesTokens(systemPromptParts);
|
|
103
|
-
const systemContextTokens =
|
|
104
|
-
const systemPromptTokens = Math.max(
|
|
107
|
+
const systemContextTokens = estimateTextTokensHeuristic(systemPromptParts.slice(1));
|
|
108
|
+
const systemPromptTokens = Math.max(
|
|
109
|
+
0,
|
|
110
|
+
estimateTextTokensHeuristic(systemPromptParts[0] ?? "") - skillsTokens - rulesTokens,
|
|
111
|
+
);
|
|
105
112
|
return { rulesTokens, skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens };
|
|
106
113
|
}
|
|
107
114
|
|
|
@@ -112,7 +119,7 @@ function estimateRulesTokens(systemPromptParts: readonly string[]): number {
|
|
|
112
119
|
fragments.push(match[0]);
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
|
-
return fragments.length === 0 ? 0 :
|
|
122
|
+
return fragments.length === 0 ? 0 : estimateTextTokensHeuristic(fragments);
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
function splitLastUserTurn(messages: readonly AgentMessage[]): {
|
|
@@ -130,7 +137,7 @@ function splitLastUserTurn(messages: readonly AgentMessage[]): {
|
|
|
130
137
|
let regularMessagesTokens = 0;
|
|
131
138
|
let lastUserTurnTokens = 0;
|
|
132
139
|
for (let i = 0; i < messages.length; i++) {
|
|
133
|
-
const tokens =
|
|
140
|
+
const tokens = estimateMessageTokensHeuristic(messages[i]);
|
|
134
141
|
if (i === lastUserIndex) {
|
|
135
142
|
lastUserTurnTokens = tokens;
|
|
136
143
|
} else {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "../../session/messages";
|
|
28
28
|
import type { SessionContext } from "../../session/session-manager";
|
|
29
29
|
import { formatBytes, formatDuration } from "../../tools/render-utils";
|
|
30
|
+
import { buildAbortDisplayMessage } from "./abort-message";
|
|
30
31
|
|
|
31
32
|
type TextBlock = { type: "text"; text: string };
|
|
32
33
|
interface RenderInitialMessagesOptions {
|
|
@@ -319,12 +320,10 @@ export class UiHelpers {
|
|
|
319
320
|
!isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
|
|
320
321
|
const errorMessage = hasErrorStop
|
|
321
322
|
? message.stopReason === "aborted"
|
|
322
|
-
? (
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
: "Operation aborted";
|
|
327
|
-
})()
|
|
323
|
+
? buildAbortDisplayMessage({
|
|
324
|
+
errorMessage: message.errorMessage,
|
|
325
|
+
retryAttempt: this.ctx.session.retryAttempt,
|
|
326
|
+
})
|
|
328
327
|
: message.errorMessage || "Error"
|
|
329
328
|
: null;
|
|
330
329
|
|
|
@@ -83,4 +83,10 @@ Prioritized concrete actions.
|
|
|
83
83
|
|
|
84
84
|
## Trade-offs
|
|
85
85
|
Table or bullets comparing viable options when relevant.
|
|
86
|
+
|
|
87
|
+
Persist this full review as the durable artifact via the restricted bash CLI, passing the markdown inline (never a file path, never `/tmp`):
|
|
88
|
+
|
|
89
|
+
gjc ralplan --write --stage architect --stage_n <N> --artifact "<full review markdown>" --json
|
|
90
|
+
|
|
91
|
+
Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus the compact verdict (Architectural Status + Code Review Recommendation). Never paste the full review body back into your response — the caller reads the persisted artifact when it needs the full text.
|
|
86
92
|
</output_contract>
|
|
@@ -56,4 +56,10 @@ Review plan clarity, completeness, verification, big-picture fit, referenced fil
|
|
|
56
56
|
- Risk/Verification Rigor
|
|
57
57
|
|
|
58
58
|
If not OKAY, list concrete required fixes.
|
|
59
|
+
|
|
60
|
+
Persist this full evaluation as the durable artifact via the restricted bash CLI, passing the markdown inline (never a file path, never `/tmp`):
|
|
61
|
+
|
|
62
|
+
gjc ralplan --write --stage critic --stage_n <N> --artifact "<full evaluation markdown>" --json
|
|
63
|
+
|
|
64
|
+
Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus the compact verdict (OKAY / ITERATE / REJECT). Never paste the full evaluation body back into your response — the caller reads the persisted artifact when it needs the full text.
|
|
59
65
|
</output_contract>
|
|
@@ -18,6 +18,7 @@ Leave execution with a right-sized, evidence-grounded plan: scope, steps, accept
|
|
|
18
18
|
<constraints>
|
|
19
19
|
- Read-only: never write, edit, format, commit, push, or mutate files.
|
|
20
20
|
- Exception: you may use the restricted `bash` tool only for sanctioned GJC workflow CLI persistence (`gjc ralplan --write ...`) and GJC workflow state read/write/contract commands (`gjc state ...`). For `gjc ralplan --write`, pass the plan markdown inline in `--artifact`, not as a file path. Do not use bash for product-source writes, direct handoffs, state clears, or general shell work.
|
|
21
|
+
- Persist durable plans only through `gjc ralplan --write`. Never write plan files to `/tmp`, the repository, or any other path, and never rely on a file the caller must read back. The CLI is your only persistence channel.
|
|
21
22
|
- Inspect the repository before asking about code facts.
|
|
22
23
|
- Ask only about priorities, tradeoffs, scope decisions, timelines, or preferences that repository inspection cannot resolve.
|
|
23
24
|
- Right-size the step count to the task; do not default to a fixed number of steps.
|
|
@@ -42,7 +43,7 @@ Leave execution with a right-sized, evidence-grounded plan: scope, steps, accept
|
|
|
42
43
|
</success_criteria>
|
|
43
44
|
|
|
44
45
|
<output_contract>
|
|
45
|
-
|
|
46
|
+
Build the full plan as a single markdown document containing:
|
|
46
47
|
- Summary
|
|
47
48
|
- In scope / out of scope
|
|
48
49
|
- File-level changes
|
|
@@ -50,4 +51,10 @@ Return:
|
|
|
50
51
|
- Acceptance criteria
|
|
51
52
|
- Verification
|
|
52
53
|
- Risks and mitigations
|
|
54
|
+
|
|
55
|
+
Persist that markdown as the durable artifact via the restricted bash CLI, passing the plan inline (never a file path, never `/tmp`):
|
|
56
|
+
|
|
57
|
+
gjc ralplan --write --stage planner --stage_n <N> --artifact "<full plan markdown>" --json
|
|
58
|
+
|
|
59
|
+
Then return to the caller ONLY the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) plus a compact plan summary (<=10 lines). Never paste the full plan body back into your response — the caller reads the persisted artifact when it needs the full text.
|
|
53
60
|
</output_contract>
|
package/src/sdk.ts
CHANGED
|
@@ -1622,9 +1622,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1622
1622
|
};
|
|
1623
1623
|
|
|
1624
1624
|
const toolNamesFromRegistry = Array.from(toolRegistry.keys());
|
|
1625
|
-
const requestedToolNames =
|
|
1626
|
-
|
|
1627
|
-
|
|
1625
|
+
const requestedToolNames = options.toolNames
|
|
1626
|
+
? [
|
|
1627
|
+
...new Set([
|
|
1628
|
+
...options.toolNames.map(name => name.toLowerCase()),
|
|
1629
|
+
...(settings.get("goal.enabled") ? ["goal"] : []),
|
|
1630
|
+
]),
|
|
1631
|
+
]
|
|
1632
|
+
: toolNamesFromRegistry;
|
|
1628
1633
|
const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
|
|
1629
1634
|
const requestedToolNameSet = new Set(normalizedRequested);
|
|
1630
1635
|
// Effective discovery mode only covers built-in tools; MCP tool discovery
|
|
@@ -1635,7 +1640,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1635
1640
|
const defaultInactiveToolNames = new Set(
|
|
1636
1641
|
registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
|
|
1637
1642
|
);
|
|
1638
|
-
const requestedActiveToolNames = normalizedRequested
|
|
1643
|
+
const requestedActiveToolNames = normalizedRequested;
|
|
1639
1644
|
const initialRequestedActiveToolNames = options.toolNames
|
|
1640
1645
|
? requestedActiveToolNames
|
|
1641
1646
|
: requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
|