@gajae-code/coding-agent 0.4.5 → 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 +43 -0
- package/dist/types/commands/harness.d.ts +3 -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 +3 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +30 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- 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/owner.d.ts +1 -1
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/types.d.ts +4 -0
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- 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/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 +24 -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/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +7 -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 +7 -7
- package/src/cli.ts +8 -4
- package/src/commands/harness.ts +36 -2
- package/src/commands/launch.ts +2 -2
- package/src/commands/session.ts +3 -1
- 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 +1 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator-mcp/server.ts +54 -23
- package/src/cursor.ts +16 -2
- 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/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/owner.ts +78 -27
- package/src/harness-control-plane/receipt-spool.ts +128 -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 +4 -0
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +2 -2
- 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/tool-execution.ts +30 -13
- package/src/modes/controllers/selector-controller.ts +33 -42
- 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/sdk.ts +29 -2
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +105 -20
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +309 -58
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/task/executor.ts +69 -6
- package/src/task/receipt.ts +5 -0
- package/src/task/render.ts +21 -1
- package/src/task/types.ts +8 -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 +2 -0
- package/src/tools/resolve.ts +93 -18
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/tool-choice.ts +45 -16
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
2
|
+
|
|
2
3
|
import { sanitizeText } from "@gajae-code/utils";
|
|
3
4
|
import { formatBytes } from "../tools/render-utils";
|
|
4
5
|
import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
|
|
5
6
|
|
|
7
|
+
function sanitizeOutputChunk(rawChunk: string): string {
|
|
8
|
+
return sanitizeWithOptionalSixelPassthrough(rawChunk, sanitizeText);
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
// =============================================================================
|
|
7
12
|
// Constants
|
|
8
13
|
// =============================================================================
|
|
@@ -12,6 +17,7 @@ export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
|
|
12
17
|
export const DEFAULT_MAX_COLUMN = 1024; // Max chars per grep match line
|
|
13
18
|
|
|
14
19
|
const NL = "\n";
|
|
20
|
+
|
|
15
21
|
const ELLIPSIS = "…";
|
|
16
22
|
|
|
17
23
|
// =============================================================================
|
|
@@ -654,10 +660,11 @@ export class OutputSink {
|
|
|
654
660
|
#bufferBytes = 0;
|
|
655
661
|
#head = "";
|
|
656
662
|
#headBytes = 0;
|
|
657
|
-
#headLines = 0; // newline count inside #head
|
|
658
663
|
#headRetentionDisabled = false;
|
|
659
664
|
#totalLines = 0; // newline count
|
|
660
665
|
#totalBytes = 0;
|
|
666
|
+
#processedBytes = 0;
|
|
667
|
+
#processedLines = 0; // newline count after sanitize/column-cap
|
|
661
668
|
#sawData = false;
|
|
662
669
|
#truncated = false;
|
|
663
670
|
#lastChunkTime = 0;
|
|
@@ -674,8 +681,12 @@ export class OutputSink {
|
|
|
674
681
|
sink: Bun.FileSink;
|
|
675
682
|
};
|
|
676
683
|
|
|
677
|
-
//
|
|
684
|
+
// Raw prefix chunks not yet confirmed written to the file sink. This queue is
|
|
685
|
+
// the only artifact replay source; retained head/tail windows are lossy views.
|
|
678
686
|
#pendingFileWrites?: string[];
|
|
687
|
+
#pendingFileWriteBytes = 0;
|
|
688
|
+
#finalized = false;
|
|
689
|
+
|
|
679
690
|
#fileReady = false;
|
|
680
691
|
|
|
681
692
|
readonly #artifactPath?: string;
|
|
@@ -708,76 +719,99 @@ export class OutputSink {
|
|
|
708
719
|
this.#chunkThrottleMs = chunkThrottleMs;
|
|
709
720
|
}
|
|
710
721
|
|
|
722
|
+
#headText(): string {
|
|
723
|
+
return this.#head;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
#tailText(): string {
|
|
727
|
+
return this.#buffer;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
#setTail(text: string, bytes = Buffer.byteLength(text, "utf-8")): void {
|
|
731
|
+
this.#buffer = text;
|
|
732
|
+
this.#bufferBytes = bytes;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
#appendTail(text: string, bytes: number): void {
|
|
736
|
+
this.#buffer += text;
|
|
737
|
+
this.#bufferBytes += bytes;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
#appendHead(text: string, bytes: number): void {
|
|
741
|
+
this.#head += text;
|
|
742
|
+
this.#headBytes += bytes;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
#trimTailTo(maxBytes: number): void {
|
|
746
|
+
if (this.#bufferBytes <= maxBytes) return;
|
|
747
|
+
const { text, bytes } = truncateTailBytes(this.#buffer, maxBytes);
|
|
748
|
+
this.#buffer = text;
|
|
749
|
+
this.#bufferBytes = bytes;
|
|
750
|
+
}
|
|
751
|
+
|
|
711
752
|
/**
|
|
712
|
-
* Push a chunk of output.
|
|
713
|
-
*
|
|
753
|
+
* Push a chunk of output. Raw bytes are mirrored to artifacts, while the
|
|
754
|
+
* visible retention windows are selected from the sanitized/column-capped
|
|
755
|
+
* stream so production-default display matches the historical processed view.
|
|
714
756
|
*/
|
|
715
757
|
push(chunk: string): void {
|
|
716
|
-
|
|
758
|
+
const rawChunk = chunk;
|
|
717
759
|
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
760
|
+
// Live callbacks historically observe sanitized, uncapped chunks. The same
|
|
761
|
+
// sanitized text is also the input to visible accounting/retention.
|
|
762
|
+
const sanitizedChunk = sanitizeOutputChunk(rawChunk);
|
|
763
|
+
|
|
764
|
+
if (this.#onRawChunk && sanitizedChunk.length > 0) {
|
|
765
|
+
this.#onRawChunk(sanitizedChunk);
|
|
723
766
|
}
|
|
724
767
|
|
|
725
|
-
// Throttled onChunk: only call the callback when enough time has passed.
|
|
726
|
-
// Live preview gets the raw (pre-cap) chunk so the TUI never lags behind
|
|
727
|
-
// what reached the sink — the column cap is for the persisted LLM view.
|
|
728
768
|
if (this.#onChunk) {
|
|
729
769
|
const now = Date.now();
|
|
730
770
|
if (now - this.#lastChunkTime >= this.#chunkThrottleMs) {
|
|
731
771
|
this.#lastChunkTime = now;
|
|
732
|
-
this.#onChunk(
|
|
772
|
+
this.#onChunk(sanitizedChunk);
|
|
733
773
|
}
|
|
734
774
|
}
|
|
735
775
|
|
|
736
|
-
const rawBytes = Buffer.byteLength(
|
|
776
|
+
const rawBytes = Buffer.byteLength(rawChunk, "utf-8");
|
|
737
777
|
this.#totalBytes += rawBytes;
|
|
738
778
|
|
|
739
|
-
if (
|
|
779
|
+
if (rawChunk.length > 0) {
|
|
740
780
|
this.#sawData = true;
|
|
741
|
-
this.#totalLines += countNewlines(
|
|
781
|
+
this.#totalLines += countNewlines(rawChunk);
|
|
742
782
|
}
|
|
743
783
|
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
if (
|
|
756
|
-
this.#
|
|
784
|
+
// Mirror the original, unsanitized/uncapped bytes. Until the artifact sink is
|
|
785
|
+
// open, keep an independent raw replay prefix because retained head/tail
|
|
786
|
+
// windows are trimmed and cannot reconstruct byte-correct artifacts.
|
|
787
|
+
if (this.#artifactPath && this.#maxColumns === 0) this.#enqueueFileWrite(rawChunk, rawBytes);
|
|
788
|
+
|
|
789
|
+
if (rawBytes === 0) return;
|
|
790
|
+
|
|
791
|
+
const visibleChunk = this.#maxColumns > 0 ? this.#applyColumnCap(sanitizedChunk) : sanitizedChunk;
|
|
792
|
+
if (this.#artifactPath && this.#maxColumns > 0) this.#enqueueFileWrite(rawChunk, rawBytes);
|
|
793
|
+
if (this.#columnDroppedBytes > 0) this.#createFileSink();
|
|
794
|
+
const visibleBytes = Buffer.byteLength(visibleChunk, "utf-8");
|
|
795
|
+
if (visibleChunk.length > 0) {
|
|
796
|
+
this.#processedBytes += visibleBytes;
|
|
797
|
+
this.#processedLines += countNewlines(visibleChunk);
|
|
757
798
|
}
|
|
799
|
+
if (visibleBytes === 0) return;
|
|
758
800
|
|
|
759
|
-
|
|
801
|
+
let tailChunk = visibleChunk;
|
|
802
|
+
let tailBytes = visibleBytes;
|
|
760
803
|
|
|
761
|
-
// Head retention: drain the (capped) chunk into #head until the budget is
|
|
762
|
-
// exhausted, then forward any leftover to the tail buffer.
|
|
763
|
-
let tailChunk = capped;
|
|
764
|
-
let tailBytes = cappedBytes;
|
|
765
804
|
if (this.#headLimit > 0 && !this.#headRetentionDisabled && this.#headBytes < this.#headLimit) {
|
|
766
805
|
const room = this.#headLimit - this.#headBytes;
|
|
767
|
-
if (
|
|
768
|
-
this.#
|
|
769
|
-
this.#headBytes += cappedBytes;
|
|
770
|
-
this.#headLines += countNewlines(capped);
|
|
806
|
+
if (visibleBytes <= room) {
|
|
807
|
+
this.#appendHead(visibleChunk, visibleBytes);
|
|
771
808
|
return;
|
|
772
809
|
}
|
|
773
|
-
|
|
774
|
-
const headSlice = truncateHeadBytes(capped, room);
|
|
810
|
+
const headSlice = truncateHeadBytes(visibleChunk, room);
|
|
775
811
|
if (headSlice.bytes > 0) {
|
|
776
|
-
this.#
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
tailChunk = capped.substring(headSlice.text.length);
|
|
780
|
-
tailBytes = cappedBytes - headSlice.bytes;
|
|
812
|
+
this.#appendHead(headSlice.text, headSlice.bytes);
|
|
813
|
+
tailChunk = visibleChunk.substring(headSlice.text.length);
|
|
814
|
+
tailBytes = visibleBytes - headSlice.bytes;
|
|
781
815
|
}
|
|
782
816
|
}
|
|
783
817
|
|
|
@@ -790,6 +824,7 @@ export class OutputSink {
|
|
|
790
824
|
* cap; subsequent bytes are skipped until the next `\n`. State persists
|
|
791
825
|
* across calls so a long line split across chunks still produces one marker.
|
|
792
826
|
*/
|
|
827
|
+
|
|
793
828
|
#applyColumnCap(chunk: string): string {
|
|
794
829
|
if (chunk.length === 0) return chunk;
|
|
795
830
|
const max = this.#maxColumns;
|
|
@@ -800,11 +835,11 @@ export class OutputSink {
|
|
|
800
835
|
const segEnd = nlIdx === -1 ? chunk.length : nlIdx;
|
|
801
836
|
if (segEnd > cursor) {
|
|
802
837
|
const segment = chunk.substring(cursor, segEnd);
|
|
838
|
+
const segBytes = Buffer.byteLength(segment, "utf-8");
|
|
803
839
|
if (this.#columnEllipsisAdded) {
|
|
804
840
|
// Past the cap; drop until newline.
|
|
805
|
-
this.#columnDroppedBytes +=
|
|
841
|
+
this.#columnDroppedBytes += segBytes;
|
|
806
842
|
} else {
|
|
807
|
-
const segBytes = Buffer.byteLength(segment, "utf-8");
|
|
808
843
|
const remaining = max - this.#currentLineBytes;
|
|
809
844
|
if (segBytes <= remaining) {
|
|
810
845
|
parts.push(segment);
|
|
@@ -814,13 +849,11 @@ export class OutputSink {
|
|
|
814
849
|
// arm the skip-until-newline flag.
|
|
815
850
|
const ellipsisBytes = 3; // "…" in UTF-8
|
|
816
851
|
const headRoom = Math.max(0, remaining - ellipsisBytes);
|
|
817
|
-
let kept = "";
|
|
818
852
|
let keptBytes = 0;
|
|
819
853
|
if (headRoom > 0) {
|
|
820
854
|
const sliced = truncateHeadBytes(segment, headRoom);
|
|
821
|
-
kept = sliced.text;
|
|
822
855
|
keptBytes = sliced.bytes;
|
|
823
|
-
parts.push(
|
|
856
|
+
parts.push(sliced.text);
|
|
824
857
|
}
|
|
825
858
|
parts.push(ELLIPSIS);
|
|
826
859
|
this.#columnDroppedBytes += segBytes - keptBytes;
|
|
@@ -839,6 +872,10 @@ export class OutputSink {
|
|
|
839
872
|
return parts.join("");
|
|
840
873
|
}
|
|
841
874
|
|
|
875
|
+
#retainedSummary(text: string): { text: string; bytes: number; lines: number } {
|
|
876
|
+
return { text, bytes: Buffer.byteLength(text, "utf-8"), lines: text.length > 0 ? countNewlines(text) : 0 };
|
|
877
|
+
}
|
|
878
|
+
|
|
842
879
|
#willOverflow(dataBytes: number): boolean {
|
|
843
880
|
// Triggers file mirroring as soon as the next chunk would push us over
|
|
844
881
|
// the tail budget (head retention does not change spill-to-artifact).
|
|
@@ -852,8 +889,7 @@ export class OutputSink {
|
|
|
852
889
|
const willOverflow = this.#bufferBytes + dataBytes > threshold;
|
|
853
890
|
|
|
854
891
|
if (!willOverflow) {
|
|
855
|
-
this.#
|
|
856
|
-
this.#bufferBytes += dataBytes;
|
|
892
|
+
this.#appendTail(chunk, dataBytes);
|
|
857
893
|
return;
|
|
858
894
|
}
|
|
859
895
|
|
|
@@ -863,66 +899,75 @@ export class OutputSink {
|
|
|
863
899
|
// Avoid creating a giant intermediate string when chunk alone dominates.
|
|
864
900
|
if (dataBytes >= threshold) {
|
|
865
901
|
const { text, bytes } = truncateTailBytes(chunk, threshold);
|
|
866
|
-
this.#
|
|
867
|
-
this.#bufferBytes = bytes;
|
|
902
|
+
this.#setTail(text, bytes);
|
|
868
903
|
} else {
|
|
869
904
|
// Intermediate size is bounded (<= threshold + dataBytes), safe to concat.
|
|
870
|
-
this.#
|
|
871
|
-
this.#
|
|
872
|
-
|
|
873
|
-
const { text, bytes } = truncateTailBytes(this.#buffer, threshold);
|
|
874
|
-
this.#buffer = text;
|
|
875
|
-
this.#bufferBytes = bytes;
|
|
905
|
+
this.#appendTail(chunk, dataBytes);
|
|
906
|
+
this.#trimTailTo(threshold);
|
|
876
907
|
}
|
|
877
908
|
}
|
|
878
909
|
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
910
|
+
#queuePendingFileWrite(chunk: string, bytes = Buffer.byteLength(chunk, "utf-8")): void {
|
|
911
|
+
if (!this.#pendingFileWrites) this.#pendingFileWrites = [chunk];
|
|
912
|
+
else this.#pendingFileWrites.push(chunk);
|
|
913
|
+
|
|
914
|
+
this.#pendingFileWriteBytes += bytes;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
#enqueueFileWrite(chunk: string, bytes: number): void {
|
|
918
|
+
if (!this.#fileReady || !this.#file) {
|
|
919
|
+
this.#queuePendingFileWrite(chunk, bytes);
|
|
920
|
+
if (this.#willOverflow(bytes) || this.#pendingFileWriteBytes > this.#spillThreshold) this.#createFileSink();
|
|
887
921
|
return;
|
|
888
922
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
this.#
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
923
|
+
|
|
924
|
+
try {
|
|
925
|
+
this.#file.sink.write(chunk);
|
|
926
|
+
} catch {
|
|
927
|
+
try {
|
|
928
|
+
void this.#file.sink.end();
|
|
929
|
+
} catch {
|
|
930
|
+
/* ignore */
|
|
931
|
+
}
|
|
932
|
+
this.#file = undefined;
|
|
933
|
+
this.#fileReady = false;
|
|
934
|
+
this.#queuePendingFileWrite(chunk, bytes);
|
|
935
|
+
this.#createFileSink();
|
|
895
936
|
}
|
|
896
937
|
}
|
|
897
938
|
|
|
898
|
-
|
|
899
|
-
if (
|
|
939
|
+
#createFileSink(): boolean {
|
|
940
|
+
if (this.#finalized) return false;
|
|
941
|
+
|
|
942
|
+
if (!this.#artifactPath) return false;
|
|
943
|
+
if (this.#fileReady) return this.#file != null;
|
|
900
944
|
try {
|
|
901
945
|
const sink = Bun.file(this.#artifactPath).writer();
|
|
902
946
|
this.#file = { path: this.#artifactPath, artifactId: this.#artifactId, sink };
|
|
903
947
|
|
|
904
|
-
|
|
905
|
-
if (
|
|
906
|
-
sink.write(
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// Drain any chunks that arrived while the sink was being created
|
|
910
|
-
if (this.#pendingFileWrites) {
|
|
911
|
-
for (const pending of this.#pendingFileWrites) {
|
|
912
|
-
sink.write(pending);
|
|
913
|
-
}
|
|
914
|
-
this.#pendingFileWrites = undefined;
|
|
948
|
+
const pending = this.#pendingFileWrites;
|
|
949
|
+
if (pending) {
|
|
950
|
+
for (const chunk of pending) sink.write(chunk);
|
|
915
951
|
}
|
|
916
952
|
|
|
917
953
|
this.#fileReady = true;
|
|
954
|
+
this.#pendingFileWrites = undefined;
|
|
955
|
+
this.#pendingFileWriteBytes = 0;
|
|
956
|
+
|
|
957
|
+
return true;
|
|
918
958
|
} catch {
|
|
919
959
|
try {
|
|
920
|
-
|
|
960
|
+
void this.#file?.sink?.end();
|
|
921
961
|
} catch {
|
|
922
962
|
/* ignore */
|
|
923
963
|
}
|
|
924
964
|
this.#file = undefined;
|
|
925
|
-
|
|
965
|
+
// Keep #pendingFileWriteBytes in sync with the preserved queue so
|
|
966
|
+
// later retry/threshold decisions don't undercount retained bytes.
|
|
967
|
+
this.#pendingFileWriteBytes = this.#pendingFileWrites
|
|
968
|
+
? this.#pendingFileWrites.reduce((sum, chunk) => sum + Buffer.byteLength(chunk), 0)
|
|
969
|
+
: 0;
|
|
970
|
+
return false;
|
|
926
971
|
}
|
|
927
972
|
}
|
|
928
973
|
|
|
@@ -953,14 +998,14 @@ export class OutputSink {
|
|
|
953
998
|
* branch in `dump()` against stale totals.
|
|
954
999
|
*/
|
|
955
1000
|
replace(text: string): void {
|
|
956
|
-
this.#
|
|
957
|
-
this.#bufferBytes = Buffer.byteLength(text, "utf-8");
|
|
1001
|
+
this.#setTail(text);
|
|
958
1002
|
this.#head = "";
|
|
959
1003
|
this.#headBytes = 0;
|
|
960
|
-
this.#headLines = 0;
|
|
961
1004
|
this.#headRetentionDisabled = true;
|
|
962
1005
|
this.#totalBytes = this.#bufferBytes;
|
|
1006
|
+
this.#processedBytes = this.#bufferBytes;
|
|
963
1007
|
this.#totalLines = countNewlines(text);
|
|
1008
|
+
this.#processedLines = this.#totalLines;
|
|
964
1009
|
this.#sawData = text.length > 0;
|
|
965
1010
|
this.#truncated = false;
|
|
966
1011
|
this.#currentLineBytes = 0;
|
|
@@ -973,19 +1018,34 @@ export class OutputSink {
|
|
|
973
1018
|
const noticeLine = notice ? `[${notice}]\n` : "";
|
|
974
1019
|
const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
|
|
975
1020
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1021
|
+
let artifactId: string | undefined;
|
|
1022
|
+
if (this.#file) {
|
|
1023
|
+
artifactId = this.#file.artifactId;
|
|
1024
|
+
await this.#file.sink.end();
|
|
1025
|
+
this.#finalized = true;
|
|
1026
|
+
}
|
|
1027
|
+
if (this.#finalized) {
|
|
1028
|
+
// Terminal: the artifact is closed; replay state is no longer needed.
|
|
1029
|
+
this.#pendingFileWrites = undefined;
|
|
1030
|
+
this.#pendingFileWriteBytes = 0;
|
|
1031
|
+
this.#fileReady = false;
|
|
1032
|
+
}
|
|
1033
|
+
// Non-finalized dumps (no artifact sink ever opened) keep the raw replay
|
|
1034
|
+
// queue so a later post-dump push that spills produces a CUMULATIVE
|
|
1035
|
+
// artifact, matching the cumulative visible summary/counters.
|
|
1036
|
+
|
|
1037
|
+
// Compose the visible output from already-processed retention windows.
|
|
1038
|
+
const processedTotalLines = this.#sawData ? this.#processedLines + 1 : 0;
|
|
1039
|
+
const headText = this.#headText();
|
|
1040
|
+
const head = this.#retainedSummary(headText);
|
|
1041
|
+
|
|
1042
|
+
const headBytes = head.bytes;
|
|
1043
|
+
const headLines = head.lines + (headBytes > 0 && !headText.endsWith("\n") ? 1 : 0);
|
|
1044
|
+
const tailBuf = this.#tailText();
|
|
1045
|
+
const tail = this.#retainedSummary(tailBuf);
|
|
1046
|
+
const tailBytes = tail.bytes;
|
|
1047
|
+
const tailLines = tailBuf.length > 0 ? tail.lines + 1 : 0;
|
|
1048
|
+
const effectiveTotalBytes = this.#processedBytes;
|
|
989
1049
|
|
|
990
1050
|
let body: string;
|
|
991
1051
|
let outputBytes: number;
|
|
@@ -996,23 +1056,25 @@ export class OutputSink {
|
|
|
996
1056
|
if (headBytes > 0 && effectiveTotalBytes > headBytes + tailBytes) {
|
|
997
1057
|
// Middle was elided. Emit head + marker + tail.
|
|
998
1058
|
elidedBytes = Math.max(0, effectiveTotalBytes - headBytes - tailBytes);
|
|
999
|
-
elidedLines = Math.max(0,
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
markerBytes
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1059
|
+
elidedLines = Math.max(0, processedTotalLines - headLines - tailLines);
|
|
1060
|
+
if (elidedLines === 0) {
|
|
1061
|
+
body = headText;
|
|
1062
|
+
outputBytes = headBytes;
|
|
1063
|
+
outputLines = headLines;
|
|
1064
|
+
this.#truncated = true;
|
|
1065
|
+
} else {
|
|
1066
|
+
const marker = formatMiddleElisionMarker(elidedLines, elidedBytes);
|
|
1067
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
1068
|
+
const headSep = headText.endsWith("\n") ? "" : "\n";
|
|
1069
|
+
const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
|
|
1070
|
+
body = `${headText}${headSep}${marker}${tailSep}${tailBuf}`;
|
|
1071
|
+
outputBytes = headBytes + markerBytes + tailBytes + headSep.length + tailSep.length;
|
|
1072
|
+
outputLines = headLines + 1 + tailLines;
|
|
1073
|
+
this.#truncated = true;
|
|
1074
|
+
}
|
|
1013
1075
|
} else if (headBytes > 0) {
|
|
1014
1076
|
// Head + tail combine into the full buffered output (no overlap or elision).
|
|
1015
|
-
body = `${
|
|
1077
|
+
body = `${headText}${tailBuf}`;
|
|
1016
1078
|
outputBytes = headBytes + tailBytes;
|
|
1017
1079
|
outputLines = body.length > 0 ? countNewlines(body) + 1 : 0;
|
|
1018
1080
|
} else {
|
|
@@ -1021,6 +1083,7 @@ export class OutputSink {
|
|
|
1021
1083
|
outputLines = tailLines;
|
|
1022
1084
|
}
|
|
1023
1085
|
|
|
1086
|
+
if (this.#columnDroppedBytes > 0) this.#truncated = true;
|
|
1024
1087
|
return {
|
|
1025
1088
|
output: `${noticeLine}${body}`,
|
|
1026
1089
|
truncated: this.#truncated,
|
|
@@ -1032,7 +1095,7 @@ export class OutputSink {
|
|
|
1032
1095
|
elidedLines,
|
|
1033
1096
|
columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
|
|
1034
1097
|
columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
|
|
1035
|
-
artifactId
|
|
1098
|
+
artifactId,
|
|
1036
1099
|
};
|
|
1037
1100
|
}
|
|
1038
1101
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolChoice } from "@gajae-code/ai";
|
|
2
|
+
import { logger } from "@gajae-code/utils";
|
|
2
3
|
|
|
3
4
|
// ── Callback types ──────────────────────────────────────────────────────────
|
|
4
5
|
|
|
@@ -168,6 +169,28 @@ export class ToolChoiceQueue {
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Drop an in-flight yield after runtime tool_choice degradation. This bypasses
|
|
174
|
+
* onRejected so forced directives cannot requeue themselves after the model
|
|
175
|
+
* proved incapable of honoring the forced choice.
|
|
176
|
+
*/
|
|
177
|
+
degradeInFlight(reason?: string): string | undefined {
|
|
178
|
+
const inFlight = this.#inFlight;
|
|
179
|
+
this.#inFlight = undefined;
|
|
180
|
+
if (!inFlight) return undefined;
|
|
181
|
+
|
|
182
|
+
const label = inFlight.directive.label;
|
|
183
|
+
if (this.#queue[0] === inFlight.directive) {
|
|
184
|
+
this.#queue.shift();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
logger.debug("ToolChoiceQueue: dropped in-flight directive after tool_choice degradation", {
|
|
188
|
+
label,
|
|
189
|
+
reason,
|
|
190
|
+
});
|
|
191
|
+
return label;
|
|
192
|
+
}
|
|
193
|
+
|
|
171
194
|
/** True if there is an in-flight yield that hasn't been resolved or rejected. */
|
|
172
195
|
get hasInFlight(): boolean {
|
|
173
196
|
return this.#inFlight !== undefined;
|