@gajae-code/coding-agent 0.4.4 → 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 +40 -0
- package/dist/types/cli/fast-help.d.ts +1 -0
- package/dist/types/cli/setup-cli.d.ts +2 -0
- package/dist/types/commands/harness.d.ts +3 -0
- package/dist/types/commands/setup.d.ts +6 -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 +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- 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/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 +7 -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/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +10 -0
- package/package.json +8 -7
- package/scripts/build-binary.ts +4 -0
- package/src/cli/fast-help.ts +80 -0
- package/src/cli/setup-cli.ts +12 -3
- package/src/cli.ts +107 -16
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +92 -9
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/setup.ts +4 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +385 -182
- package/src/cursor.ts +30 -2
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -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 -5
- package/src/main.ts +7 -3
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/controllers/event-controller.ts +5 -4
- 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/sdk.ts +9 -4
- package/src/session/agent-session.ts +16 -5
- package/src/session/session-manager.ts +20 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +2 -0
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +7 -0
- package/src/tools/index.ts +2 -2
- package/src/tools/subagent-render.ts +10 -1
- 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,
|
|
@@ -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) {
|
|
@@ -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
|
|
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));
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
calculatePromptTokens,
|
|
42
42
|
collectEntriesForBranchSummary,
|
|
43
43
|
compact,
|
|
44
|
+
estimateMessageTokensHeuristic,
|
|
44
45
|
estimateTokens,
|
|
45
46
|
generateBranchSummary,
|
|
46
47
|
generateHandoff,
|
|
@@ -4388,7 +4389,7 @@ export class AgentSession {
|
|
|
4388
4389
|
return false;
|
|
4389
4390
|
}
|
|
4390
4391
|
|
|
4391
|
-
const previousTools = this.getActiveToolNames()
|
|
4392
|
+
const previousTools = this.getActiveToolNames();
|
|
4392
4393
|
const goalTools = [...new Set([...previousTools, "goal"])];
|
|
4393
4394
|
await this.#goalRuntime.createGoal({ objective: pendingGoal.objective });
|
|
4394
4395
|
await this.setActiveToolsByName(goalTools);
|
|
@@ -6063,6 +6064,9 @@ export class AgentSession {
|
|
|
6063
6064
|
return undefined;
|
|
6064
6065
|
}
|
|
6065
6066
|
|
|
6067
|
+
// getBranch() returns materialized copies for blob-externalized entries, so
|
|
6068
|
+
// the pruning mutations must be written back into the canonical store.
|
|
6069
|
+
this.sessionManager.applyEntryMessageUpdates(result.prunedEntries);
|
|
6066
6070
|
await this.sessionManager.rewriteEntries();
|
|
6067
6071
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6068
6072
|
this.agent.replaceMessages(sessionContext.messages);
|
|
@@ -6507,12 +6511,18 @@ export class AgentSession {
|
|
|
6507
6511
|
// Case 2: Threshold - turn succeeded but context is getting large
|
|
6508
6512
|
// Skip if this was an error (non-overflow errors don't have usage data)
|
|
6509
6513
|
if (assistantMessage.stopReason === "error") return;
|
|
6510
|
-
const pruneResult = await this.#pruneToolOutputs();
|
|
6511
6514
|
let contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
6515
|
+
const maxOutputTokens = this.model?.maxTokens ?? 0;
|
|
6516
|
+
// Cache-epoch invariant: pruning rewrites already-sent toolResult history,
|
|
6517
|
+
// which breaks the provider prompt-cache prefix mid-epoch. Only prune at a
|
|
6518
|
+
// sanctioned maintenance boundary, i.e. when the un-pruned context already
|
|
6519
|
+
// crosses the compaction threshold. Pruning may then avert full compaction.
|
|
6520
|
+
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
|
|
6521
|
+
const pruneResult = await this.#pruneToolOutputs();
|
|
6512
6522
|
if (pruneResult) {
|
|
6513
6523
|
contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
6514
6524
|
}
|
|
6515
|
-
if (shouldCompact(contextTokens, contextWindow, compactionSettings,
|
|
6525
|
+
if (shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) {
|
|
6516
6526
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
6517
6527
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
6518
6528
|
if (!promoted) {
|
|
@@ -8338,6 +8348,7 @@ export class AgentSession {
|
|
|
8338
8348
|
onChunk,
|
|
8339
8349
|
signal: abortController.signal,
|
|
8340
8350
|
sessionKey: this.sessionId,
|
|
8351
|
+
cwd,
|
|
8341
8352
|
timeout: clampTimeout("bash") * 1000,
|
|
8342
8353
|
env: buildGjcRuntimeSessionEnv({
|
|
8343
8354
|
sessionFile: this.sessionManager.getSessionFile(),
|
|
@@ -9527,7 +9538,7 @@ export class AgentSession {
|
|
|
9527
9538
|
// No usage data - estimate all messages
|
|
9528
9539
|
let estimated = 0;
|
|
9529
9540
|
for (const message of messages) {
|
|
9530
|
-
estimated +=
|
|
9541
|
+
estimated += estimateMessageTokensHeuristic(message);
|
|
9531
9542
|
}
|
|
9532
9543
|
return {
|
|
9533
9544
|
tokens: estimated,
|
|
@@ -9537,7 +9548,7 @@ export class AgentSession {
|
|
|
9537
9548
|
const usageTokens = calculatePromptTokens(lastUsage);
|
|
9538
9549
|
let trailingTokens = 0;
|
|
9539
9550
|
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
9540
|
-
trailingTokens +=
|
|
9551
|
+
trailingTokens += estimateMessageTokensHeuristic(messages[i]);
|
|
9541
9552
|
}
|
|
9542
9553
|
|
|
9543
9554
|
return {
|
|
@@ -3125,6 +3125,26 @@ export class SessionManager {
|
|
|
3125
3125
|
return entry.id;
|
|
3126
3126
|
}
|
|
3127
3127
|
|
|
3128
|
+
/**
|
|
3129
|
+
* Write mutated message entries back into the canonical entry store by id.
|
|
3130
|
+
*
|
|
3131
|
+
* `getBranch()` materializes resident-blob entries into copies, so in-place
|
|
3132
|
+
* mutation of returned entries (e.g. pruning tool outputs) does not affect
|
|
3133
|
+
* the canonical store. This applies such mutations for real.
|
|
3134
|
+
*/
|
|
3135
|
+
applyEntryMessageUpdates(entries: readonly SessionMessageEntry[]): void {
|
|
3136
|
+
for (const updated of entries) {
|
|
3137
|
+
const canonical = this.#byId.get(updated.id);
|
|
3138
|
+
if (canonical?.type !== "message") continue;
|
|
3139
|
+
const residentEntry = prepareEntryForResidentSync(
|
|
3140
|
+
{ ...canonical, message: updated.message },
|
|
3141
|
+
this.#residentBlobStore,
|
|
3142
|
+
) as SessionMessageEntry;
|
|
3143
|
+
canonical.message = residentEntry.message;
|
|
3144
|
+
}
|
|
3145
|
+
this.#needsFullRewriteOnNextPersist = true;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3128
3148
|
/**
|
|
3129
3149
|
* Rewrite the session file after in-place entry updates.
|
|
3130
3150
|
* Use sparingly (e.g., pruning old tool outputs).
|
|
@@ -10,15 +10,16 @@ These instructions teach a Hermes-style coordinator how to operate GJC through t
|
|
|
10
10
|
2. Send exactly one bounded task prompt with `{{TOOL_PREFIX}}_send_prompt`.
|
|
11
11
|
3. Store the returned `turn_id`.
|
|
12
12
|
4. Poll `{{TOOL_PREFIX}}_read_turn` or `{{TOOL_PREFIX}}_await_turn` for that `turn_id` until the turn is terminal.
|
|
13
|
+
If a second task is needed while one turn is active, pass `queue: true`; the next queued turn is promoted after the active turn is reported terminal.
|
|
13
14
|
5. If GJC asks a structured question, use `{{TOOL_PREFIX}}_list_questions` and answer with `{{TOOL_PREFIX}}_submit_question_answer`.
|
|
14
15
|
6. Use `{{TOOL_PREFIX}}_report_status` for coordinator-visible status and final reports.
|
|
15
16
|
7. Use `{{TOOL_PREFIX}}_read_tail` only as advisory debug output when structured turn state is insufficient.
|
|
16
17
|
|
|
17
18
|
Do not report completion to the user until the GJC turn is terminal. Do not infer completion from terminal scrollback alone.
|
|
18
19
|
|
|
19
|
-
##
|
|
20
|
+
## Worktree, model, and provider policy
|
|
20
21
|
|
|
21
|
-
The Hermes bridge does not choose a model/provider.
|
|
22
|
+
The Hermes bridge does not choose a model/provider. Generated setup configures `GJC_COORDINATOR_MCP_SESSION_COMMAND` to `gjc --worktree` by default, so GJC creates and tracks the worktree while still using normal local model/provider resolution. Keep worktree creation inside GJC rather than creating unmanaged Hermes-side git worktrees; this preserves the original project identity for session listing and resume. If the operator config supplies a different `GJC_COORDINATOR_MCP_SESSION_COMMAND`, preserve it as explicit user intent.
|
|
22
23
|
|
|
23
24
|
Provider-specific commands are examples only, never product defaults.
|
|
24
25
|
|
|
@@ -24,6 +24,8 @@ export interface HermesSetupFlags {
|
|
|
24
24
|
repo?: string;
|
|
25
25
|
profile?: string;
|
|
26
26
|
sessionCommand?: string;
|
|
27
|
+
noWorktree?: boolean;
|
|
28
|
+
worktreeName?: string;
|
|
27
29
|
stateRoot?: string;
|
|
28
30
|
mutation?: string[];
|
|
29
31
|
artifactByteCap?: string;
|
|
@@ -47,6 +49,11 @@ export interface CoordinatorSetupSpec {
|
|
|
47
49
|
repo?: string;
|
|
48
50
|
};
|
|
49
51
|
sessionCommand?: string;
|
|
52
|
+
sessionCommandSource: "default" | "explicit";
|
|
53
|
+
worktree: {
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
name?: string;
|
|
56
|
+
};
|
|
50
57
|
stateRoot?: string;
|
|
51
58
|
mutationPolicy: {
|
|
52
59
|
classes: HermesMutationClass[];
|
|
@@ -157,6 +164,39 @@ function parseByteCap(value: string | undefined): number | undefined {
|
|
|
157
164
|
return parsed;
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
function normalizeWorktreeName(value: string | undefined): string | undefined {
|
|
168
|
+
const trimmed = optionalTrim(value);
|
|
169
|
+
if (!trimmed) return undefined;
|
|
170
|
+
if (trimmed.startsWith("-") || !/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(trimmed)) {
|
|
171
|
+
throw new HermesSetupError(`Invalid Hermes worktree name: ${trimmed}`, 2);
|
|
172
|
+
}
|
|
173
|
+
return trimmed;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolveHermesWorktree(flags: HermesSetupFlags): CoordinatorSetupSpec["worktree"] {
|
|
177
|
+
if (flags.noWorktree && flags.worktreeName) {
|
|
178
|
+
throw new HermesSetupError("Use either --no-worktree or --worktree-name, not both.", 2);
|
|
179
|
+
}
|
|
180
|
+
const name = normalizeWorktreeName(flags.worktreeName);
|
|
181
|
+
return flags.noWorktree ? { enabled: false } : { enabled: true, ...(name ? { name } : {}) };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function resolveHermesSessionCommand(gjcCommand: string, flags: HermesSetupFlags): string {
|
|
185
|
+
const explicit = optionalTrim(flags.sessionCommand);
|
|
186
|
+
if (explicit) {
|
|
187
|
+
if (flags.noWorktree || flags.worktreeName) {
|
|
188
|
+
throw new HermesSetupError(
|
|
189
|
+
"Use either --session-command or Hermes worktree flags; explicit session commands are preserved exactly.",
|
|
190
|
+
2,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return explicit;
|
|
194
|
+
}
|
|
195
|
+
const worktree = resolveHermesWorktree(flags);
|
|
196
|
+
if (!worktree.enabled) return gjcCommand;
|
|
197
|
+
return worktree.name ? `${gjcCommand} --worktree ${worktree.name}` : `${gjcCommand} --worktree`;
|
|
198
|
+
}
|
|
199
|
+
|
|
160
200
|
function normalizeInstallTarget(flags: HermesSetupFlags): CoordinatorSetupSpec["installTarget"] {
|
|
161
201
|
if (flags.target && flags.profileDir) {
|
|
162
202
|
throw new HermesSetupError("Use exactly one of --target or --profile-dir for Hermes setup install targets.", 2);
|
|
@@ -169,20 +209,24 @@ function normalizeInstallTarget(flags: HermesSetupFlags): CoordinatorSetupSpec["
|
|
|
169
209
|
|
|
170
210
|
export function buildHermesSetupSpec(flags: HermesSetupFlags): CoordinatorSetupSpec {
|
|
171
211
|
const roots = normalizeRoots(flags.root);
|
|
212
|
+
const gjcCommand = optionalTrim(flags.gjcCommand) ?? DEFAULT_GJC_COMMAND;
|
|
213
|
+
const sessionCommand = resolveHermesSessionCommand(gjcCommand, flags);
|
|
172
214
|
return {
|
|
173
215
|
schemaVersion: 1,
|
|
174
216
|
coordinator: "hermes",
|
|
175
217
|
serverKey: optionalTrim(flags.serverKey) ?? DEFAULT_SERVER_KEY,
|
|
176
218
|
serverName: COORDINATOR_MCP_SERVER_NAME,
|
|
177
219
|
protocolVersion: COORDINATOR_MCP_PROTOCOL_VERSION,
|
|
178
|
-
gjcCommand
|
|
220
|
+
gjcCommand,
|
|
179
221
|
args: ["mcp-serve", "coordinator"],
|
|
180
222
|
roots,
|
|
181
223
|
namespace: {
|
|
182
224
|
...(optionalTrim(flags.profile) ? { profile: optionalTrim(flags.profile) } : {}),
|
|
183
225
|
...(optionalTrim(flags.repo) ? { repo: optionalTrim(flags.repo) } : {}),
|
|
184
226
|
},
|
|
185
|
-
|
|
227
|
+
worktree: resolveHermesWorktree(flags),
|
|
228
|
+
sessionCommandSource: optionalTrim(flags.sessionCommand) ? "explicit" : "default",
|
|
229
|
+
sessionCommand,
|
|
186
230
|
...(optionalTrim(flags.stateRoot) ? { stateRoot: path.resolve(flags.stateRoot!) } : {}),
|
|
187
231
|
mutationPolicy: {
|
|
188
232
|
classes: parseMutationClasses(flags.mutation),
|
|
@@ -214,6 +258,8 @@ function signaturePayload(spec: CoordinatorSetupSpec): Record<string, unknown> {
|
|
|
214
258
|
contractDocVersion: spec.contractDocVersion,
|
|
215
259
|
coordinator: spec.coordinator,
|
|
216
260
|
mutationClasses: spec.mutationPolicy.classes,
|
|
261
|
+
worktree: spec.worktree,
|
|
262
|
+
sessionCommandSource: spec.sessionCommandSource,
|
|
217
263
|
namespace: spec.namespace,
|
|
218
264
|
operatorTemplateVersion: spec.operatorTemplateVersion,
|
|
219
265
|
roots: spec.roots,
|
|
@@ -360,7 +406,9 @@ async function runSmoke(spec: CoordinatorSetupSpec): Promise<HermesSetupResult["
|
|
|
360
406
|
const requiredTools = [...COORDINATOR_MCP_TOOL_NAMES];
|
|
361
407
|
const server = createCoordinatorMcpServer({ env: {} });
|
|
362
408
|
const listed = await server.handleJsonRpc({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} });
|
|
363
|
-
const
|
|
409
|
+
const listedResult = isRecord(listed.result) ? listed.result : {};
|
|
410
|
+
const tools = Array.isArray(listedResult.tools) ? listedResult.tools : [];
|
|
411
|
+
const advertised = new Set(tools.map(tool => (isRecord(tool) ? String(tool.name) : "")));
|
|
364
412
|
const missingTools = requiredTools.filter(tool => !advertised.has(tool));
|
|
365
413
|
return {
|
|
366
414
|
ok: missingTools.length === 0,
|
|
@@ -398,11 +446,18 @@ export async function runHermesSetup(flags: HermesSetupFlags): Promise<HermesSet
|
|
|
398
446
|
mode,
|
|
399
447
|
files_written,
|
|
400
448
|
previews,
|
|
401
|
-
warnings:
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
449
|
+
warnings:
|
|
450
|
+
spec.sessionCommandSource === "explicit"
|
|
451
|
+
? [
|
|
452
|
+
"Using explicit GJC_COORDINATOR_MCP_SESSION_COMMAND exactly as supplied; provider/model/worktree validation is not performed.",
|
|
453
|
+
]
|
|
454
|
+
: spec.worktree.enabled
|
|
455
|
+
? [
|
|
456
|
+
`GJC_COORDINATOR_MCP_SESSION_COMMAND defaults to '${spec.sessionCommand}' so GJC owns worktree creation and resume identity.`,
|
|
457
|
+
]
|
|
458
|
+
: [
|
|
459
|
+
"GJC_COORDINATOR_MCP_SESSION_COMMAND defaults to the configured gjc command with worktree isolation disabled by user request.",
|
|
460
|
+
],
|
|
406
461
|
smoke,
|
|
407
462
|
};
|
|
408
463
|
}
|