@gajae-code/coding-agent 0.2.2 → 0.2.3
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 +28 -0
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/commands/deep-interview.d.ts +41 -0
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +36 -0
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +18 -0
- package/dist/types/hooks/skill-state.d.ts +5 -0
- package/dist/types/memories/index.d.ts +1 -1
- package/dist/types/memory-backend/local-backend.d.ts +3 -3
- package/dist/types/modes/components/hook-selector.d.ts +7 -0
- package/dist/types/modes/components/settings-selector.d.ts +0 -2
- package/dist/types/modes/utils/context-usage.d.ts +6 -2
- package/dist/types/sdk.d.ts +6 -2
- package/dist/types/session/agent-session.d.ts +45 -1
- package/dist/types/session/session-manager.d.ts +3 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -0
- package/dist/types/setup/provider-onboarding.d.ts +29 -5
- package/dist/types/skill-state/active-state.d.ts +26 -1
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/initial-phase.d.ts +12 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/types.d.ts +11 -0
- package/dist/types/tools/index.d.ts +20 -1
- package/dist/types/tools/skill.d.ts +47 -0
- package/dist/types/utils/changelog.d.ts +18 -2
- package/package.json +7 -7
- package/src/cli/setup-cli.ts +26 -12
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +25 -2
- package/src/commands/setup.ts +2 -0
- package/src/commands/state.ts +1 -0
- package/src/config/settings-schema.ts +41 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +19 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -0
- package/src/defaults/gjc/skills/team/SKILL.md +10 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +10 -0
- package/src/discovery/helpers.ts +24 -1
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +268 -1
- package/src/gjc-runtime/state-runtime.ts +173 -4
- package/src/hooks/skill-state.ts +8 -6
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/internal-urls/memory-protocol.ts +3 -2
- package/src/main.ts +2 -3
- package/src/memories/index.ts +2 -1
- package/src/memory-backend/local-backend.ts +14 -6
- package/src/modes/components/hook-selector.ts +156 -1
- package/src/modes/components/settings-selector.ts +5 -12
- package/src/modes/controllers/command-controller.ts +2 -3
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +4 -11
- package/src/modes/utils/context-usage.ts +66 -17
- package/src/prompts/agents/architect.md +3 -0
- package/src/prompts/agents/executor.md +2 -0
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/system/subagent-system-prompt.md +6 -0
- package/src/prompts/tools/skill.md +28 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/sdk.ts +50 -10
- package/src/session/agent-session.ts +204 -21
- package/src/session/session-manager.ts +9 -1
- package/src/setup/model-onboarding-guidance.ts +6 -3
- package/src/setup/provider-onboarding.ts +177 -16
- package/src/skill-state/active-state.ts +150 -25
- package/src/skill-state/deep-interview-mutation-guard.ts +11 -24
- package/src/skill-state/initial-phase.ts +17 -0
- package/src/slash-commands/builtin-registry.ts +51 -13
- package/src/slash-commands/helpers/context-report.ts +123 -13
- package/src/task/agents.ts +1 -0
- package/src/task/executor.ts +9 -1
- package/src/task/index.ts +91 -4
- package/src/task/types.ts +6 -0
- package/src/tools/ask.ts +2 -0
- package/src/tools/index.ts +23 -1
- package/src/tools/skill.ts +153 -0
- package/src/utils/changelog.ts +67 -44
package/src/sdk.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type CredentialDisabledEvent,
|
|
13
13
|
type Message,
|
|
14
14
|
type Model,
|
|
15
|
+
type ProviderSessionState,
|
|
15
16
|
type SimpleStreamOptions,
|
|
16
17
|
streamSimple,
|
|
17
18
|
} from "@gajae-code/ai";
|
|
@@ -82,7 +83,7 @@ import {
|
|
|
82
83
|
obfuscateMessages,
|
|
83
84
|
SecretObfuscator,
|
|
84
85
|
} from "./secrets";
|
|
85
|
-
import { AgentSession } from "./session/agent-session";
|
|
86
|
+
import { AgentSession, type ForkContextSeed } from "./session/agent-session";
|
|
86
87
|
import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
|
|
87
88
|
import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "./session/auth-storage";
|
|
88
89
|
import { type CustomMessage, convertToLlm } from "./session/messages";
|
|
@@ -320,6 +321,10 @@ export interface CreateAgentSessionOptions {
|
|
|
320
321
|
* `@opentelemetry/api` package returns a no-op tracer in that case.
|
|
321
322
|
*/
|
|
322
323
|
telemetry?: AgentTelemetryConfig;
|
|
324
|
+
/** Optional fork-context seed used to initialize a child session before its first prompt. */
|
|
325
|
+
forkContextSeed?: ForkContextSeed;
|
|
326
|
+
/** Optional provider state override. Fork-context children should omit this by default. */
|
|
327
|
+
providerSessionState?: Map<string, ProviderSessionState>;
|
|
323
328
|
}
|
|
324
329
|
|
|
325
330
|
/** Result from createAgentSession */
|
|
@@ -861,7 +866,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
861
866
|
logger.time("sessionManager", () =>
|
|
862
867
|
SessionManager.create(cwd, SessionManager.getDefaultSessionDir(cwd, agentDir)),
|
|
863
868
|
);
|
|
864
|
-
const
|
|
869
|
+
const logicalSessionId = sessionManager.getSessionId();
|
|
870
|
+
const providerSessionId = options.providerSessionId ?? options.forkContextSeed?.cacheIdentity ?? logicalSessionId;
|
|
865
871
|
const modelApiKeyAvailability = new Map<string, boolean>();
|
|
866
872
|
const getModelAvailabilityKey = (candidate: Model): string =>
|
|
867
873
|
`${candidate.provider}\u0000${candidate.baseUrl ?? ""}`;
|
|
@@ -1153,7 +1159,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1153
1159
|
trackEvalExecution: (execution, abortController) =>
|
|
1154
1160
|
session ? session.trackEvalExecution(execution, abortController) : execution,
|
|
1155
1161
|
getSessionId: () => sessionManager.getSessionId?.() ?? null,
|
|
1162
|
+
getActiveSkillState: () => session?.getActiveSkillState(),
|
|
1163
|
+
getActiveSkillPhase: () => session?.getActiveSkillPhase(),
|
|
1156
1164
|
getHindsightSessionState: () => session?.getHindsightSessionState(),
|
|
1165
|
+
get model() {
|
|
1166
|
+
return agent?.state.model ?? model;
|
|
1167
|
+
},
|
|
1157
1168
|
getAgentId: () => resolvedAgentId,
|
|
1158
1169
|
getToolByName: name => session?.getToolByName(name),
|
|
1159
1170
|
agentRegistry,
|
|
@@ -1195,6 +1206,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1195
1206
|
attribution: "agent",
|
|
1196
1207
|
timestamp: Date.now(),
|
|
1197
1208
|
}),
|
|
1209
|
+
sendCustomMessage: (msg, opts) => session.sendCustomMessage(msg, opts),
|
|
1198
1210
|
peekQueueInvoker: () => session.peekQueueInvoker(),
|
|
1199
1211
|
peekStandingResolveHandler: () => session.peekStandingResolveHandler(),
|
|
1200
1212
|
setStandingResolveHandler: handler => session.setStandingResolveHandler(handler),
|
|
@@ -1210,6 +1222,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1210
1222
|
authStorage,
|
|
1211
1223
|
modelRegistry,
|
|
1212
1224
|
getTelemetry: () => agent?.telemetry,
|
|
1225
|
+
buildForkContextSeed: forkOptions => session.buildForkContextSeed(forkOptions),
|
|
1213
1226
|
};
|
|
1214
1227
|
|
|
1215
1228
|
// Wire process-wide internal URL singletons owned by their real classes.
|
|
@@ -1726,17 +1739,43 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1726
1739
|
? undefined
|
|
1727
1740
|
: serviceTierSetting;
|
|
1728
1741
|
|
|
1742
|
+
const appendOnlyContext =
|
|
1743
|
+
model && resolveAppendOnlyMode(settings.get("provider.appendOnlyContext"), model.provider)
|
|
1744
|
+
? new AppendOnlyContextManager()
|
|
1745
|
+
: undefined;
|
|
1746
|
+
if (appendOnlyContext && options.forkContextSeed && !hasExistingSession) {
|
|
1747
|
+
if (options.forkContextSeed.appendOnlyPrefixSnapshot) {
|
|
1748
|
+
(
|
|
1749
|
+
appendOnlyContext.prefix as typeof appendOnlyContext.prefix & {
|
|
1750
|
+
importSnapshot(
|
|
1751
|
+
snapshot: NonNullable<ForkContextSeed["appendOnlyPrefixSnapshot"]>,
|
|
1752
|
+
options: { intentTracing: boolean },
|
|
1753
|
+
): void;
|
|
1754
|
+
}
|
|
1755
|
+
).importSnapshot(options.forkContextSeed.appendOnlyPrefixSnapshot, { intentTracing: !!intentField });
|
|
1756
|
+
}
|
|
1757
|
+
(
|
|
1758
|
+
appendOnlyContext as AppendOnlyContextManager & {
|
|
1759
|
+
seedNormalizedMessages(messages: readonly Message[]): void;
|
|
1760
|
+
}
|
|
1761
|
+
).seedNormalizedMessages(options.forkContextSeed.messages);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1729
1764
|
agent = new Agent({
|
|
1730
1765
|
initialState: {
|
|
1731
1766
|
systemPrompt,
|
|
1732
1767
|
model,
|
|
1733
1768
|
thinkingLevel: toReasoningEffort(thinkingLevel),
|
|
1734
1769
|
tools: initialTools,
|
|
1770
|
+
...(options.forkContextSeed && !hasExistingSession
|
|
1771
|
+
? { messages: options.forkContextSeed.agentMessages }
|
|
1772
|
+
: {}),
|
|
1735
1773
|
},
|
|
1736
1774
|
convertToLlm: convertToLlmFinal,
|
|
1737
1775
|
onPayload,
|
|
1738
1776
|
onResponse,
|
|
1739
|
-
sessionId:
|
|
1777
|
+
sessionId: logicalSessionId,
|
|
1778
|
+
providerSessionId,
|
|
1740
1779
|
transformContext,
|
|
1741
1780
|
steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
|
|
1742
1781
|
followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
|
|
@@ -1756,13 +1795,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1756
1795
|
getApiKey: async provider => {
|
|
1757
1796
|
// Read agent.sessionId at call time so credential selection stays aligned
|
|
1758
1797
|
// with metadataResolver after /new, fork, resume, or branch switches.
|
|
1759
|
-
const key = await modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
|
|
1798
|
+
const key = await modelRegistry.getApiKeyForProvider(provider, agent.providerSessionId ?? agent.sessionId);
|
|
1760
1799
|
if (!key) {
|
|
1761
1800
|
throw new Error(`No API key found for provider "${provider}"`);
|
|
1762
1801
|
}
|
|
1763
1802
|
return key;
|
|
1764
1803
|
},
|
|
1765
|
-
getAuthCredentialType: provider =>
|
|
1804
|
+
getAuthCredentialType: provider =>
|
|
1805
|
+
modelRegistry.getSessionCredentialType(provider, agent.providerSessionId ?? agent.sessionId),
|
|
1766
1806
|
streamFn: (streamModel, context, streamOptions) =>
|
|
1767
1807
|
streamSimple(streamModel, context, {
|
|
1768
1808
|
...streamOptions,
|
|
@@ -1793,11 +1833,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1793
1833
|
intentTracing: !!intentField,
|
|
1794
1834
|
getToolChoice: () => session?.nextToolChoice(),
|
|
1795
1835
|
telemetry: options.telemetry,
|
|
1796
|
-
appendOnlyContext
|
|
1797
|
-
? resolveAppendOnlyMode(settings.get("provider.appendOnlyContext"), model.provider)
|
|
1798
|
-
? new AppendOnlyContextManager()
|
|
1799
|
-
: undefined
|
|
1800
|
-
: undefined,
|
|
1836
|
+
appendOnlyContext,
|
|
1801
1837
|
});
|
|
1802
1838
|
|
|
1803
1839
|
cursorEventEmitter = event => agent.emitExternalEvent(event);
|
|
@@ -1836,6 +1872,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1836
1872
|
skillWarnings,
|
|
1837
1873
|
skillsSettings: settings.getGroup("skills"),
|
|
1838
1874
|
modelRegistry,
|
|
1875
|
+
taskDepth,
|
|
1839
1876
|
toolRegistry,
|
|
1840
1877
|
transformContext,
|
|
1841
1878
|
onPayload,
|
|
@@ -1868,6 +1905,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1868
1905
|
agentId: resolvedAgentId,
|
|
1869
1906
|
agentRegistry,
|
|
1870
1907
|
providerSessionId: options.providerSessionId,
|
|
1908
|
+
providerCacheSessionId: providerSessionId,
|
|
1909
|
+
forkContextSeed: options.forkContextSeed,
|
|
1910
|
+
providerSessionState: options.providerSessionState,
|
|
1871
1911
|
});
|
|
1872
1912
|
hasSession = true;
|
|
1873
1913
|
if (asyncJobManager) {
|
|
@@ -28,8 +28,10 @@ import {
|
|
|
28
28
|
type AgentTool,
|
|
29
29
|
AppendOnlyContextManager,
|
|
30
30
|
resolveTelemetry,
|
|
31
|
+
type StablePrefixSnapshot,
|
|
31
32
|
ThinkingLevel,
|
|
32
33
|
} from "@gajae-code/agent-core";
|
|
34
|
+
import { normalizeMessagesForProvider } from "@gajae-code/agent-core/agent-loop";
|
|
33
35
|
import {
|
|
34
36
|
AUTO_HANDOFF_THRESHOLD_FOCUS,
|
|
35
37
|
CompactionCancelledError,
|
|
@@ -75,6 +77,33 @@ import {
|
|
|
75
77
|
resolveServiceTier,
|
|
76
78
|
streamSimple,
|
|
77
79
|
} from "@gajae-code/ai";
|
|
80
|
+
|
|
81
|
+
export interface ForkContextSeedMetadata {
|
|
82
|
+
sourceSessionId: string;
|
|
83
|
+
parentMessageCount: number;
|
|
84
|
+
includedMessages: number;
|
|
85
|
+
skippedMessages: number;
|
|
86
|
+
approximateTokens: number;
|
|
87
|
+
maxMessages: number;
|
|
88
|
+
maxTokens: number;
|
|
89
|
+
skippedReasons: Record<string, number>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ForkContextSeed {
|
|
93
|
+
messages: Message[];
|
|
94
|
+
agentMessages: AgentMessage[];
|
|
95
|
+
metadata: ForkContextSeedMetadata;
|
|
96
|
+
cacheIdentity?: string;
|
|
97
|
+
appendOnlyPrefixSnapshot?: StablePrefixSnapshot;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ForkContextSeedOptions {
|
|
101
|
+
maxMessages: number;
|
|
102
|
+
maxTokens: number;
|
|
103
|
+
cacheIdentity?: string;
|
|
104
|
+
signal?: AbortSignal;
|
|
105
|
+
}
|
|
106
|
+
|
|
78
107
|
import { MacOSPowerAssertion } from "@gajae-code/natives";
|
|
79
108
|
import {
|
|
80
109
|
extractRetryHint,
|
|
@@ -168,7 +197,7 @@ import {
|
|
|
168
197
|
} from "../runtime-mcp/discoverable-tool-metadata";
|
|
169
198
|
import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
|
|
170
199
|
import { formatNoCredentialOnboardingError, formatNoModelOnboardingError } from "../setup/model-onboarding-guidance";
|
|
171
|
-
import { isCanonicalGjcWorkflowSkill
|
|
200
|
+
import { isCanonicalGjcWorkflowSkill } from "../skill-state/active-state";
|
|
172
201
|
import { assertDeepInterviewMutationAllowed } from "../skill-state/deep-interview-mutation-guard";
|
|
173
202
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
174
203
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
@@ -246,6 +275,13 @@ export type AgentSessionEvent =
|
|
|
246
275
|
| { type: "thinking_level_changed"; thinkingLevel: ThinkingLevel | undefined }
|
|
247
276
|
| { type: "goal_updated"; goal: Goal | null; state?: GoalModeState };
|
|
248
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Safe path component pattern used to validate session-id segments before
|
|
280
|
+
* joining them into `.gjc/state` paths. Mirrors the regex used by the
|
|
281
|
+
* `gjc state` runtime selector resolver.
|
|
282
|
+
*/
|
|
283
|
+
const SAFE_PATH_COMPONENT = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
|
|
284
|
+
|
|
249
285
|
/** Listener function for agent session events */
|
|
250
286
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
251
287
|
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
|
|
@@ -283,6 +319,8 @@ export interface AgentSessionConfig {
|
|
|
283
319
|
skillsSettings?: SkillsSettings;
|
|
284
320
|
/** Model registry for API key resolution and model discovery */
|
|
285
321
|
modelRegistry: ModelRegistry;
|
|
322
|
+
/** Task recursion depth for nested sessions. Top-level sessions use 0. */
|
|
323
|
+
taskDepth?: number;
|
|
286
324
|
/** Tool registry for LSP and settings */
|
|
287
325
|
toolRegistry?: Map<string, AgentTool>;
|
|
288
326
|
/** Current session pre-LLM message transform pipeline */
|
|
@@ -331,6 +369,10 @@ export interface AgentSessionConfig {
|
|
|
331
369
|
* **MUST NOT** dispose it on their own teardown.
|
|
332
370
|
*/
|
|
333
371
|
ownedAsyncJobManager?: AsyncJobManager;
|
|
372
|
+
/** Optional fork-context seed used to initialize a child session before its first prompt. */
|
|
373
|
+
forkContextSeed?: ForkContextSeed;
|
|
374
|
+
/** Optional provider state override. Fork-context children should omit this by default. */
|
|
375
|
+
providerSessionState?: Map<string, ProviderSessionState>;
|
|
334
376
|
/** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */
|
|
335
377
|
agentId?: string;
|
|
336
378
|
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
@@ -342,6 +384,8 @@ export interface AgentSessionConfig {
|
|
|
342
384
|
* so that credential sticky selection is consistent with the session's streaming calls.
|
|
343
385
|
*/
|
|
344
386
|
providerSessionId?: string;
|
|
387
|
+
/** Optional provider-facing cache identity, distinct from logical session identity. */
|
|
388
|
+
providerCacheSessionId?: string;
|
|
345
389
|
}
|
|
346
390
|
|
|
347
391
|
/** Options for AgentSession.prompt() */
|
|
@@ -749,6 +793,7 @@ export class AgentSession {
|
|
|
749
793
|
readonly agent: Agent;
|
|
750
794
|
readonly sessionManager: SessionManager;
|
|
751
795
|
readonly settings: Settings;
|
|
796
|
+
readonly taskDepth: number;
|
|
752
797
|
readonly yieldQueue: YieldQueue;
|
|
753
798
|
|
|
754
799
|
#powerAssertion: MacOSPowerAssertion | undefined;
|
|
@@ -836,6 +881,7 @@ export class AgentSession {
|
|
|
836
881
|
#agentId: string | undefined;
|
|
837
882
|
#agentRegistry: AgentRegistry | undefined;
|
|
838
883
|
#providerSessionId: string | undefined;
|
|
884
|
+
#providerCacheSessionId: string | undefined;
|
|
839
885
|
#isDisposed = false;
|
|
840
886
|
// Extension system
|
|
841
887
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
@@ -1007,6 +1053,7 @@ export class AgentSession {
|
|
|
1007
1053
|
this.agent = config.agent;
|
|
1008
1054
|
this.sessionManager = config.sessionManager;
|
|
1009
1055
|
this.settings = config.settings;
|
|
1056
|
+
this.taskDepth = config.taskDepth ?? 0;
|
|
1010
1057
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
1011
1058
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
1012
1059
|
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
@@ -1020,6 +1067,9 @@ export class AgentSession {
|
|
|
1020
1067
|
this.#customCommands = config.customCommands ?? [];
|
|
1021
1068
|
this.#skillsSettings = config.skillsSettings;
|
|
1022
1069
|
this.#modelRegistry = config.modelRegistry;
|
|
1070
|
+
if (config.providerSessionState) {
|
|
1071
|
+
this.#providerSessionState = config.providerSessionState;
|
|
1072
|
+
}
|
|
1023
1073
|
this.#validateRetryFallbackChains();
|
|
1024
1074
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
1025
1075
|
this.#requestedToolNames = config.requestedToolNames;
|
|
@@ -1099,6 +1149,7 @@ export class AgentSession {
|
|
|
1099
1149
|
this.#agentId = config.agentId;
|
|
1100
1150
|
this.#agentRegistry = config.agentRegistry;
|
|
1101
1151
|
this.#providerSessionId = config.providerSessionId;
|
|
1152
|
+
this.#providerCacheSessionId = config.providerCacheSessionId;
|
|
1102
1153
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
1103
1154
|
const event: AgentEvent = {
|
|
1104
1155
|
type: "message_update",
|
|
@@ -1196,6 +1247,46 @@ export class AgentSession {
|
|
|
1196
1247
|
return this.#toolChoiceQueue;
|
|
1197
1248
|
}
|
|
1198
1249
|
|
|
1250
|
+
/** Current skill prompt executing in this session, if any. */
|
|
1251
|
+
getActiveSkillState(): { skill: string; session_id?: string } | undefined {
|
|
1252
|
+
if (!this.#activeSkillState) return undefined;
|
|
1253
|
+
return {
|
|
1254
|
+
skill: this.#activeSkillState.skill,
|
|
1255
|
+
...(this.#activeSkillState.sessionId ? { session_id: this.#activeSkillState.sessionId } : {}),
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** Best-effort accessor for the active skill's `current_phase` field from
|
|
1260
|
+
* its persisted mode-state file. Used by the `skill` tool to enforce the
|
|
1261
|
+
* terminal-phase chain guard. Returns undefined when no active skill is
|
|
1262
|
+
* recorded or the mode-state file is missing/unreadable; callers should
|
|
1263
|
+
* treat undefined as a non-terminal phase (refuses to chain). */
|
|
1264
|
+
getActiveSkillPhase(): string | undefined {
|
|
1265
|
+
const active = this.#activeSkillState;
|
|
1266
|
+
if (!active) return undefined;
|
|
1267
|
+
// Path safety: refuse to read mode-state files when the skill or
|
|
1268
|
+
// session-id are not safe path components. The `skill` tool
|
|
1269
|
+
// interprets undefined as a non-terminal phase, so chaining is
|
|
1270
|
+
// refused — there is no risk of bypassing the guard via a custom
|
|
1271
|
+
// skill name with `..` or a session-id with separators.
|
|
1272
|
+
if (!isCanonicalGjcWorkflowSkill(active.skill)) return undefined;
|
|
1273
|
+
if (active.sessionId !== undefined && !SAFE_PATH_COMPONENT.test(active.sessionId)) {
|
|
1274
|
+
return undefined;
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
const stateDir = path.join(this.sessionManager.getCwd(), ".gjc", "state");
|
|
1278
|
+
const segments = active.sessionId
|
|
1279
|
+
? [stateDir, "sessions", encodeURIComponent(active.sessionId).replaceAll(".", "%2E")]
|
|
1280
|
+
: [stateDir];
|
|
1281
|
+
const filePath = path.join(...segments, `${active.skill}-state.json`);
|
|
1282
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
1283
|
+
const parsed = JSON.parse(raw) as { current_phase?: unknown };
|
|
1284
|
+
return typeof parsed.current_phase === "string" ? parsed.current_phase : undefined;
|
|
1285
|
+
} catch {
|
|
1286
|
+
return undefined;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1199
1290
|
/** Peek the in-flight directive's invocation handler for use by the resolve tool. */
|
|
1200
1291
|
peekQueueInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined {
|
|
1201
1292
|
return this.#toolChoiceQueue.peekInFlightInvoker();
|
|
@@ -1219,6 +1310,100 @@ export class AgentSession {
|
|
|
1219
1310
|
return this.#providerSessionState;
|
|
1220
1311
|
}
|
|
1221
1312
|
|
|
1313
|
+
async buildForkContextSeed(options: ForkContextSeedOptions): Promise<ForkContextSeed> {
|
|
1314
|
+
const transformedMessages = await this.#transformContext([...this.messages], options.signal);
|
|
1315
|
+
const convertedMessages = await this.#convertToLlm(transformedMessages);
|
|
1316
|
+
const providerMessages = this.model
|
|
1317
|
+
? normalizeMessagesForProvider(convertedMessages, this.model)
|
|
1318
|
+
: convertedMessages;
|
|
1319
|
+
const maxMessages = Math.min(500, Math.max(0, Math.trunc(options.maxMessages)));
|
|
1320
|
+
const maxTokens = Math.max(0, Math.trunc(options.maxTokens));
|
|
1321
|
+
const selected: Message[] = [];
|
|
1322
|
+
const skippedReasons: Record<string, number> = {};
|
|
1323
|
+
let skippedMessages = 0;
|
|
1324
|
+
let approximateTokens = 0;
|
|
1325
|
+
|
|
1326
|
+
const recordSkip = (reason: string) => {
|
|
1327
|
+
skippedMessages++;
|
|
1328
|
+
skippedReasons[reason] = (skippedReasons[reason] ?? 0) + 1;
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
const sanitizeMessage = (message: Message): Message | undefined => {
|
|
1332
|
+
if (message.role === "developer") {
|
|
1333
|
+
recordSkip("developer-role");
|
|
1334
|
+
return undefined;
|
|
1335
|
+
}
|
|
1336
|
+
if (message.role === "toolResult") {
|
|
1337
|
+
recordSkip("tool-result-role");
|
|
1338
|
+
return undefined;
|
|
1339
|
+
}
|
|
1340
|
+
if (message.role !== "user" && message.role !== "assistant") {
|
|
1341
|
+
recordSkip("unsupported-role");
|
|
1342
|
+
return undefined;
|
|
1343
|
+
}
|
|
1344
|
+
const cloned = structuredClone(message) as Message;
|
|
1345
|
+
if ("providerPayload" in cloned) {
|
|
1346
|
+
delete (cloned as { providerPayload?: unknown }).providerPayload;
|
|
1347
|
+
}
|
|
1348
|
+
if (Array.isArray(cloned.content)) {
|
|
1349
|
+
const sanitizedContent: TextContent[] = [];
|
|
1350
|
+
for (const block of cloned.content) {
|
|
1351
|
+
if (block.type === "text") {
|
|
1352
|
+
sanitizedContent.push(block);
|
|
1353
|
+
} else if (block.type === "image") {
|
|
1354
|
+
sanitizedContent.push({ type: "text", text: "[Image omitted from fork-context seed]" });
|
|
1355
|
+
} else if (block.type !== "thinking") {
|
|
1356
|
+
recordSkip(`unsupported-content-${block.type}`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return { ...cloned, content: sanitizedContent } as Message;
|
|
1360
|
+
}
|
|
1361
|
+
return cloned;
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
for (let i = providerMessages.length - 1; i >= 0; i--) {
|
|
1365
|
+
if (selected.length >= maxMessages) {
|
|
1366
|
+
recordSkip("message-limit");
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
const sanitized = sanitizeMessage(providerMessages[i]!);
|
|
1370
|
+
if (!sanitized) continue;
|
|
1371
|
+
const messageTokens = estimateTokens(sanitized);
|
|
1372
|
+
if (maxTokens > 0 && approximateTokens + messageTokens > maxTokens) {
|
|
1373
|
+
recordSkip("token-limit");
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
selected.unshift(sanitized);
|
|
1377
|
+
approximateTokens += messageTokens;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const messages = selected;
|
|
1381
|
+
let appendOnlyPrefixSnapshot: StablePrefixSnapshot | undefined;
|
|
1382
|
+
const appendOnly = this.agent.appendOnlyContext;
|
|
1383
|
+
if (appendOnly) {
|
|
1384
|
+
if (!appendOnly.prefix.built) {
|
|
1385
|
+
appendOnly.prefix.build(this.agent.state, { intentTracing: this.agent.intentTracing });
|
|
1386
|
+
}
|
|
1387
|
+
appendOnlyPrefixSnapshot = appendOnly.prefix.exportSnapshot() ?? undefined;
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
messages,
|
|
1391
|
+
agentMessages: messages.map(message => structuredClone(message) as AgentMessage),
|
|
1392
|
+
metadata: {
|
|
1393
|
+
sourceSessionId: this.sessionId,
|
|
1394
|
+
parentMessageCount: providerMessages.length,
|
|
1395
|
+
includedMessages: messages.length,
|
|
1396
|
+
skippedMessages,
|
|
1397
|
+
approximateTokens,
|
|
1398
|
+
maxMessages,
|
|
1399
|
+
maxTokens,
|
|
1400
|
+
skippedReasons,
|
|
1401
|
+
},
|
|
1402
|
+
cacheIdentity: options.cacheIdentity ?? this.sessionId,
|
|
1403
|
+
appendOnlyPrefixSnapshot,
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1222
1407
|
getHindsightSessionState(): HindsightSessionState | undefined {
|
|
1223
1408
|
return this.#hindsightSessionState;
|
|
1224
1409
|
}
|
|
@@ -1555,11 +1740,7 @@ export class AgentSession {
|
|
|
1555
1740
|
}
|
|
1556
1741
|
|
|
1557
1742
|
const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
|
|
1558
|
-
if (
|
|
1559
|
-
!this.#ttsrAbortPending ||
|
|
1560
|
-
this.#promptGeneration !== generation ||
|
|
1561
|
-
targetAssistantIndex === -1
|
|
1562
|
-
) {
|
|
1743
|
+
if (!this.#ttsrAbortPending || this.#promptGeneration !== generation) {
|
|
1563
1744
|
this.#ttsrAbortPending = false;
|
|
1564
1745
|
this.#pendingTtsrInjections = [];
|
|
1565
1746
|
this.#perToolTtsrInjections.clear();
|
|
@@ -1569,8 +1750,8 @@ export class AgentSession {
|
|
|
1569
1750
|
this.#ttsrAbortPending = false;
|
|
1570
1751
|
this.#perToolTtsrInjections.clear();
|
|
1571
1752
|
const ttsrSettings = this.#ttsrManager?.getSettings();
|
|
1572
|
-
if (ttsrSettings?.contextMode === "discard") {
|
|
1573
|
-
// Remove the partial/aborted assistant turn from agent state
|
|
1753
|
+
if (ttsrSettings?.contextMode === "discard" && targetAssistantIndex !== -1) {
|
|
1754
|
+
// Remove the partial/aborted assistant turn from agent state when it was persisted.
|
|
1574
1755
|
this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
|
|
1575
1756
|
}
|
|
1576
1757
|
// Inject TTSR rules as system reminder before retry
|
|
@@ -2757,6 +2938,7 @@ export class AgentSession {
|
|
|
2757
2938
|
#syncAgentSessionId(sessionId?: string): void {
|
|
2758
2939
|
const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
|
2759
2940
|
this.agent.sessionId = sid;
|
|
2941
|
+
this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
|
|
2760
2942
|
this.agent.setMetadataResolver((provider: string) =>
|
|
2761
2943
|
buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
|
|
2762
2944
|
);
|
|
@@ -4092,18 +4274,19 @@ export class AgentSession {
|
|
|
4092
4274
|
const details = message.details;
|
|
4093
4275
|
if (!details || typeof details !== "object") return;
|
|
4094
4276
|
const name = (details as { name?: unknown }).name;
|
|
4095
|
-
if (typeof name !== "string" || !
|
|
4096
|
-
const
|
|
4277
|
+
if (typeof name !== "string" || !name.trim()) return;
|
|
4278
|
+
const skill = name.trim();
|
|
4097
4279
|
const sessionId = this.sessionManager.getSessionId();
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4280
|
+
// Canonical GJC workflow skills (deep-interview, ralplan, ultragoal, team)
|
|
4281
|
+
// own their `.gjc/state/skill-active-state.json` row through the
|
|
4282
|
+
// `gjc state handoff` and `gjc state clear` runtime verbs. The prompt
|
|
4283
|
+
// observer here used to overwrite the row with `phase: running` and
|
|
4284
|
+
// later remove it with `active:false`, which clobbered handoff lineage
|
|
4285
|
+
// (`handoff_from`/`handoff_at`) and made the HUD inconsistent with
|
|
4286
|
+
// mode-state. The observational filesystem write is now skipped for
|
|
4287
|
+
// canonical skills; the in-memory `#activeSkillState` tracking below
|
|
4288
|
+
// keeps `getActiveSkillState` accurate for the chain guard.
|
|
4289
|
+
this.#activeSkillState = active ? { skill, sessionId } : undefined;
|
|
4107
4290
|
}
|
|
4108
4291
|
|
|
4109
4292
|
async #syncSkillPromptActiveStateSafely(
|
|
@@ -6147,7 +6330,7 @@ export class AgentSession {
|
|
|
6147
6330
|
|
|
6148
6331
|
#closeCodexProviderSessionsForHistoryRewrite(): void {
|
|
6149
6332
|
const currentModel = this.model;
|
|
6150
|
-
if (
|
|
6333
|
+
if (currentModel?.api !== "openai-codex-responses") return;
|
|
6151
6334
|
this.#closeProviderSessionsForModelSwitch(currentModel, currentModel);
|
|
6152
6335
|
}
|
|
6153
6336
|
|
|
@@ -8228,7 +8411,7 @@ export class AgentSession {
|
|
|
8228
8411
|
const previousSessionFile = this.sessionFile;
|
|
8229
8412
|
const selectedEntry = this.sessionManager.getEntry(entryId);
|
|
8230
8413
|
|
|
8231
|
-
if (
|
|
8414
|
+
if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
|
|
8232
8415
|
throw new Error("Invalid entry ID for branching");
|
|
8233
8416
|
}
|
|
8234
8417
|
|
|
@@ -176,6 +176,8 @@ export interface SessionInitEntry extends SessionEntryBase {
|
|
|
176
176
|
tools: string[];
|
|
177
177
|
/** Output schema if structured output was requested */
|
|
178
178
|
outputSchema?: unknown;
|
|
179
|
+
/** Fork-context seed metadata for subagent debugging/replay. */
|
|
180
|
+
forkContext?: unknown;
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
/** Mode change entry - tracks agent mode transitions (e.g. plan mode). */
|
|
@@ -2714,7 +2716,13 @@ export class SessionManager {
|
|
|
2714
2716
|
}
|
|
2715
2717
|
|
|
2716
2718
|
/** Append session init metadata (for subagent debugging/replay). Returns entry id. */
|
|
2717
|
-
appendSessionInit(init: {
|
|
2719
|
+
appendSessionInit(init: {
|
|
2720
|
+
systemPrompt: string;
|
|
2721
|
+
task: string;
|
|
2722
|
+
tools: string[];
|
|
2723
|
+
outputSchema?: unknown;
|
|
2724
|
+
forkContext?: unknown;
|
|
2725
|
+
}): string {
|
|
2718
2726
|
const entry: SessionInitEntry = {
|
|
2719
2727
|
type: "session_init",
|
|
2720
2728
|
id: generateId(this.#byId),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const MODEL_ONBOARDING_API_PROVIDER_COMMAND =
|
|
2
2
|
"/provider add --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <model>";
|
|
3
|
+
export const MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND = "/provider add --preset <minimax|minimax-cn|glm>";
|
|
3
4
|
|
|
4
5
|
export const MODEL_ONBOARDING_SETUP_COMMAND = "gjc setup provider";
|
|
5
6
|
export const MODEL_ONBOARDING_OAUTH_COMMAND = "/provider login [provider-id] or /login [provider-id]";
|
|
@@ -9,14 +10,15 @@ export function formatModelOnboardingGuidance(): string {
|
|
|
9
10
|
"Model selection only shows configured providers.",
|
|
10
11
|
"Assignment targets are DEFAULT plus the GJC role agents: EXECUTOR, ARCHITECT, PLANNER, and CRITIC.",
|
|
11
12
|
"Legacy model-role aliases are compatibility-only and are not shown as assignment targets.",
|
|
12
|
-
`
|
|
13
|
+
`Provider presets: ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND} --preset <preset>).`,
|
|
14
|
+
`API-compatible custom providers: ${MODEL_ONBOARDING_API_PROVIDER_COMMAND}.`,
|
|
13
15
|
`OAuth/subscription providers: ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
|
|
14
16
|
"Then run /model to select a configured model or assign it to a target.",
|
|
15
17
|
].join("\n");
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function formatModelOnboardingInlineHint(): string {
|
|
19
|
-
return `Add API
|
|
21
|
+
return `Add MiniMax/GLM presets with ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND}; custom API providers with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}); OAuth/subscription with ${MODEL_ONBOARDING_OAUTH_COMMAND}; then run /model for DEFAULT, EXECUTOR, ARCHITECT, PLANNER, and CRITIC.`;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export function formatNoModelOnboardingError(): string {
|
|
@@ -27,7 +29,8 @@ export function formatNoCredentialOnboardingError(providerId: string): string {
|
|
|
27
29
|
return [
|
|
28
30
|
`No credentials found for ${providerId}.`,
|
|
29
31
|
"",
|
|
30
|
-
`For
|
|
32
|
+
`For MiniMax/GLM presets, configure credentials with ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND} --preset <preset>).`,
|
|
33
|
+
`For custom API-compatible providers, use ${MODEL_ONBOARDING_API_PROVIDER_COMMAND}.`,
|
|
31
34
|
`For OAuth/subscription providers, use ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
|
|
32
35
|
"Then run /model to select a configured model or assign it to DEFAULT, EXECUTOR, ARCHITECT, PLANNER, or CRITIC.",
|
|
33
36
|
].join("\n");
|