@gajae-code/coding-agent 0.4.4 → 0.5.0
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 +83 -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 +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +35 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- package/dist/types/harness-control-plane/owner.d.ts +1 -1
- 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/receipt-spool.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/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/types.d.ts +13 -1
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/blob-store.d.ts +20 -1
- package/dist/types/session/session-manager.d.ts +32 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -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 +2 -0
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +17 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/utils/tool-choice.d.ts +14 -1
- 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 +112 -17
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +128 -11
- package/src/commands/launch.ts +2 -2
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/session.ts +3 -1
- package/src/commands/setup.ts +4 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +255 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +427 -193
- package/src/cursor.ts +46 -4
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/export/html/index.ts +13 -9
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -0
- package/src/gjc-runtime/team-runtime.ts +33 -7
- package/src/gjc-runtime/tmux-common.ts +15 -0
- package/src/gjc-runtime/tmux-sessions.ts +19 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +87 -28
- 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/receipt-spool.ts +128 -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/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +23 -0
- package/src/harness-control-plane/types.ts +33 -1
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/main.ts +7 -3
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/model-selector.ts +353 -181
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/controllers/selector-controller.ts +33 -42
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +44 -14
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- 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 +38 -6
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +121 -25
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +328 -57
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/executor.ts +69 -6
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +7 -0
- package/src/task/render.ts +21 -1
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +15 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +4 -2
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +10 -1
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/title-generator.ts +16 -2
- package/src/utils/tool-choice.ts +45 -16
|
@@ -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;
|
|
@@ -152,6 +152,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
152
152
|
isError?: boolean;
|
|
153
153
|
details?: any;
|
|
154
154
|
};
|
|
155
|
+
#textOutputCache?: { content: unknown; showImages: boolean; terminalImageProtocol: unknown; output: string };
|
|
155
156
|
// Edit preview state
|
|
156
157
|
#editMode?: EditMode;
|
|
157
158
|
#editDiffPreview?: PerFileDiffPreview[];
|
|
@@ -197,9 +198,11 @@ export class ToolExecutionComponent extends Container {
|
|
|
197
198
|
|
|
198
199
|
this.addChild(new Spacer(1));
|
|
199
200
|
|
|
200
|
-
// Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
// Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
|
|
202
|
+
// Vertical padding is 0: block separation comes solely from the leading Spacer
|
|
203
|
+
// (1 blank line above each block), matching reference TUIs (083.2).
|
|
204
|
+
this.#contentBox = new Box(1, 0, (text: string) => theme.bg("toolPendingBg", text));
|
|
205
|
+
this.#contentText = new Text("", 1, 0, (text: string) => theme.bg("toolPendingBg", text));
|
|
203
206
|
|
|
204
207
|
// Use Box for custom tools or built-in tools that have renderers
|
|
205
208
|
const hasRenderer = toolName in toolRenderers;
|
|
@@ -295,6 +298,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
295
298
|
isPartial = false,
|
|
296
299
|
_toolCallId?: string,
|
|
297
300
|
): void {
|
|
301
|
+
this.#textOutputCache = undefined;
|
|
298
302
|
this.#result = result;
|
|
299
303
|
this.#isPartial = isPartial;
|
|
300
304
|
// When tool is complete, ensure args are marked complete so spinner stops
|
|
@@ -397,6 +401,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
397
401
|
|
|
398
402
|
setShowImages(show: boolean): void {
|
|
399
403
|
this.#showImages = show;
|
|
404
|
+
this.#textOutputCache = undefined;
|
|
400
405
|
this.#updateDisplay();
|
|
401
406
|
}
|
|
402
407
|
|
|
@@ -514,7 +519,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
514
519
|
const fileBgFn = fileResult.isError
|
|
515
520
|
? (text: string) => theme.bg("toolErrorBg", text)
|
|
516
521
|
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
517
|
-
const fileBox = new Box(1,
|
|
522
|
+
const fileBox = new Box(1, 0, fileBgFn);
|
|
518
523
|
try {
|
|
519
524
|
const resultComponent = renderer.renderResult(
|
|
520
525
|
{ content: [], details: fileResult, isError: fileResult.isError },
|
|
@@ -540,7 +545,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
540
545
|
const pendingSpacer = new Spacer(1);
|
|
541
546
|
this.#multiFileBoxes.push(pendingSpacer);
|
|
542
547
|
this.addChild(pendingSpacer);
|
|
543
|
-
const pendingBox = new Box(1,
|
|
548
|
+
const pendingBox = new Box(1, 0, (text: string) => theme.bg("toolPendingBg", text));
|
|
544
549
|
const pendingText = renderStatusLine(
|
|
545
550
|
{
|
|
546
551
|
icon: "pending",
|
|
@@ -723,16 +728,27 @@ export class ToolExecutionComponent extends Container {
|
|
|
723
728
|
#getTextOutput(): string {
|
|
724
729
|
if (!this.#result) return "";
|
|
725
730
|
|
|
726
|
-
const
|
|
727
|
-
const
|
|
731
|
+
const content = this.#result.content;
|
|
732
|
+
const terminalImageProtocol = TERMINAL.imageProtocol;
|
|
733
|
+
const cached = this.#textOutputCache;
|
|
734
|
+
if (
|
|
735
|
+
cached?.content === content &&
|
|
736
|
+
cached.showImages === this.#showImages &&
|
|
737
|
+
cached.terminalImageProtocol === terminalImageProtocol
|
|
738
|
+
) {
|
|
739
|
+
return cached.output;
|
|
740
|
+
}
|
|
728
741
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
.
|
|
742
|
+
const textParts: string[] = [];
|
|
743
|
+
for (const block of content ?? []) {
|
|
744
|
+
if (block.type !== "text") continue;
|
|
745
|
+
const text = block.text || "";
|
|
746
|
+
textParts.push(sanitizeWithOptionalSixelPassthrough(text, sanitizeText));
|
|
747
|
+
}
|
|
748
|
+
let output = textParts.join("\n");
|
|
734
749
|
|
|
735
|
-
|
|
750
|
+
const imageBlocks = this.#getAllImageBlocks();
|
|
751
|
+
if (imageBlocks.length > 0 && (!terminalImageProtocol || !this.#showImages)) {
|
|
736
752
|
const imageIndicators = imageBlocks
|
|
737
753
|
.map((img: any) => {
|
|
738
754
|
const dims = img.data ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
|
|
@@ -742,6 +758,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
742
758
|
output = output ? `${output}\n${imageIndicators}` : imageIndicators;
|
|
743
759
|
}
|
|
744
760
|
|
|
761
|
+
this.#textOutputCache = { content, showImages: this.#showImages, terminalImageProtocol, output };
|
|
745
762
|
return output;
|
|
746
763
|
}
|
|
747
764
|
|
|
@@ -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) {
|
|
@@ -5,6 +5,7 @@ import type { Component, OverlayHandle } from "@gajae-code/tui";
|
|
|
5
5
|
import { Input, Loader, Spacer, Text } from "@gajae-code/tui";
|
|
6
6
|
import { getAgentDbPath, getProjectDir } from "@gajae-code/utils";
|
|
7
7
|
import { activateModelProfile } from "../../config/model-profile-activation";
|
|
8
|
+
import { recommendModelProfileForProvider } from "../../config/model-profiles";
|
|
8
9
|
import { settings } from "../../config/settings";
|
|
9
10
|
import { DebugSelectorComponent } from "../../debug";
|
|
10
11
|
import { disableProvider, enableProvider } from "../../discovery";
|
|
@@ -45,7 +46,7 @@ import { CustomProviderWizardComponent, type CustomProviderWizardSubmit } from "
|
|
|
45
46
|
import { ExtensionDashboard } from "../components/extensions";
|
|
46
47
|
import { HistorySearchComponent } from "../components/history-search";
|
|
47
48
|
import { JobsOverlayComponent } from "../components/jobs-overlay";
|
|
48
|
-
import { ModelSelectorComponent
|
|
49
|
+
import { ModelSelectorComponent } from "../components/model-selector";
|
|
49
50
|
import { OAuthSelectorComponent } from "../components/oauth-selector";
|
|
50
51
|
import { PluginSelectorComponent } from "../components/plugin-selector";
|
|
51
52
|
import {
|
|
@@ -537,12 +538,6 @@ export class SelectorController {
|
|
|
537
538
|
this.ctx.session.scopedModels,
|
|
538
539
|
async selection => {
|
|
539
540
|
try {
|
|
540
|
-
if (selection.kind === "preset") {
|
|
541
|
-
await this.#applyModelAssignmentPreset(selection);
|
|
542
|
-
done();
|
|
543
|
-
this.ctx.ui.requestRender();
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
541
|
if (selection.kind === "profile") {
|
|
547
542
|
await activateModelProfile(
|
|
548
543
|
{
|
|
@@ -609,45 +604,12 @@ export class SelectorController {
|
|
|
609
604
|
done();
|
|
610
605
|
this.ctx.ui.requestRender();
|
|
611
606
|
},
|
|
612
|
-
options,
|
|
607
|
+
{ ...options, sessionId: this.ctx.session.sessionId },
|
|
613
608
|
);
|
|
614
609
|
return { component: selector, focus: selector };
|
|
615
610
|
});
|
|
616
611
|
}
|
|
617
612
|
|
|
618
|
-
async #applyModelAssignmentPreset(selection: Extract<ModelSelectorSelection, { kind: "preset" }>): Promise<void> {
|
|
619
|
-
const { assignments, model, preset, selector } = selection;
|
|
620
|
-
const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
|
|
621
|
-
if (!apiKey) {
|
|
622
|
-
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const defaultThinkingLevel = assignments.default;
|
|
626
|
-
await this.ctx.session.setModel(model, "default", {
|
|
627
|
-
selector,
|
|
628
|
-
thinkingLevel: defaultThinkingLevel,
|
|
629
|
-
});
|
|
630
|
-
if (defaultThinkingLevel && defaultThinkingLevel !== ThinkingLevel.Inherit) {
|
|
631
|
-
this.ctx.session.setThinkingLevel(defaultThinkingLevel);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
const overrides = this.ctx.settings.get("task.agentModelOverrides");
|
|
635
|
-
const nextOverrides = { ...overrides };
|
|
636
|
-
for (const [targetRole, presetThinkingLevel] of Object.entries(assignments) as [
|
|
637
|
-
keyof Extract<ModelSelectorSelection, { kind: "preset" }>["assignments"],
|
|
638
|
-
ThinkingLevel,
|
|
639
|
-
][]) {
|
|
640
|
-
if (!targetRole || targetRole === "default") continue;
|
|
641
|
-
nextOverrides[targetRole] =
|
|
642
|
-
presetThinkingLevel === ThinkingLevel.Inherit ? selector : `${selector}:${presetThinkingLevel}`;
|
|
643
|
-
}
|
|
644
|
-
this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
|
|
645
|
-
this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
646
|
-
this.ctx.statusLine.invalidate();
|
|
647
|
-
this.ctx.updateEditorBorderColor();
|
|
648
|
-
this.ctx.showStatus(`${preset.label}: ${selector}`);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
613
|
async showPluginSelector(mode: "install" | "uninstall" = "install"): Promise<void> {
|
|
652
614
|
const mgr = new MarketplaceManager({
|
|
653
615
|
marketplacesRegistryPath: getMarketplacesRegistryPath(),
|
|
@@ -1034,6 +996,34 @@ export class SelectorController {
|
|
|
1034
996
|
await this.showSessionSelector();
|
|
1035
997
|
}
|
|
1036
998
|
|
|
999
|
+
async #handlePostLoginModelProfileRecommendation(providerId: string): Promise<void> {
|
|
1000
|
+
const recommendedProfile = recommendModelProfileForProvider(
|
|
1001
|
+
providerId,
|
|
1002
|
+
this.ctx.session.modelRegistry.getModelProfiles(),
|
|
1003
|
+
);
|
|
1004
|
+
if (!recommendedProfile) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const activeProfile = this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default");
|
|
1009
|
+
if (activeProfile) {
|
|
1010
|
+
this.ctx.showStatus(`Preset ${recommendedProfile.name} is available in /model.`);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const confirmed = await this.ctx.showHookConfirm(`Apply ${recommendedProfile.name} now?`, "");
|
|
1015
|
+
if (!confirmed) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
await activateModelProfile({
|
|
1020
|
+
session: this.ctx.session,
|
|
1021
|
+
modelRegistry: this.ctx.session.modelRegistry,
|
|
1022
|
+
settings: this.ctx.settings,
|
|
1023
|
+
profileName: recommendedProfile.name,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1037
1027
|
async #handleOAuthLogin(providerId: string): Promise<void> {
|
|
1038
1028
|
this.ctx.showStatus(`Logging in to ${providerId}…`);
|
|
1039
1029
|
const manualInput = this.ctx.oauthManualInput;
|
|
@@ -1090,6 +1080,7 @@ export class SelectorController {
|
|
|
1090
1080
|
new Text(theme.fg("success", `${theme.status.success} Successfully logged in to ${providerId}`), 1, 0),
|
|
1091
1081
|
);
|
|
1092
1082
|
this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Credentials saved to ${getAgentDbPath()}`), 1, 0));
|
|
1083
|
+
await this.#handlePostLoginModelProfileRecommendation(providerId);
|
|
1093
1084
|
this.ctx.ui.requestRender();
|
|
1094
1085
|
} catch (error: unknown) {
|
|
1095
1086
|
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1120,7 +1111,7 @@ export class SelectorController {
|
|
|
1120
1111
|
async showOAuthSelector(mode: "login" | "logout", providerId?: string): Promise<void> {
|
|
1121
1112
|
if (providerId) {
|
|
1122
1113
|
const oauthProvider = getOAuthProviders().find(provider => provider.id === providerId);
|
|
1123
|
-
if (!oauthProvider) {
|
|
1114
|
+
if (!oauthProvider && !this.ctx.session.modelRegistry.getModelProfiles().has(providerId)) {
|
|
1124
1115
|
this.ctx.showError(`Unknown OAuth provider: ${providerId}`);
|
|
1125
1116
|
return;
|
|
1126
1117
|
}
|
|
@@ -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;
|
|
@@ -12,6 +12,7 @@ import type { SessionStats } from "../../session/agent-session";
|
|
|
12
12
|
import type {
|
|
13
13
|
RpcCommand,
|
|
14
14
|
RpcExtensionUIRequest,
|
|
15
|
+
RpcGetStateInclude,
|
|
15
16
|
RpcHandoffResult,
|
|
16
17
|
RpcHostToolCallRequest,
|
|
17
18
|
RpcHostToolCancelRequest,
|
|
@@ -442,8 +443,8 @@ export class RpcClient {
|
|
|
442
443
|
/**
|
|
443
444
|
* Get current session state.
|
|
444
445
|
*/
|
|
445
|
-
async getState(): Promise<RpcSessionState> {
|
|
446
|
-
const response = await this.#send({ type: "get_state" });
|
|
446
|
+
async getState(include?: RpcGetStateInclude[]): Promise<RpcSessionState> {
|
|
447
|
+
const response = await this.#send(include ? { type: "get_state", include } : { type: "get_state" });
|
|
447
448
|
return this.#getData(response);
|
|
448
449
|
}
|
|
449
450
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
-
import { $env,
|
|
14
|
+
import { $env, readLines, Snowflake } from "@gajae-code/utils";
|
|
15
15
|
import type {
|
|
16
16
|
ExtensionUIContext,
|
|
17
17
|
ExtensionUIDialogOptions,
|
|
@@ -169,6 +169,7 @@ export async function runRpcMode(
|
|
|
169
169
|
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
170
170
|
};
|
|
171
171
|
const emitRpcTitles = shouldEmitRpcTitles();
|
|
172
|
+
const decodeError = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
172
173
|
|
|
173
174
|
const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
|
|
174
175
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
@@ -229,6 +230,28 @@ export async function runRpcMode(
|
|
|
229
230
|
|
|
230
231
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
231
232
|
const shutdownState = { requested: false };
|
|
233
|
+
let shutdownStarted = false;
|
|
234
|
+
async function shutdown(exitCode: number, reason: string): Promise<never> {
|
|
235
|
+
if (shutdownStarted) {
|
|
236
|
+
process.exit(exitCode);
|
|
237
|
+
}
|
|
238
|
+
shutdownStarted = true;
|
|
239
|
+
hostToolBridge.rejectAllPending(`${reason} before host tool execution completed`);
|
|
240
|
+
hostUriBridge.clear(`${reason} before host URI request completed`);
|
|
241
|
+
try {
|
|
242
|
+
await session.sessionManager.ensureOnDisk();
|
|
243
|
+
} catch (err) {
|
|
244
|
+
output(error(undefined, "shutdown", decodeError(err)));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
await session.dispose();
|
|
249
|
+
} catch (err) {
|
|
250
|
+
output(error(undefined, "shutdown", decodeError(err)));
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
process.exit(exitCode);
|
|
254
|
+
}
|
|
232
255
|
|
|
233
256
|
/**
|
|
234
257
|
* Extension UI context that uses the RPC protocol.
|
|
@@ -497,16 +520,24 @@ export async function runRpcMode(
|
|
|
497
520
|
*/
|
|
498
521
|
async function checkShutdownRequested(): Promise<void> {
|
|
499
522
|
if (!shutdownState.requested) return;
|
|
523
|
+
await shutdown(0, "RPC shutdown requested");
|
|
524
|
+
}
|
|
500
525
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
526
|
+
// Listen for JSONL input using Bun's stdin. Parse frame-by-frame so a malformed
|
|
527
|
+
// command reports a parse error without poisoning the whole long-lived RPC session.
|
|
528
|
+
const inputDecoder = new TextDecoder("utf-8", { fatal: false });
|
|
529
|
+
for await (const line of readLines(Bun.stdin.stream())) {
|
|
530
|
+
const text = inputDecoder.decode(line).trim();
|
|
531
|
+
if (!text) continue;
|
|
504
532
|
|
|
505
|
-
|
|
506
|
-
|
|
533
|
+
let parsed: unknown;
|
|
534
|
+
try {
|
|
535
|
+
parsed = JSON.parse(text);
|
|
536
|
+
} catch (err) {
|
|
537
|
+
output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
507
540
|
|
|
508
|
-
// Listen for JSON input using Bun's stdin
|
|
509
|
-
for await (const parsed of readJsonl(Bun.stdin.stream())) {
|
|
510
541
|
try {
|
|
511
542
|
// Handle extension UI responses
|
|
512
543
|
if ((parsed as RpcExtensionUIResponse).type === "extension_ui_response") {
|
|
@@ -540,13 +571,12 @@ export async function runRpcMode(
|
|
|
540
571
|
|
|
541
572
|
// Check for deferred shutdown request (idle between commands)
|
|
542
573
|
await checkShutdownRequested();
|
|
543
|
-
} catch (
|
|
544
|
-
output(error(undefined, "parse", `Failed to parse command: ${
|
|
574
|
+
} catch (err) {
|
|
575
|
+
output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
|
|
545
576
|
}
|
|
546
577
|
}
|
|
547
578
|
|
|
548
|
-
// stdin closed — RPC client is gone, exit cleanly
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
process.exit(0);
|
|
579
|
+
// stdin closed — RPC client is gone, flush durable state and exit cleanly
|
|
580
|
+
await shutdown(0, "RPC client disconnected");
|
|
581
|
+
throw new Error("RPC shutdown returned unexpectedly");
|
|
552
582
|
}
|
|
@@ -12,6 +12,8 @@ import type { ContextUsage } from "../../extensibility/extensions/types";
|
|
|
12
12
|
import type { SessionStats } from "../../session/agent-session";
|
|
13
13
|
import type { TodoPhase } from "../../tools/todo-write";
|
|
14
14
|
|
|
15
|
+
export type RpcGetStateInclude = "tools" | "dumpTools" | "systemPrompt";
|
|
16
|
+
|
|
15
17
|
// ============================================================================
|
|
16
18
|
// RPC Commands (stdin)
|
|
17
19
|
// ============================================================================
|
|
@@ -26,7 +28,7 @@ export type RpcCommand =
|
|
|
26
28
|
| { id?: string; type: "new_session"; parentSession?: string }
|
|
27
29
|
|
|
28
30
|
// State
|
|
29
|
-
| { id?: string; type: "get_state" }
|
|
31
|
+
| { id?: string; type: "get_state"; include?: RpcGetStateInclude[] }
|
|
30
32
|
| { id?: string; type: "set_todos"; phases: TodoPhase[] }
|
|
31
33
|
| { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
|
|
32
34
|
| { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
|
|
@@ -99,8 +101,9 @@ export interface RpcSessionState {
|
|
|
99
101
|
messageCount: number;
|
|
100
102
|
queuedMessageCount: number;
|
|
101
103
|
todoPhases: TodoPhase[];
|
|
102
|
-
/**
|
|
104
|
+
/** Optional static system prompt blocks. Omitted by default; request with get_state include ["systemPrompt"]. */
|
|
103
105
|
systemPrompt?: string[];
|
|
106
|
+
/** Optional static tool schemas. Omitted by default; request with get_state include ["tools"]. */
|
|
104
107
|
dumpTools?: Array<{ name: string; description: string; parameters: unknown }>;
|
|
105
108
|
/** Current context window usage. Null tokens/percent when unknown (e.g. right after compaction). */
|
|
106
109
|
contextUsage?: ContextUsage;
|
|
@@ -183,14 +183,19 @@ export async function dispatchRpcCommand(
|
|
|
183
183
|
messageCount: session.messages.length,
|
|
184
184
|
queuedMessageCount: session.queuedMessageCount,
|
|
185
185
|
todoPhases: session.getTodoPhases(),
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
contextUsage: session.getContextUsage(),
|
|
187
|
+
};
|
|
188
|
+
const include = new Set(command.include ?? []);
|
|
189
|
+
if (include.has("systemPrompt")) {
|
|
190
|
+
state.systemPrompt = session.systemPrompt;
|
|
191
|
+
}
|
|
192
|
+
if (include.has("tools") || include.has("dumpTools")) {
|
|
193
|
+
state.dumpTools = session.agent.state.tools.map(tool => ({
|
|
188
194
|
name: tool.name,
|
|
189
195
|
description: tool.description,
|
|
190
196
|
parameters: tool.parameters,
|
|
191
|
-
}))
|
|
192
|
-
|
|
193
|
-
};
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
194
199
|
return rpcSuccess(id, "get_state", state);
|
|
195
200
|
}
|
|
196
201
|
|
|
@@ -28,6 +28,15 @@ function stringArray(value: unknown): value is string[] {
|
|
|
28
28
|
return Array.isArray(value) && value.every(item => typeof item === "string");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
const GET_STATE_INCLUDES = new Set(["tools", "dumpTools", "systemPrompt"]);
|
|
32
|
+
|
|
33
|
+
function optionalGetStateInclude(value: unknown): boolean {
|
|
34
|
+
return (
|
|
35
|
+
value === undefined ||
|
|
36
|
+
(Array.isArray(value) && value.every(item => typeof item === "string" && GET_STATE_INCLUDES.has(item)))
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
function todoPhase(value: unknown): boolean {
|
|
32
41
|
if (!isRecord(value) || typeof value.name !== "string" || !Array.isArray(value.tasks)) return false;
|
|
33
42
|
return value.tasks.every(
|
|
@@ -96,7 +105,9 @@ export function isRpcCommand(value: unknown): value is RpcCommand {
|
|
|
96
105
|
case "follow_up":
|
|
97
106
|
return stringField(value, "message") && optionalArray(value.images);
|
|
98
107
|
case "abort":
|
|
108
|
+
return true;
|
|
99
109
|
case "get_state":
|
|
110
|
+
return optionalGetStateInclude(value.include);
|
|
100
111
|
case "cycle_model":
|
|
101
112
|
case "get_available_models":
|
|
102
113
|
case "cycle_thinking_level":
|
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 {
|