@gajae-code/coding-agent 0.13.0 → 0.13.1
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 +18 -0
- package/dist/types/config/file-lock.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +17 -5
- package/dist/types/config/settings.d.ts +10 -1
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/eval/py/kernel.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/runtime/process-lifecycle.d.ts +12 -31
- package/dist/types/sdk/broker/identity.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
- package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
- package/dist/types/sdk/host/query/cursor.d.ts +4 -0
- package/dist/types/sdk/host/query/handlers.d.ts +6 -0
- package/dist/types/sdk/host/session-runtime.d.ts +7 -6
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/turn-result.d.ts +37 -0
- package/dist/types/session/agent-session.d.ts +8 -11
- package/dist/types/session/blob-store.d.ts +6 -3
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
- package/dist/types/task/executor.d.ts +4 -7
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/utils/edit-mode.d.ts +69 -0
- package/package.json +8 -8
- package/scripts/generate-hotkeys-docs.ts +8 -6
- package/src/commands/sdk.ts +23 -2
- package/src/config/file-lock.ts +25 -5
- package/src/config/keybindings.ts +4 -2
- package/src/config/model-registry.ts +20 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +29 -22
- package/src/config/settings.ts +42 -10
- package/src/coordinator-mcp/server.ts +84 -106
- package/src/edit/index.ts +5 -19
- package/src/edit/renderer.ts +3 -2
- package/src/edit/streaming.ts +28 -15
- package/src/eval/py/executor.ts +108 -10
- package/src/eval/py/kernel.ts +86 -12
- package/src/eval/py/runtime.ts +200 -16
- package/src/exec/bash-executor.ts +36 -37
- package/src/gjc-runtime/gc-runtime.ts +25 -3
- package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
- package/src/gjc-runtime/session-state-sidecar.ts +110 -4
- package/src/gjc-runtime/tmux-sessions.ts +34 -5
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/modes/acp/acp-agent.ts +10 -2
- package/src/modes/components/model-selector.ts +28 -8
- package/src/modes/controllers/plan-mode-controller.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/runtime/process-lifecycle.ts +126 -27
- package/src/runtime-mcp/transports/stdio.ts +11 -6
- package/src/sdk/acp/adapter.ts +18 -10
- package/src/sdk/broker/broker.ts +55 -4
- package/src/sdk/broker/ensure.ts +10 -7
- package/src/sdk/broker/identity.ts +10 -0
- package/src/sdk/broker/lifecycle-ledger.ts +56 -6
- package/src/sdk/broker/lifecycle.ts +53 -10
- package/src/sdk/broker/session-index.ts +28 -11
- package/src/sdk/broker/startup-failure.ts +101 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
- package/src/sdk/bus/conversation-store.ts +171 -50
- package/src/sdk/bus/index.ts +101 -47
- package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
- package/src/sdk/bus/notification-service.ts +3 -3
- package/src/sdk/bus/reconciliation-store.ts +16 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -2
- package/src/sdk/bus/telegram-daemon.ts +131 -29
- package/src/sdk/bus/topic-registry.ts +38 -9
- package/src/sdk/cli/session-cli.ts +20 -7
- package/src/sdk/client/discovery.ts +23 -2
- package/src/sdk/host/query/cursor.ts +4 -0
- package/src/sdk/host/query/handlers.ts +99 -100
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +98 -1
- package/src/sdk/host/session-runtime.ts +57 -15
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
- package/src/sdk/host/websocket-transport.ts +54 -30
- package/src/sdk/index.ts +1 -0
- package/src/sdk/mcp/server.ts +19 -6
- package/src/sdk/protocol/operation-inventory.generated.json +1 -1
- package/src/sdk/protocol/operation-registry.ts +8 -12
- package/src/sdk/session.ts +5 -14
- package/src/sdk/turn-result.ts +53 -0
- package/src/session/agent-session.ts +112 -69
- package/src/session/blob-store.ts +91 -21
- package/src/session/session-manager.ts +408 -81
- package/src/session/session-storage.ts +68 -10
- package/src/slash-commands/helpers/fast-status-report.ts +24 -8
- package/src/task/executor.ts +25 -8
- package/src/task/index.ts +0 -2
- package/src/tools/index.ts +1 -1
- package/src/tools/read.ts +12 -11
- package/src/tools/search.ts +7 -7
- package/src/utils/edit-mode.ts +180 -9
- package/src/web/search/providers/insane.ts +55 -1
|
@@ -748,14 +748,47 @@ function fsyncDirectorySync(pathname: string): void {
|
|
|
748
748
|
|
|
749
749
|
/** Best-effort identity-checked removal of one of our own staged temp names. */
|
|
750
750
|
function unlinkOwnedStagedSync(stagingPath: string, expected: { dev: bigint; ino: bigint }): void {
|
|
751
|
+
let fd: number | undefined;
|
|
751
752
|
try {
|
|
752
753
|
const named = fs.lstatSync(stagingPath, { bigint: true });
|
|
753
754
|
if (!named.isFile() || named.isSymbolicLink() || named.dev !== expected.dev || named.ino !== expected.ino) return;
|
|
754
|
-
|
|
755
|
-
|
|
755
|
+
fd = fs.openSync(stagingPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
756
|
+
const opened = fs.fstatSync(fd, { bigint: true });
|
|
757
|
+
if (
|
|
758
|
+
!opened.isFile() ||
|
|
759
|
+
opened.dev !== named.dev ||
|
|
760
|
+
opened.ino !== named.ino ||
|
|
761
|
+
opened.nlink !== named.nlink ||
|
|
762
|
+
opened.size !== named.size ||
|
|
763
|
+
opened.mtimeNs !== named.mtimeNs ||
|
|
764
|
+
opened.ctimeNs !== named.ctimeNs
|
|
765
|
+
)
|
|
766
|
+
return;
|
|
767
|
+
const hash = createHash("sha256");
|
|
768
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
769
|
+
let offset = 0;
|
|
770
|
+
while (offset < Number(opened.size)) {
|
|
771
|
+
const length = Math.min(buffer.byteLength, Number(opened.size) - offset);
|
|
772
|
+
const count = fs.readSync(fd, buffer, 0, length, offset);
|
|
773
|
+
if (count === 0) return;
|
|
774
|
+
hash.update(buffer.subarray(0, count));
|
|
775
|
+
offset += count;
|
|
776
|
+
}
|
|
777
|
+
const after = fs.fstatSync(fd, { bigint: true });
|
|
778
|
+
if (
|
|
779
|
+
after.dev !== opened.dev ||
|
|
780
|
+
after.ino !== opened.ino ||
|
|
781
|
+
after.nlink !== opened.nlink ||
|
|
782
|
+
after.size !== opened.size ||
|
|
783
|
+
after.mtimeNs !== opened.mtimeNs ||
|
|
784
|
+
after.ctimeNs !== opened.ctimeNs
|
|
785
|
+
)
|
|
786
|
+
return;
|
|
787
|
+
exactRemoveSessionStorageLockPath(stagingPath, after, hash.digest("hex"), after.nlink > 1n);
|
|
756
788
|
} catch {
|
|
757
789
|
// ENOENT means the name was already consumed or removed; cleanup is best-effort.
|
|
758
|
-
|
|
790
|
+
} finally {
|
|
791
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
759
792
|
}
|
|
760
793
|
}
|
|
761
794
|
|
|
@@ -1601,16 +1634,16 @@ function parseSessionStorageLockOwner(bytes: Buffer): SessionStorageLockOwner |
|
|
|
1601
1634
|
}
|
|
1602
1635
|
}
|
|
1603
1636
|
|
|
1604
|
-
function
|
|
1637
|
+
function exactRemoveSessionStorageLockPathResult(
|
|
1605
1638
|
lockPath: string,
|
|
1606
1639
|
expected: fs.BigIntStats,
|
|
1607
1640
|
sha256: string,
|
|
1608
1641
|
allowHardLink = false,
|
|
1609
|
-
):
|
|
1642
|
+
): native.NativeExactUnlinkResult {
|
|
1610
1643
|
try {
|
|
1611
1644
|
const parent = fs.lstatSync(path.dirname(lockPath), { bigint: true });
|
|
1612
|
-
if (!parent.isDirectory() || parent.isSymbolicLink()) return false;
|
|
1613
|
-
|
|
1645
|
+
if (!parent.isDirectory() || parent.isSymbolicLink()) return { ok: false, code: "parent_mismatch" };
|
|
1646
|
+
return nativeSessionStorage().exactUnlink(lockPath, {
|
|
1614
1647
|
dev: expected.dev,
|
|
1615
1648
|
ino: expected.ino,
|
|
1616
1649
|
nlink: expected.nlink,
|
|
@@ -1622,12 +1655,27 @@ function exactRemoveSessionStorageLockPath(
|
|
|
1622
1655
|
quarantineName: `${path.basename(lockPath)}.reap-${randomUUID()}`,
|
|
1623
1656
|
allowHardLink,
|
|
1624
1657
|
});
|
|
1625
|
-
return result.ok || (result.code === "cleanup_pending" && typeof result.detachedPath === "string");
|
|
1626
1658
|
} catch {
|
|
1627
|
-
return false;
|
|
1659
|
+
return { ok: false, code: "native_failure" };
|
|
1628
1660
|
}
|
|
1629
1661
|
}
|
|
1630
1662
|
|
|
1663
|
+
function exactRemoveSessionStorageLockPath(
|
|
1664
|
+
lockPath: string,
|
|
1665
|
+
expected: fs.BigIntStats,
|
|
1666
|
+
sha256: string,
|
|
1667
|
+
allowHardLink = false,
|
|
1668
|
+
): boolean {
|
|
1669
|
+
const result = exactRemoveSessionStorageLockPathResult(lockPath, expected, sha256, allowHardLink);
|
|
1670
|
+
return (
|
|
1671
|
+
result.ok ||
|
|
1672
|
+
(result.code === "cleanup_pending" &&
|
|
1673
|
+
result.payloadDurable === true &&
|
|
1674
|
+
!result.retainedSuccessorPath &&
|
|
1675
|
+
!result.retainedUnknownPath)
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1631
1679
|
function exactRemoveOwnedSessionStorageLockPath(
|
|
1632
1680
|
lockPath: string,
|
|
1633
1681
|
expected: Pick<fs.BigIntStats, "dev" | "ino">,
|
|
@@ -1836,7 +1884,17 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1836
1884
|
return {
|
|
1837
1885
|
releaseSync: () => {
|
|
1838
1886
|
if (released) return;
|
|
1839
|
-
|
|
1887
|
+
const removal = exactRemoveSessionStorageLockPathResult(lockPath, expected, ownerDigest!);
|
|
1888
|
+
if (!removal.ok) {
|
|
1889
|
+
if (
|
|
1890
|
+
removal.code === "cleanup_pending" &&
|
|
1891
|
+
removal.payloadDurable === true &&
|
|
1892
|
+
!removal.retainedSuccessorPath &&
|
|
1893
|
+
!removal.retainedUnknownPath
|
|
1894
|
+
) {
|
|
1895
|
+
released = true;
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1840
1898
|
try {
|
|
1841
1899
|
const current = fs.lstatSync(lockPath, { bigint: true });
|
|
1842
1900
|
if (current.dev === expected.dev && current.ino === expected.ino) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Model, modelSupportsServiceTier, modelsAreEqual } from "@gajae-code/ai/core";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A single line in the `/fast status` report: a labelled model and whether fast
|
|
@@ -55,7 +55,7 @@ export function formatFastStatusReport(args: FormatFastStatusReportArgs): string
|
|
|
55
55
|
export interface FastStatusSessionLike {
|
|
56
56
|
readonly model?: Model;
|
|
57
57
|
/** Fast predicate against the main session tier (current model + `modelRoles`). */
|
|
58
|
-
isFastForProvider(provider?: string): boolean;
|
|
58
|
+
isFastForProvider(provider?: string, supportsServiceTier?: boolean): boolean;
|
|
59
59
|
/**
|
|
60
60
|
* Current-model EFFECTIVE fast state (intent minus any provider auto-disable).
|
|
61
61
|
* Used for the current-model row so it matches what the next request does.
|
|
@@ -63,8 +63,13 @@ export interface FastStatusSessionLike {
|
|
|
63
63
|
*/
|
|
64
64
|
isFastModeActive?(): boolean;
|
|
65
65
|
/** Fast predicate against the effective subagent tier (`task.agentModelOverrides` roles). */
|
|
66
|
-
isFastForSubagentProvider(provider?: string): boolean;
|
|
66
|
+
isFastForSubagentProvider(provider?: string, supportsServiceTier?: boolean): boolean;
|
|
67
67
|
resolveRoleModelWithThinking(role: string): { model?: Model };
|
|
68
|
+
/** Runtime fallback position for the configured DEFAULT chain. */
|
|
69
|
+
getDefaultFallbackRuntimeState?(): {
|
|
70
|
+
chain: { entries: readonly string[] };
|
|
71
|
+
controller: { activeIndex: number };
|
|
72
|
+
};
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
/** A role to enumerate in the report, with the tier source its subagent runs under. */
|
|
@@ -106,15 +111,26 @@ export function buildFastStatusReport(args: BuildFastStatusReportArgs): string {
|
|
|
106
111
|
// on pure intent. Fall back to intent when a fake omits `isFastModeActive`.
|
|
107
112
|
const currentFast = session.isFastModeActive
|
|
108
113
|
? session.isFastModeActive()
|
|
109
|
-
: session.isFastForProvider(session.model?.provider);
|
|
114
|
+
: session.isFastForProvider(session.model?.provider, modelSupportsServiceTier(session.model));
|
|
110
115
|
const rows: FastStatusRow[] = [{ label: "현재 모델", model: session.model, fast: currentFast }];
|
|
111
116
|
for (const target of roleTargets) {
|
|
112
117
|
const resolved = session.resolveRoleModelWithThinking(target.id);
|
|
113
|
-
|
|
118
|
+
const fallbackState = target.id === "default" ? session.getDefaultFallbackRuntimeState?.() : undefined;
|
|
119
|
+
const activeDefaultFallback =
|
|
120
|
+
fallbackState !== undefined &&
|
|
121
|
+
fallbackState.chain.entries.length > 1 &&
|
|
122
|
+
fallbackState.controller.activeIndex > 0 &&
|
|
123
|
+
fallbackState.controller.activeIndex < fallbackState.chain.entries.length;
|
|
124
|
+
const rowModel = activeDefaultFallback && session.model ? session.model : resolved.model;
|
|
125
|
+
if (rowModel) {
|
|
126
|
+
const supportsServiceTier = modelSupportsServiceTier(rowModel);
|
|
127
|
+
const isCurrentModel = session.model !== undefined && modelsAreEqual(session.model, rowModel);
|
|
114
128
|
const fast = target.isSubagentRole
|
|
115
|
-
? session.isFastForSubagentProvider(
|
|
116
|
-
:
|
|
117
|
-
|
|
129
|
+
? session.isFastForSubagentProvider(rowModel.provider, supportsServiceTier)
|
|
130
|
+
: isCurrentModel
|
|
131
|
+
? currentFast
|
|
132
|
+
: session.isFastForProvider(rowModel.provider, supportsServiceTier);
|
|
133
|
+
rows.push({ label: target.label, model: rowModel, fast });
|
|
118
134
|
}
|
|
119
135
|
}
|
|
120
136
|
return formatFastStatusReport({ rows, iconFast, formatInactive });
|
package/src/task/executor.ts
CHANGED
|
@@ -15,7 +15,14 @@ import type {
|
|
|
15
15
|
} from "@gajae-code/agent-core";
|
|
16
16
|
import { recordHandoff, resolveTelemetry } from "@gajae-code/agent-core";
|
|
17
17
|
import { estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
|
|
18
|
-
import
|
|
18
|
+
import {
|
|
19
|
+
type AssistantMessage,
|
|
20
|
+
isFastModeEffectiveForProvider,
|
|
21
|
+
type Message,
|
|
22
|
+
type Model,
|
|
23
|
+
modelSupportsServiceTier,
|
|
24
|
+
type ServiceTier,
|
|
25
|
+
} from "@gajae-code/ai/core";
|
|
19
26
|
import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@gajae-code/ai/utils/schema";
|
|
20
27
|
import { logger, prompt, untilAborted } from "@gajae-code/utils";
|
|
21
28
|
import { AsyncJobManager } from "../async";
|
|
@@ -248,14 +255,11 @@ export interface ExecutorOptions {
|
|
|
248
255
|
/** Parent session's registry; shared by child sessions for IRC routing and roster visibility. */
|
|
249
256
|
agentRegistry?: AgentRegistry;
|
|
250
257
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* subagents, and a main-model fast-mode auto-disable does not clobber it.
|
|
258
|
+
* Parent service-tier intent captured when the child is spawned. Used when
|
|
259
|
+
* `task.serviceTier === "inherit"` so request serialization and reporting use
|
|
260
|
+
* the same immutable child settings snapshot.
|
|
255
261
|
*/
|
|
256
262
|
inheritedServiceTier?: ServiceTier;
|
|
257
|
-
/** Resolve whether the effective subagent tier grants fast mode for the selected provider. */
|
|
258
|
-
isFastForSubagentProvider?: (provider?: string) => boolean;
|
|
259
263
|
/** Override local:// protocol options so subagent shares parent's local:// root */
|
|
260
264
|
localProtocolOptions?: LocalProtocolOptions;
|
|
261
265
|
/**
|
|
@@ -865,6 +869,11 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
865
869
|
|
|
866
870
|
const settings = options.settings ?? Settings.isolated();
|
|
867
871
|
const subagentSettings = createSubagentSettings(settings, options.inheritedServiceTier);
|
|
872
|
+
const configuredSubagentServiceTier = subagentSettings.get("serviceTier");
|
|
873
|
+
const subagentServiceTier = configuredSubagentServiceTier === "none" ? undefined : configuredSubagentServiceTier;
|
|
874
|
+
const isFastForModel = (candidate: Model | undefined): boolean =>
|
|
875
|
+
candidate !== undefined &&
|
|
876
|
+
isFastModeEffectiveForProvider(subagentServiceTier, candidate.provider, modelSupportsServiceTier(candidate));
|
|
868
877
|
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
869
878
|
const maxRuntimeMs = Math.max(0, Math.trunc(Number(settings.get("task.maxRuntimeMs") ?? 0) || 0));
|
|
870
879
|
const parentDepth = options.taskDepth ?? 0;
|
|
@@ -1533,7 +1542,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1533
1542
|
resolvedModelString = formatModelString(model);
|
|
1534
1543
|
activeProviderModelString = resolvedModelString;
|
|
1535
1544
|
}
|
|
1536
|
-
progress.fastMode =
|
|
1545
|
+
progress.fastMode = isFastForModel(model);
|
|
1537
1546
|
if (authFallbackUsed && model && requestedModel) {
|
|
1538
1547
|
modelSubstitutionWarning = {
|
|
1539
1548
|
requested: formatModelString(requestedModel),
|
|
@@ -1954,6 +1963,14 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1954
1963
|
}
|
|
1955
1964
|
if (event.type === "model_fallback_switched") {
|
|
1956
1965
|
activeProviderModelString = event.to;
|
|
1966
|
+
progress.fastMode = isFastForModel(session.model);
|
|
1967
|
+
AsyncJobManager.instance()?.updateSubagentModel?.(options.subagentId ?? id, {
|
|
1968
|
+
requestedModel: modelSubstitutionWarning?.requested ?? resolvedModelString,
|
|
1969
|
+
effectiveModel: event.to,
|
|
1970
|
+
modelFellBack: true,
|
|
1971
|
+
fastMode: progress.fastMode,
|
|
1972
|
+
});
|
|
1973
|
+
scheduleProgress(true);
|
|
1957
1974
|
forwardSubagentEvent(event);
|
|
1958
1975
|
return;
|
|
1959
1976
|
}
|
package/src/task/index.ts
CHANGED
|
@@ -2027,7 +2027,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
2027
2027
|
agentRegistry: this.session.agentRegistry,
|
|
2028
2028
|
settings: this.session.settings,
|
|
2029
2029
|
inheritedServiceTier: this.session.serviceTier,
|
|
2030
|
-
isFastForSubagentProvider: provider => this.session.isFastForSubagentProvider?.(provider) ?? false,
|
|
2031
2030
|
contextFiles,
|
|
2032
2031
|
skills: availableSkills,
|
|
2033
2032
|
autoloadSkills: resolvedAutoloadSkills,
|
|
@@ -2105,7 +2104,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
2105
2104
|
agentRegistry: this.session.agentRegistry,
|
|
2106
2105
|
settings: this.session.settings,
|
|
2107
2106
|
inheritedServiceTier: this.session.serviceTier,
|
|
2108
|
-
isFastForSubagentProvider: provider => this.session.isFastForSubagentProvider?.(provider) ?? false,
|
|
2109
2107
|
contextFiles,
|
|
2110
2108
|
skills: availableSkills,
|
|
2111
2109
|
autoloadSkills: resolvedAutoloadSkills,
|
package/src/tools/index.ts
CHANGED
|
@@ -282,7 +282,7 @@ export interface ToolSession {
|
|
|
282
282
|
/** Live service-tier intent of the parent session, inherited by `inherit` subagents. */
|
|
283
283
|
serviceTier?: ServiceTier;
|
|
284
284
|
/** Whether the effective subagent tier grants fast mode for a resolved provider. */
|
|
285
|
-
isFastForSubagentProvider?: (provider?: string) => boolean;
|
|
285
|
+
isFastForSubagentProvider?: (provider?: string, supportsServiceTier?: boolean) => boolean;
|
|
286
286
|
/** Plan mode state (if active) */
|
|
287
287
|
getPlanModeState?: () => PlanModeState | undefined;
|
|
288
288
|
/** Goal mode state (if active or paused) */
|
package/src/tools/read.ts
CHANGED
|
@@ -1447,7 +1447,18 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1447
1447
|
readonly name = "read";
|
|
1448
1448
|
readonly label = "Read";
|
|
1449
1449
|
readonly loadMode = "essential";
|
|
1450
|
-
|
|
1450
|
+
get description(): string {
|
|
1451
|
+
const displayMode = resolveFileDisplayMode(this.session);
|
|
1452
|
+
return prompt.render(readDescription, {
|
|
1453
|
+
DEFAULT_LIMIT: String(this.#defaultLimit),
|
|
1454
|
+
DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
|
|
1455
|
+
TRUNCATION_DEFAULT: String(this.session.settings.get("read.truncation") ?? "last"),
|
|
1456
|
+
RECEIPT_LINES: String(this.session.settings.get("read.receiptBudgetLines")),
|
|
1457
|
+
RECEIPT_KIB: String(this.session.settings.get("read.receiptBudgetBytes")),
|
|
1458
|
+
IS_HL_MODE: displayMode.hashLines,
|
|
1459
|
+
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1451
1462
|
readonly parameters = readSchema;
|
|
1452
1463
|
readonly nonAbortable = true;
|
|
1453
1464
|
readonly strict = true;
|
|
@@ -1457,21 +1468,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1457
1468
|
readonly #defaultLimit: number;
|
|
1458
1469
|
|
|
1459
1470
|
constructor(private readonly session: ToolSession) {
|
|
1460
|
-
const displayMode = resolveFileDisplayMode(session);
|
|
1461
1471
|
this.#autoResizeImages = session.settings.get("images.autoResize");
|
|
1462
1472
|
this.#defaultLimit = Math.max(
|
|
1463
1473
|
1,
|
|
1464
1474
|
Math.min(session.settings.get("read.defaultLimit") ?? DEFAULT_MAX_LINES, DEFAULT_MAX_LINES),
|
|
1465
1475
|
);
|
|
1466
|
-
this.description = prompt.render(readDescription, {
|
|
1467
|
-
DEFAULT_LIMIT: String(this.#defaultLimit),
|
|
1468
|
-
DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
|
|
1469
|
-
TRUNCATION_DEFAULT: String(this.session.settings.get("read.truncation") ?? "last"),
|
|
1470
|
-
RECEIPT_LINES: String(this.session.settings.get("read.receiptBudgetLines")),
|
|
1471
|
-
RECEIPT_KIB: String(this.session.settings.get("read.receiptBudgetBytes")),
|
|
1472
|
-
IS_HL_MODE: displayMode.hashLines,
|
|
1473
|
-
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
|
|
1474
|
-
});
|
|
1475
1476
|
}
|
|
1476
1477
|
|
|
1477
1478
|
async #resolveArchiveReadPath(readPath: string, signal?: AbortSignal): Promise<ResolvedArchiveReadPath | null> {
|
package/src/tools/search.ts
CHANGED
|
@@ -237,17 +237,17 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
237
237
|
readonly label = "Search";
|
|
238
238
|
readonly loadMode = "discoverable";
|
|
239
239
|
readonly summary = "Search file contents using ripgrep (fast text search)";
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
constructor(private readonly session: ToolSession) {
|
|
245
|
-
const displayMode = resolveFileDisplayMode(session);
|
|
246
|
-
this.description = prompt.render(searchDescription, {
|
|
240
|
+
get description(): string {
|
|
241
|
+
const displayMode = resolveFileDisplayMode(this.session);
|
|
242
|
+
return prompt.render(searchDescription, {
|
|
247
243
|
IS_HL_MODE: displayMode.hashLines,
|
|
248
244
|
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
|
|
249
245
|
});
|
|
250
246
|
}
|
|
247
|
+
readonly parameters = searchSchema;
|
|
248
|
+
readonly strict = true;
|
|
249
|
+
|
|
250
|
+
constructor(private readonly session: ToolSession) {}
|
|
251
251
|
|
|
252
252
|
async execute(
|
|
253
253
|
_toolCallId: string,
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -2,8 +2,15 @@ import { $pickenv } from "@gajae-code/utils/env";
|
|
|
2
2
|
|
|
3
3
|
export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch";
|
|
4
4
|
|
|
5
|
+
/** Setting value for `edit.mode`: an executable mode or `auto` (model-family routing). */
|
|
6
|
+
export type EditModeSetting = "auto" | EditMode;
|
|
7
|
+
|
|
8
|
+
/** Fallback executable mode used when automatic routing cannot identify the model. */
|
|
5
9
|
export const DEFAULT_EDIT_MODE: EditMode = "hashline";
|
|
6
10
|
|
|
11
|
+
/** Default `edit.mode` setting value. */
|
|
12
|
+
export const DEFAULT_EDIT_MODE_SETTING: EditModeSetting = "auto";
|
|
13
|
+
|
|
7
14
|
const EDIT_MODE_IDS = {
|
|
8
15
|
apply_patch: "apply_patch",
|
|
9
16
|
hashline: "hashline",
|
|
@@ -14,29 +21,193 @@ const EDIT_MODE_IDS = {
|
|
|
14
21
|
|
|
15
22
|
export const EDIT_MODES = Object.keys(EDIT_MODE_IDS) as EditMode[];
|
|
16
23
|
|
|
24
|
+
export const EDIT_MODE_SETTINGS: EditModeSetting[] = ["auto", ...EDIT_MODES];
|
|
25
|
+
|
|
17
26
|
export function normalizeEditMode(mode?: string | null): EditMode | undefined {
|
|
18
27
|
if (!mode) return undefined;
|
|
19
|
-
return EDIT_MODE_IDS[mode as keyof typeof EDIT_MODE_IDS];
|
|
28
|
+
return Object.hasOwn(EDIT_MODE_IDS, mode) ? EDIT_MODE_IDS[mode as keyof typeof EDIT_MODE_IDS] : undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Model-family detection (automatic routing)
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export type ModelEditFamily = "gpt" | "codex" | "claude" | "deepseek" | "qwen" | "minimax" | "glm" | "kimi" | "unknown";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Built-in family → edit mode mapping used by `edit.mode: auto`.
|
|
39
|
+
*
|
|
40
|
+
* GPT/Codex models are trained on the OpenAI patch grammar; Claude, DeepSeek,
|
|
41
|
+
* and Qwen perform best with exact search/replace; MiniMax, GLM, and
|
|
42
|
+
* Kimi/Moonshot benchmark best with hash-anchored edits. Unknown models fall
|
|
43
|
+
* back to hashline, the safest fail-closed default.
|
|
44
|
+
*/
|
|
45
|
+
const FAMILY_EDIT_MODES: Record<Exclude<ModelEditFamily, "unknown">, EditMode> = {
|
|
46
|
+
gpt: "apply_patch",
|
|
47
|
+
codex: "apply_patch",
|
|
48
|
+
claude: "replace",
|
|
49
|
+
deepseek: "replace",
|
|
50
|
+
qwen: "replace",
|
|
51
|
+
minimax: "hashline",
|
|
52
|
+
glm: "hashline",
|
|
53
|
+
kimi: "hashline",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect the model family from a model ID, ignoring the provider prefix so
|
|
58
|
+
* equivalent models served through different providers route identically
|
|
59
|
+
* (`openai/gpt-5.4`, `openrouter/openai/gpt-5.4`, `company-gateway/gpt-5.4`).
|
|
60
|
+
*
|
|
61
|
+
* Matching only examines the final path segment. A direct family prefix or a
|
|
62
|
+
* known provider-qualified family name is accepted, while arbitrary token
|
|
63
|
+
* substrings return `unknown` instead of guessing.
|
|
64
|
+
*/
|
|
65
|
+
export function detectModelEditFamily(modelId: string | undefined): ModelEditFamily {
|
|
66
|
+
if (!modelId) return "unknown";
|
|
67
|
+
const normalized = modelId.trim().toLowerCase();
|
|
68
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
69
|
+
const model = segments.at(-1) ?? normalized;
|
|
70
|
+
|
|
71
|
+
if (/^codex(?:$|[-_.\d])|^(?:gpt|(?:duo-chat|openai)-gpt)-\d+(?:[-_.][\w]+)*[-_.]codex(?:$|[-_.\d])/.test(model))
|
|
72
|
+
return "codex";
|
|
73
|
+
if (
|
|
74
|
+
/^(?:gpt-\d+(?:$|[-_.])|gpt-oss(?:$|[-_.\d])|chatgpt(?:$|[-_.]\d))|(?:^|\.)openai\.(?:gpt|chatgpt)(?:$|[-_.\d])/.test(
|
|
75
|
+
model,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
return "gpt";
|
|
79
|
+
if (/^claude(?:$|[-_.\d])|(?:^|\.)anthropic\.claude(?:$|[-_.\d])/.test(model)) return "claude";
|
|
80
|
+
if (/^deepseek(?:$|[-_.\d])|(?:^|\.)deepseek(?:$|[-_.\d])/.test(model)) return "deepseek";
|
|
81
|
+
if (/^qwen(?:$|[-_.\d])|(?:^|\.)qwen\.qwen(?:$|[-_.\d])/.test(model)) return "qwen";
|
|
82
|
+
if (/^minimax(?:$|[-_.\d])/.test(model)) return "minimax";
|
|
83
|
+
if (/^glm(?:$|[-_.\d])/.test(model)) return "glm";
|
|
84
|
+
if (/^(?:kimi|moonshot)(?:$|[-_.\d])/.test(model)) return "kimi";
|
|
85
|
+
|
|
86
|
+
return "unknown";
|
|
20
87
|
}
|
|
21
88
|
|
|
89
|
+
/** Built-in mode for a detected family; `undefined` for `unknown`. */
|
|
90
|
+
export function builtinEditModeForFamily(family: ModelEditFamily): EditMode | undefined {
|
|
91
|
+
if (family === "unknown") return undefined;
|
|
92
|
+
return FAMILY_EDIT_MODES[family];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
// Resolution
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
export type EditModeSource = "environment" | "model-override" | "setting" | "catalog" | "builtin-family" | "fallback";
|
|
100
|
+
|
|
101
|
+
export type ResolvedEditModeDetails = {
|
|
102
|
+
mode: EditMode;
|
|
103
|
+
source: EditModeSource;
|
|
104
|
+
modelId?: string;
|
|
105
|
+
family?: ModelEditFamily;
|
|
106
|
+
matchedRule?: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Raw `edit.modelVariants` match: pattern plus the configured (unvalidated) value. */
|
|
110
|
+
export type EditVariantMatch = {
|
|
111
|
+
pattern: string;
|
|
112
|
+
value: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
22
115
|
export interface EditModeSettingsLike {
|
|
23
116
|
get(key: "edit.mode"): unknown;
|
|
117
|
+
/** Legacy accessor: first *valid* matching `edit.modelVariants` value. */
|
|
24
118
|
getEditVariantForModel?(model: string | undefined): EditMode | null;
|
|
119
|
+
/**
|
|
120
|
+
* Discriminated accessor: first matching `edit.modelVariants` rule with its
|
|
121
|
+
* raw value. Lets the resolver fail closed on invalid matched values
|
|
122
|
+
* instead of silently falling through to another mode.
|
|
123
|
+
*/
|
|
124
|
+
matchEditVariantForModel?(model: string | undefined): EditVariantMatch | null;
|
|
25
125
|
}
|
|
26
126
|
|
|
27
127
|
export interface EditModeSessionLike {
|
|
28
128
|
settings: EditModeSettingsLike;
|
|
29
129
|
getActiveModelString?: () => string | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* Optional model-catalog edit recommendation; beats the built-in family
|
|
132
|
+
* mapping when present.
|
|
133
|
+
*
|
|
134
|
+
* Extension point: production sessions do not wire this yet — the model
|
|
135
|
+
* catalog does not expose an edit-protocol recommendation (`applyPatchToolType`
|
|
136
|
+
* is the OpenAI wire representation of an already-selected `apply_patch`
|
|
137
|
+
* mode, not a mode selector). Catalog metadata generation is deliberately
|
|
138
|
+
* deferred until the family-based path is proven for arbitrary custom-provider
|
|
139
|
+
* IDs; when it lands, sessions can expose this hook without a resolver change.
|
|
140
|
+
*/
|
|
141
|
+
getCatalogEditMode?: () => EditMode | undefined;
|
|
30
142
|
}
|
|
31
143
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Read the forced edit mode from `GJC_EDIT_VARIANT`/`PI_EDIT_VARIANT`.
|
|
146
|
+
* The environment force is the emergency kill switch and beats every other
|
|
147
|
+
* source. Invalid values fail fast; empty and `auto` mean "not forced".
|
|
148
|
+
*/
|
|
149
|
+
export function resolveForcedEnvEditMode(): EditMode | undefined {
|
|
150
|
+
const raw = $pickenv("GJC_EDIT_VARIANT", "PI_EDIT_VARIANT");
|
|
151
|
+
if (!raw || raw === "auto") return undefined;
|
|
152
|
+
const mode = normalizeEditMode(raw);
|
|
153
|
+
if (!mode) {
|
|
154
|
+
throw new Error(`Invalid GJC_EDIT_VARIANT: ${raw}`);
|
|
155
|
+
}
|
|
156
|
+
return mode;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the active edit mode with provenance.
|
|
161
|
+
*
|
|
162
|
+
* Precedence:
|
|
163
|
+
* 1. `GJC_EDIT_VARIANT`/`PI_EDIT_VARIANT` environment force (invalid → throw).
|
|
164
|
+
* 2. Matching user `edit.modelVariants` rule (matched invalid → throw).
|
|
165
|
+
* 3. Explicit `edit.mode` setting when it is not `auto`.
|
|
166
|
+
* 4. Model-catalog edit recommendation, when the session exposes one.
|
|
167
|
+
* 5. Built-in model-family mapping.
|
|
168
|
+
* 6. `hashline` fallback.
|
|
169
|
+
*/
|
|
170
|
+
export function resolveEditModeDetails(session: EditModeSessionLike): ResolvedEditModeDetails {
|
|
171
|
+
const modelId = session.getActiveModelString?.();
|
|
172
|
+
|
|
173
|
+
const envMode = resolveForcedEnvEditMode();
|
|
174
|
+
if (envMode) return { mode: envMode, source: "environment", modelId };
|
|
175
|
+
|
|
176
|
+
const settings = session.settings;
|
|
177
|
+
if (settings.matchEditVariantForModel) {
|
|
178
|
+
const match = settings.matchEditVariantForModel(modelId);
|
|
179
|
+
if (match) {
|
|
180
|
+
const mode = normalizeEditMode(match.value);
|
|
181
|
+
if (!mode) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Invalid edit.modelVariants value "${match.value}" for pattern "${match.pattern}" (expected one of: ${EDIT_MODES.join(", ")})`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return { mode, source: "model-override", modelId, matchedRule: match.pattern };
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
const legacyVariant = settings.getEditVariantForModel?.(modelId);
|
|
190
|
+
if (legacyVariant) return { mode: legacyVariant, source: "model-override", modelId };
|
|
191
|
+
}
|
|
36
192
|
|
|
37
|
-
const
|
|
38
|
-
|
|
193
|
+
const rawSetting = settings.get("edit.mode");
|
|
194
|
+
const settingValue = typeof rawSetting === "string" ? rawSetting : undefined;
|
|
195
|
+
if (settingValue && settingValue !== "auto") {
|
|
196
|
+
const settingsMode = normalizeEditMode(settingValue);
|
|
197
|
+
if (settingsMode) return { mode: settingsMode, source: "setting", modelId };
|
|
198
|
+
}
|
|
39
199
|
|
|
40
|
-
const
|
|
41
|
-
|
|
200
|
+
const family = detectModelEditFamily(modelId);
|
|
201
|
+
|
|
202
|
+
const catalogMode = session.getCatalogEditMode?.();
|
|
203
|
+
if (catalogMode) return { mode: catalogMode, source: "catalog", modelId, family };
|
|
204
|
+
|
|
205
|
+
const builtinMode = builtinEditModeForFamily(family);
|
|
206
|
+
if (builtinMode) return { mode: builtinMode, source: "builtin-family", modelId, family };
|
|
207
|
+
|
|
208
|
+
return { mode: DEFAULT_EDIT_MODE, source: "fallback", modelId, family };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function resolveEditMode(session: EditModeSessionLike): EditMode {
|
|
212
|
+
return resolveEditModeDetails(session).mode;
|
|
42
213
|
}
|
|
@@ -25,6 +25,7 @@ const DEFAULT_NUM_RESULTS = 10;
|
|
|
25
25
|
const MAX_NUM_RESULTS = 20;
|
|
26
26
|
const PUBLIC_ROUTE_TIMEOUT_MS = 15_000;
|
|
27
27
|
const DISCOVERY_LIMIT = 8;
|
|
28
|
+
const MAX_PUBLIC_ROUTE_RESPONSE_BYTES = 1024 * 1024;
|
|
28
29
|
|
|
29
30
|
const USER_AGENT = "Gajae-Code insane-search safe-public-routes/1.0 (+https://github.com/Yeachan-Heo/gajae-code)";
|
|
30
31
|
|
|
@@ -146,6 +147,59 @@ function attempt(
|
|
|
146
147
|
return { platform, route, ok, status, bytes: new TextEncoder().encode(body).byteLength, note };
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
class ResponseTooLargeError extends Error {
|
|
151
|
+
override readonly name = "response_too_large";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function cancelBody(body: ReadableStream<Uint8Array> | null): Promise<void> {
|
|
155
|
+
if (!body) return;
|
|
156
|
+
try {
|
|
157
|
+
await body.cancel();
|
|
158
|
+
} catch {
|
|
159
|
+
// Best-effort teardown; preserve the stable size-limit diagnostic.
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function readResponseText(response: Response): Promise<string> {
|
|
164
|
+
const contentLength = response.headers.get("content-length");
|
|
165
|
+
if (contentLength !== null) {
|
|
166
|
+
const declaredBytes = Number.parseInt(contentLength, 10);
|
|
167
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > MAX_PUBLIC_ROUTE_RESPONSE_BYTES) {
|
|
168
|
+
await cancelBody(response.body);
|
|
169
|
+
throw new ResponseTooLargeError();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const reader = response.body?.getReader();
|
|
174
|
+
if (!reader) return "";
|
|
175
|
+
|
|
176
|
+
const decoder = new TextDecoder();
|
|
177
|
+
let totalBytes = 0;
|
|
178
|
+
let text = "";
|
|
179
|
+
try {
|
|
180
|
+
while (true) {
|
|
181
|
+
const { done, value } = await reader.read();
|
|
182
|
+
if (done) break;
|
|
183
|
+
if (!value || value.byteLength === 0) continue;
|
|
184
|
+
|
|
185
|
+
totalBytes += value.byteLength;
|
|
186
|
+
if (totalBytes > MAX_PUBLIC_ROUTE_RESPONSE_BYTES) {
|
|
187
|
+
try {
|
|
188
|
+
await reader.cancel();
|
|
189
|
+
} catch {
|
|
190
|
+
// Best-effort teardown; preserve the stable size-limit diagnostic.
|
|
191
|
+
}
|
|
192
|
+
throw new ResponseTooLargeError();
|
|
193
|
+
}
|
|
194
|
+
text += decoder.decode(value, { stream: true });
|
|
195
|
+
}
|
|
196
|
+
text += decoder.decode();
|
|
197
|
+
return text;
|
|
198
|
+
} finally {
|
|
199
|
+
reader.releaseLock();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
149
203
|
async function fetchText(
|
|
150
204
|
url: string,
|
|
151
205
|
signal?: AbortSignal,
|
|
@@ -158,7 +212,7 @@ async function fetchText(
|
|
|
158
212
|
redirect: "follow",
|
|
159
213
|
signal: withHardTimeout(signal, PUBLIC_ROUTE_TIMEOUT_MS),
|
|
160
214
|
});
|
|
161
|
-
const text = await response
|
|
215
|
+
const text = await readResponseText(response);
|
|
162
216
|
return { status: response.status, text, contentType: response.headers.get("content-type") ?? "" };
|
|
163
217
|
}
|
|
164
218
|
|