@selesai/code 0.13.26 → 0.13.27
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 +74 -0
- package/dist/cli/args.js +1 -0
- package/dist/cli/session-picker.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/core/agent-session-auto-handoff.test.js +2 -1
- package/dist/core/agent-session-length-continuation.test.js +2 -1
- package/dist/core/agent-session.d.ts +58 -7
- package/dist/core/agent-session.js +334 -167
- package/dist/core/bug-report-upload.d.ts +11 -0
- package/dist/core/bug-report-upload.js +21 -0
- package/dist/core/bug-report.d.ts +175 -0
- package/dist/core/bug-report.js +291 -0
- package/dist/core/cache-stats.js +12 -1
- package/dist/core/cache-warmer.d.ts +101 -0
- package/dist/core/cache-warmer.js +341 -0
- package/dist/core/compaction/branch-summarization.js +2 -2
- package/dist/core/compaction/compaction.d.ts +6 -5
- package/dist/core/compaction/compaction.js +49 -42
- package/dist/core/crash-log.d.ts +21 -0
- package/dist/core/crash-log.js +70 -0
- package/dist/core/experimental.d.ts +0 -4
- package/dist/core/experimental.js +0 -4
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/jiti-loader.d.ts +1 -0
- package/dist/core/extensions/jiti-loader.js +3 -0
- package/dist/core/extensions/jiti-static-loader.d.ts +1 -0
- package/dist/core/extensions/jiti-static-loader.js +3 -0
- package/dist/core/extensions/loader.js +39 -49
- package/dist/core/extensions/runner.d.ts +10 -7
- package/dist/core/extensions/runner.js +85 -67
- package/dist/core/extensions/types.d.ts +66 -52
- package/dist/core/extensions/virtual-modules.d.ts +2 -0
- package/dist/core/extensions/virtual-modules.js +39 -0
- package/dist/core/extensions/wrapper.js +1 -20
- package/dist/core/index.d.ts +2 -1
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.js +1 -1
- package/dist/core/messages.js +1 -0
- package/dist/core/model-config.d.ts +101 -20
- package/dist/core/model-config.js +25 -18
- package/dist/core/model-resolver.js +2 -1
- package/dist/core/model-runtime.js +5 -3
- package/dist/core/provider-composer.d.ts +7 -2
- package/dist/core/provider-composer.js +10 -5
- package/dist/core/radius.d.ts +3 -0
- package/dist/core/radius.js +6 -0
- package/dist/core/sdk.js +60 -38
- package/dist/core/session-export.d.ts +6 -2
- package/dist/core/session-export.js +14 -13
- package/dist/core/session-format.test.d.ts +1 -0
- package/dist/core/session-format.test.js +96 -0
- package/dist/core/session-manager.d.ts +29 -6
- package/dist/core/session-manager.js +159 -96
- package/dist/core/settings-manager.d.ts +22 -5
- package/dist/core/settings-manager.js +56 -23
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/system-prompt.d.ts +49 -4
- package/dist/core/system-prompt.js +155 -102
- package/dist/core/system-prompt.test.js +61 -23
- package/dist/core/tools/bash.d.ts +2 -1
- package/dist/core/tools/bash.js +10 -4
- package/dist/core/tools/edit.js +1 -2
- package/dist/core/tools/read.js +1 -2
- package/dist/core/tools/renderers/bash.js +9 -1
- package/dist/core/tools/write.js +1 -2
- package/dist/core/usage-totals.d.ts +1 -1
- package/dist/core/usage-totals.js +5 -1
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +14 -6
- package/dist/extensions/pi-web-agent/src/extension.ts +3 -1
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +7 -3
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +3 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +2 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +4 -2
- package/dist/extensions/pi-web-agent/src/search/tokenin.ts +4 -1
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +2 -1
- package/dist/extensions/pi-web-agent/src/types.ts +4 -0
- package/dist/index.d.ts +3 -2
- package/dist/main.js +13 -9
- package/dist/migrations.js +2 -2
- package/dist/modes/interactive/bug-report.d.ts +15 -0
- package/dist/modes/interactive/bug-report.js +205 -0
- package/dist/modes/interactive/chat-viewport.js +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +12 -5
- package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
- package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
- package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
- package/dist/modes/interactive/components/extension-editor.js +7 -2
- package/dist/modes/interactive/components/extension-input.d.ts +2 -0
- package/dist/modes/interactive/components/extension-input.js +6 -0
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
- package/dist/modes/interactive/components/extension-selector.js +4 -0
- package/dist/modes/interactive/components/footer.js +4 -1
- package/dist/modes/interactive/components/session-selector.d.ts +5 -5
- package/dist/modes/interactive/components/session-selector.js +74 -48
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.js +11 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
- package/dist/modes/interactive/components/status-indicator.d.ts +2 -2
- package/dist/modes/interactive/components/status-indicator.js +7 -7
- package/dist/modes/interactive/components/tool-execution.js +18 -9
- package/dist/modes/interactive/components/tree-selector.js +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts +15 -2
- package/dist/modes/interactive/interactive-mode.js +257 -81
- package/dist/modes/interactive/session-share.d.ts +2 -0
- package/dist/modes/interactive/session-share.js +8 -4
- package/dist/modes/interactive/tui-renderer.js +2 -2
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/utils/clipboard-command.d.ts +6 -0
- package/dist/utils/clipboard-command.js +44 -0
- package/dist/utils/clipboard-image.js +55 -89
- package/dist/utils/clipboard.d.ts +1 -1
- package/dist/utils/clipboard.js +118 -108
- package/dist/utils/wsl.d.ts +2 -0
- package/dist/utils/wsl.js +14 -0
- package/dist/utils/zip.d.ts +6 -0
- package/dist/utils/zip.js +59 -0
- package/package.json +9 -9
- package/dist/utils/clipboard-native.d.ts +0 -10
- package/dist/utils/clipboard-native.js +0 -19
|
@@ -86,7 +86,6 @@ const OpenAICompletionsCompatSchema = Type.Object({
|
|
|
86
86
|
supportsOpenAIGrammarTools: Type.Optional(Type.Boolean()),
|
|
87
87
|
supportsStrictMode: Type.Optional(Type.Boolean()),
|
|
88
88
|
sendSessionAffinityHeaders: Type.Optional(Type.Boolean()),
|
|
89
|
-
deferredToolsMode: Type.Optional(Type.Literal("kimi")),
|
|
90
89
|
sessionAffinityFormat: Type.Optional(Type.Union([Type.Literal("openai"), Type.Literal("openai-nosession"), Type.Literal("openrouter")])),
|
|
91
90
|
supportsLongCacheRetention: Type.Optional(Type.Boolean()),
|
|
92
91
|
vllmPriority: Type.Optional(Type.Number()),
|
|
@@ -97,10 +96,26 @@ const OpenAIResponsesCompatSchema = Type.Object({
|
|
|
97
96
|
supportsLongCacheRetention: Type.Optional(Type.Boolean()),
|
|
98
97
|
supportsStrictMode: Type.Optional(Type.Boolean()),
|
|
99
98
|
supportsOpenAIGrammarTools: Type.Optional(Type.Boolean()),
|
|
100
|
-
supportsAdditionalTools: Type.Optional(Type.Boolean()),
|
|
101
|
-
supportsToolSearch: Type.Optional(Type.Boolean()),
|
|
102
99
|
supportsMaxOutputTokens: Type.Optional(Type.Boolean()),
|
|
103
100
|
});
|
|
101
|
+
const ModelCostRatesSchema = {
|
|
102
|
+
input: Type.Number(),
|
|
103
|
+
output: Type.Number(),
|
|
104
|
+
cacheRead: Type.Number(),
|
|
105
|
+
cacheWrite: Type.Number(),
|
|
106
|
+
};
|
|
107
|
+
const ModelCostTierSchema = Type.Object({
|
|
108
|
+
inputTokensAbove: Type.Number(),
|
|
109
|
+
...ModelCostRatesSchema,
|
|
110
|
+
});
|
|
111
|
+
const ModelCostSchema = Type.Object({
|
|
112
|
+
...ModelCostRatesSchema,
|
|
113
|
+
tiers: Type.Optional(Type.Array(ModelCostTierSchema)),
|
|
114
|
+
});
|
|
115
|
+
const ModelPromptCacheSchema = Type.Object({
|
|
116
|
+
short: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
|
|
117
|
+
long: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
|
|
118
|
+
});
|
|
104
119
|
const AnthropicMessagesCompatSchema = Type.Object({
|
|
105
120
|
supportsEagerToolInputStreaming: Type.Optional(Type.Boolean()),
|
|
106
121
|
supportsLongCacheRetention: Type.Optional(Type.Boolean()),
|
|
@@ -111,27 +126,17 @@ const AnthropicMessagesCompatSchema = Type.Object({
|
|
|
111
126
|
allowEmptySignature: Type.Optional(Type.Boolean()),
|
|
112
127
|
supportsStrictTools: Type.Optional(Type.Boolean()),
|
|
113
128
|
supportsMidConvoEffort: Type.Optional(Type.Boolean()),
|
|
114
|
-
|
|
129
|
+
allowedFallbackModels: Type.Optional(Type.Array(Type.Object({
|
|
130
|
+
provider: Type.String({ minLength: 1 }),
|
|
131
|
+
model: Type.String({ minLength: 1 }),
|
|
132
|
+
cost: ModelCostSchema,
|
|
133
|
+
}), { maxItems: 3 })),
|
|
115
134
|
});
|
|
116
135
|
const ProviderCompatSchema = Type.Union([
|
|
117
136
|
OpenAICompletionsCompatSchema,
|
|
118
137
|
OpenAIResponsesCompatSchema,
|
|
119
138
|
AnthropicMessagesCompatSchema,
|
|
120
139
|
]);
|
|
121
|
-
const ModelCostRatesSchema = {
|
|
122
|
-
input: Type.Number(),
|
|
123
|
-
output: Type.Number(),
|
|
124
|
-
cacheRead: Type.Number(),
|
|
125
|
-
cacheWrite: Type.Number(),
|
|
126
|
-
};
|
|
127
|
-
const ModelCostTierSchema = Type.Object({
|
|
128
|
-
inputTokensAbove: Type.Number(),
|
|
129
|
-
...ModelCostRatesSchema,
|
|
130
|
-
});
|
|
131
|
-
const ModelCostSchema = Type.Object({
|
|
132
|
-
...ModelCostRatesSchema,
|
|
133
|
-
tiers: Type.Optional(Type.Array(ModelCostTierSchema)),
|
|
134
|
-
});
|
|
135
140
|
const ModelDefinitionSchema = Type.Object({
|
|
136
141
|
id: Type.String({ minLength: 1 }),
|
|
137
142
|
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
@@ -141,6 +146,7 @@ const ModelDefinitionSchema = Type.Object({
|
|
|
141
146
|
thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema),
|
|
142
147
|
input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))),
|
|
143
148
|
cost: Type.Optional(ModelCostSchema),
|
|
149
|
+
promptCache: Type.Optional(ModelPromptCacheSchema),
|
|
144
150
|
contextWindow: Type.Optional(Type.Number()),
|
|
145
151
|
maxTokens: Type.Optional(Type.Number()),
|
|
146
152
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
@@ -158,6 +164,7 @@ const ModelOverrideSchema = Type.Object({
|
|
|
158
164
|
cacheWrite: Type.Optional(Type.Number()),
|
|
159
165
|
tiers: Type.Optional(Type.Array(ModelCostTierSchema)),
|
|
160
166
|
})),
|
|
167
|
+
promptCache: Type.Optional(ModelPromptCacheSchema),
|
|
161
168
|
contextWindow: Type.Optional(Type.Number()),
|
|
162
169
|
maxTokens: Type.Optional(Type.Number()),
|
|
163
170
|
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
|
|
@@ -14,7 +14,7 @@ export const defaultModelPerProvider = {
|
|
|
14
14
|
openai: "gpt-5.5",
|
|
15
15
|
"azure-openai-responses": "gpt-5.4",
|
|
16
16
|
"openai-codex": "gpt-5.5",
|
|
17
|
-
radius: "
|
|
17
|
+
radius: "balanced",
|
|
18
18
|
nvidia: "nvidia/nemotron-3-super-120b-a12b",
|
|
19
19
|
deepseek: "deepseek-v4-pro",
|
|
20
20
|
google: "gemini-3.1-pro-preview",
|
|
@@ -39,6 +39,7 @@ export const defaultModelPerProvider = {
|
|
|
39
39
|
opencode: "kimi-k2.6",
|
|
40
40
|
"opencode-go": "kimi-k2.6",
|
|
41
41
|
"kimi-coding": "kimi-for-coding",
|
|
42
|
+
meta: "muse-spark-1.3",
|
|
42
43
|
"cloudflare-workers-ai": "@cf/moonshotai/kimi-k2.6",
|
|
43
44
|
"cloudflare-ai-gateway": "workers-ai/@cf/moonshotai/kimi-k2.6",
|
|
44
45
|
"qwen-token-plan": "qwen3.7-max",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dirname, join } from "node:path";
|
|
2
|
-
import { createModels, lazyStream, ModelsError, } from "@earendil-works/pi-ai";
|
|
2
|
+
import { createModels, lazyStream, ModelsError, normalizeContext, } from "@earendil-works/pi-ai";
|
|
3
3
|
import * as builtinProviderCatalog from "@earendil-works/pi-ai/providers/all";
|
|
4
4
|
import { getAgentDir, getBundledDefaultsDir } from "../config.js";
|
|
5
5
|
import { AuthStorage as DefaultAuthStorage } from "./auth-storage.js";
|
|
@@ -340,18 +340,20 @@ export class ModelRuntime {
|
|
|
340
340
|
};
|
|
341
341
|
}
|
|
342
342
|
stream(model, context, options) {
|
|
343
|
+
const transcript = normalizeContext(context);
|
|
343
344
|
return lazyStream(model, async () => {
|
|
344
345
|
const prepared = await this.prepareRequest(model, options);
|
|
345
|
-
return prepared.provider.stream(prepared.model,
|
|
346
|
+
return prepared.provider.stream(prepared.model, transcript, prepared.options);
|
|
346
347
|
});
|
|
347
348
|
}
|
|
348
349
|
complete(model, context, options) {
|
|
349
350
|
return this.stream(model, context, options).result();
|
|
350
351
|
}
|
|
351
352
|
streamSimple(model, context, options) {
|
|
353
|
+
const transcript = normalizeContext(context);
|
|
352
354
|
return lazyStream(model, async () => {
|
|
353
355
|
const prepared = await this.prepareRequest(model, options);
|
|
354
|
-
return prepared.provider.streamSimple(prepared.model,
|
|
356
|
+
return prepared.provider.streamSimple(prepared.model, transcript, prepared.options);
|
|
355
357
|
});
|
|
356
358
|
}
|
|
357
359
|
completeSimple(model, context, options) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Api, type AssistantMessageEventStream, type
|
|
1
|
+
import { type Api, type AssistantMessageEventStream, type Model, type OAuthCredentials, type OAuthLoginCallbacks, type Provider, type ProviderHeaders, type RefreshModelsContext, type SimpleStreamOptions, type TranscriptContext } from "@earendil-works/pi-ai";
|
|
2
2
|
import type { ModelConfig, ModelsJsonProvider } from "./model-config.ts";
|
|
3
3
|
import { clearConfigValueCache } from "./resolve-config-value.ts";
|
|
4
4
|
export interface ExtensionOAuthConfig {
|
|
@@ -16,7 +16,12 @@ export interface ProviderConfigInput {
|
|
|
16
16
|
baseUrl?: string;
|
|
17
17
|
apiKey?: string;
|
|
18
18
|
api?: Api;
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Declarative custom-provider streaming handler. The context is a normalized transcript:
|
|
21
|
+
* read the prompt and tools from its system messages. Invoke `options.onPayload` before
|
|
22
|
+
* sending and `options.onResponse` after receiving the response.
|
|
23
|
+
*/
|
|
24
|
+
streamSimple?: (model: Model<Api>, context: TranscriptContext, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
|
20
25
|
headers?: Record<string, string>;
|
|
21
26
|
authHeader?: boolean;
|
|
22
27
|
oauth?: ExtensionOAuthConfig;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { lazyStream, } from "@earendil-works/pi-ai";
|
|
2
|
+
import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
2
3
|
import { getApiProvider } from "@earendil-works/pi-ai/compat";
|
|
3
4
|
import { clearConfigValueCache, getConfigValueEnvVarNames, isCommandConfigValue, isConfigValueConfigured, resolveConfigValueOrThrow, resolveHeadersOrThrow, } from "./resolve-config-value.js";
|
|
4
5
|
export const clearApiKeyCache = clearConfigValueCache;
|
|
@@ -321,20 +322,24 @@ export function composeModelProvider(providerId, base, modelConfig, extension) {
|
|
|
321
322
|
throw new Error(`Provider ${providerId}: no authentication method configured.`);
|
|
322
323
|
const supportsBaseApi = (model) => base?.getModels().some((entry) => entry.api === model.api) ?? false;
|
|
323
324
|
const streamWith = (model, context, options, simple) => lazyStream(model, async () => {
|
|
325
|
+
// Provider boundary: the platform's streaming entry points hand providers a
|
|
326
|
+
// branded TranscriptContext. Lift the ordinary request context here, where the
|
|
327
|
+
// fork forwards into a platform adapter.
|
|
328
|
+
const transcript = normalizeContext(context);
|
|
324
329
|
if (extension?.streamSimple && model.api === extension.api) {
|
|
325
|
-
return extension.streamSimple(model,
|
|
330
|
+
return extension.streamSimple(model, transcript, options);
|
|
326
331
|
}
|
|
327
332
|
if (base && supportsBaseApi(model)) {
|
|
328
333
|
return simple
|
|
329
|
-
? base.streamSimple(model,
|
|
330
|
-
: base.stream(model,
|
|
334
|
+
? base.streamSimple(model, transcript, options)
|
|
335
|
+
: base.stream(model, transcript, options);
|
|
331
336
|
}
|
|
332
337
|
const api = getApiProvider(model.api);
|
|
333
338
|
if (!api)
|
|
334
339
|
throw new Error(`No API provider registered for api: ${model.api}`);
|
|
335
340
|
return simple
|
|
336
|
-
? api.streamSimple(model,
|
|
337
|
-
: api.stream(model,
|
|
341
|
+
? api.streamSimple(model, transcript, options)
|
|
342
|
+
: api.stream(model, transcript, options);
|
|
338
343
|
});
|
|
339
344
|
return {
|
|
340
345
|
id: providerId,
|
package/dist/core/radius.d.ts
CHANGED
package/dist/core/radius.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
+
import { DEFAULT_RADIUS_GATEWAY, normalizeRadiusGatewayUrl } from "@earendil-works/pi-ai/providers/radius-config";
|
|
1
2
|
export const RADIUS_PROVIDER_ID = "radius";
|
|
3
|
+
export const ENV_RADIUS_GATEWAY = "PI_RADIUS_GATEWAY";
|
|
4
|
+
/** Radius gateway origin, honoring the `PI_RADIUS_GATEWAY` override. */
|
|
5
|
+
export function getRadiusGatewayUrl() {
|
|
6
|
+
return normalizeRadiusGatewayUrl(process.env[ENV_RADIUS_GATEWAY] ?? DEFAULT_RADIUS_GATEWAY);
|
|
7
|
+
}
|
package/dist/core/sdk.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getAgentDir } from "../config.js";
|
|
|
5
5
|
import { resolvePath } from "../utils/paths.js";
|
|
6
6
|
import { AgentSession } from "./agent-session.js";
|
|
7
7
|
import { formatNoModelsAvailableMessage } from "./auth-guidance.js";
|
|
8
|
+
import { CacheWarmer } from "./cache-warmer.js";
|
|
8
9
|
import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
|
|
9
10
|
import { convertToLlm } from "./messages.js";
|
|
10
11
|
import { findInitialModel } from "./model-resolver.js";
|
|
@@ -179,6 +180,53 @@ export async function createAgentSession(options = {}) {
|
|
|
179
180
|
});
|
|
180
181
|
};
|
|
181
182
|
const extensionRunnerRef = {};
|
|
183
|
+
const cacheWarmer = new CacheWarmer(modelRuntime, sessionManager, () => settingsManager.getCacheWarmingMode(), async (event) => extensionRunnerRef.current?.emitCacheWarmingDecision(event) ?? event.action);
|
|
184
|
+
const buildRequestOptions = (requestModel, options = {}) => {
|
|
185
|
+
const providerRetrySettings = settingsManager.getProviderRetrySettings();
|
|
186
|
+
const httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs();
|
|
187
|
+
const effectiveTimeoutMs = httpIdleTimeoutMs === 0 ? 2147483647 : httpIdleTimeoutMs;
|
|
188
|
+
const headerRunner = extensionRunnerRef.current;
|
|
189
|
+
return {
|
|
190
|
+
...options,
|
|
191
|
+
timeoutMs: options.timeoutMs ?? providerRetrySettings.timeoutMs ?? effectiveTimeoutMs,
|
|
192
|
+
websocketConnectTimeoutMs: options.websocketConnectTimeoutMs ?? settingsManager.getWebSocketConnectTimeoutMs(),
|
|
193
|
+
maxRetries: options.maxRetries ?? providerRetrySettings.maxRetries,
|
|
194
|
+
maxRetryDelayMs: options.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,
|
|
195
|
+
transformHeaders: async (requestHeaders) => {
|
|
196
|
+
const headers = mergeProviderAttributionHeaders(requestModel, settingsManager, options.sessionId, requestHeaders);
|
|
197
|
+
return headerRunner?.hasHandlers("before_provider_headers")
|
|
198
|
+
? headerRunner.emitBeforeProviderHeaders(headers ?? {})
|
|
199
|
+
: (headers ?? {});
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
const cacheContextIsCurrent = (requestModel) => {
|
|
204
|
+
const messages = agent.state.messages;
|
|
205
|
+
return () => {
|
|
206
|
+
const currentModel = agent.state.model;
|
|
207
|
+
const currentMessages = agent.state.messages;
|
|
208
|
+
return (currentModel.provider === requestModel.provider &&
|
|
209
|
+
currentModel.id === requestModel.id &&
|
|
210
|
+
messages.length <= currentMessages.length &&
|
|
211
|
+
messages.every((message, index) => currentMessages[index] === message));
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
const transformProviderPayload = async (payload) => {
|
|
215
|
+
const runner = extensionRunnerRef.current;
|
|
216
|
+
if (!runner?.hasHandlers("before_provider_request"))
|
|
217
|
+
return payload;
|
|
218
|
+
return runner.emitBeforeProviderRequest(payload);
|
|
219
|
+
};
|
|
220
|
+
const handleProviderResponse = async (response) => {
|
|
221
|
+
const runner = extensionRunnerRef.current;
|
|
222
|
+
if (!runner?.hasHandlers("after_provider_response"))
|
|
223
|
+
return;
|
|
224
|
+
await runner.emit({
|
|
225
|
+
type: "after_provider_response",
|
|
226
|
+
status: response.status,
|
|
227
|
+
headers: response.headers,
|
|
228
|
+
});
|
|
229
|
+
};
|
|
182
230
|
agent = new Agent({
|
|
183
231
|
initialState: {
|
|
184
232
|
systemPrompt: "",
|
|
@@ -188,46 +236,19 @@ export async function createAgentSession(options = {}) {
|
|
|
188
236
|
},
|
|
189
237
|
convertToLlm: convertToLlmWithBlockImages,
|
|
190
238
|
streamFn: async (model, context, options) => {
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return modelRuntime.streamSimple(model, context, {
|
|
200
|
-
...options,
|
|
201
|
-
timeoutMs,
|
|
202
|
-
websocketConnectTimeoutMs,
|
|
203
|
-
maxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries,
|
|
204
|
-
maxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs,
|
|
205
|
-
transformHeaders: async (requestHeaders) => {
|
|
206
|
-
const headers = mergeProviderAttributionHeaders(model, settingsManager, options?.sessionId, requestHeaders);
|
|
207
|
-
return headerRunner?.hasHandlers("before_provider_headers")
|
|
208
|
-
? headerRunner.emitBeforeProviderHeaders(headers ?? {})
|
|
209
|
-
: (headers ?? {});
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
},
|
|
213
|
-
onPayload: async (payload, _model) => {
|
|
214
|
-
const runner = extensionRunnerRef.current;
|
|
215
|
-
if (!runner?.hasHandlers("before_provider_request")) {
|
|
216
|
-
return payload;
|
|
217
|
-
}
|
|
218
|
-
return runner.emitBeforeProviderRequest(payload);
|
|
219
|
-
},
|
|
220
|
-
onResponse: async (response, _model) => {
|
|
221
|
-
const runner = extensionRunnerRef.current;
|
|
222
|
-
if (!runner?.hasHandlers("after_provider_response")) {
|
|
223
|
-
return;
|
|
239
|
+
const requestOptions = buildRequestOptions(model, options);
|
|
240
|
+
// Compaction and summaries use their own routing ids; only session requests
|
|
241
|
+
// replace the cache entry, so warming restarts from them. Keep warming while
|
|
242
|
+
// the current transcript still extends the request's prefix. Agent state may
|
|
243
|
+
// shallow-copy the messages array or refresh the model object without changing
|
|
244
|
+
// the provider request, so top-level object identity is not a valid cache key.
|
|
245
|
+
if (options?.sessionId === sessionManager.getSessionId()) {
|
|
246
|
+
cacheWarmer.start({ model, context, options: requestOptions }, cacheContextIsCurrent(model));
|
|
224
247
|
}
|
|
225
|
-
|
|
226
|
-
type: "after_provider_response",
|
|
227
|
-
status: response.status,
|
|
228
|
-
headers: response.headers,
|
|
229
|
-
});
|
|
248
|
+
return modelRuntime.streamSimple(model, context, requestOptions);
|
|
230
249
|
},
|
|
250
|
+
onPayload: transformProviderPayload,
|
|
251
|
+
onResponse: handleProviderResponse,
|
|
231
252
|
sessionId: sessionManager.getSessionId(),
|
|
232
253
|
transformContext: async (messages) => {
|
|
233
254
|
const runner = extensionRunnerRef.current;
|
|
@@ -264,6 +285,7 @@ export async function createAgentSession(options = {}) {
|
|
|
264
285
|
resourceLoader,
|
|
265
286
|
customTools: options.customTools,
|
|
266
287
|
modelRuntime,
|
|
288
|
+
cacheWarmer,
|
|
267
289
|
initialActiveToolNames,
|
|
268
290
|
allowedToolNames,
|
|
269
291
|
excludedToolNames,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
1
|
import { type SessionManager } from "./session-manager.ts";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
type TrailingEntries = (parentId: string | null, timestamp: string) => readonly object[];
|
|
3
|
+
/** Serialize the current branch and optional export-only entries as JSONL. */
|
|
4
|
+
export declare function serializeSessionBranch(sessionManager: SessionManager, createTrailingEntries?: TrailingEntries): string;
|
|
5
|
+
/** Write the current session branch and optional export-only entries as JSONL. */
|
|
6
|
+
export declare function exportSessionToJsonl(sessionManager: SessionManager, outputPath?: string, createTrailingEntries?: TrailingEntries): string;
|
|
7
|
+
export {};
|
|
@@ -2,13 +2,8 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { resolvePath } from "../utils/paths.js";
|
|
4
4
|
import { CURRENT_SESSION_VERSION } from "./session-manager.js";
|
|
5
|
-
/**
|
|
6
|
-
export function
|
|
7
|
-
const filePath = resolvePath(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`, process.cwd());
|
|
8
|
-
const dir = dirname(filePath);
|
|
9
|
-
if (!existsSync(dir)) {
|
|
10
|
-
mkdirSync(dir, { recursive: true });
|
|
11
|
-
}
|
|
5
|
+
/** Serialize the current branch and optional export-only entries as JSONL. */
|
|
6
|
+
export function serializeSessionBranch(sessionManager, createTrailingEntries) {
|
|
12
7
|
const timestamp = new Date().toISOString();
|
|
13
8
|
const header = {
|
|
14
9
|
type: "session",
|
|
@@ -17,15 +12,21 @@ export function exportSessionToJsonl(sessionManager, outputPath, createTrailingE
|
|
|
17
12
|
timestamp,
|
|
18
13
|
cwd: sessionManager.getCwd(),
|
|
19
14
|
};
|
|
20
|
-
const
|
|
15
|
+
const entries = [header];
|
|
21
16
|
let parentId = null;
|
|
22
17
|
for (const entry of sessionManager.getBranch()) {
|
|
23
|
-
|
|
18
|
+
entries.push({ ...entry, parentId });
|
|
24
19
|
parentId = entry.id;
|
|
25
20
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
entries.push(...(createTrailingEntries?.(parentId, timestamp) ?? []));
|
|
22
|
+
return `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
|
|
23
|
+
}
|
|
24
|
+
/** Write the current session branch and optional export-only entries as JSONL. */
|
|
25
|
+
export function exportSessionToJsonl(sessionManager, outputPath, createTrailingEntries) {
|
|
26
|
+
const filePath = resolvePath(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`, process.cwd());
|
|
27
|
+
const dir = dirname(filePath);
|
|
28
|
+
if (!existsSync(dir))
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
writeFileSync(filePath, serializeSessionBranch(sessionManager, createTrailingEntries));
|
|
30
31
|
return filePath;
|
|
31
32
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { buildContextEntries, buildSessionContext, sessionEntryToContextMessages, } from "./session-manager.js";
|
|
3
|
+
const base = (id, parentId) => ({
|
|
4
|
+
id,
|
|
5
|
+
parentId,
|
|
6
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
7
|
+
});
|
|
8
|
+
const user = (id, parentId, text) => ({
|
|
9
|
+
...base(id, parentId),
|
|
10
|
+
type: "message",
|
|
11
|
+
message: { role: "user", content: [{ type: "text", text }], timestamp: 1 },
|
|
12
|
+
});
|
|
13
|
+
const system = (id, parentId, content, sections) => ({
|
|
14
|
+
...base(id, parentId),
|
|
15
|
+
type: "message",
|
|
16
|
+
// Session files are parsed without validation; a null content is the readable-old-entry case.
|
|
17
|
+
message: { role: "system", content: content, sections, timestamp: 1 },
|
|
18
|
+
});
|
|
19
|
+
const usage = (id, parentId) => ({
|
|
20
|
+
...base(id, parentId),
|
|
21
|
+
type: "usage",
|
|
22
|
+
kind: "cache_warm",
|
|
23
|
+
provider: "provider",
|
|
24
|
+
model: "model",
|
|
25
|
+
usage: {
|
|
26
|
+
input: 1,
|
|
27
|
+
output: 1,
|
|
28
|
+
cacheRead: 0,
|
|
29
|
+
cacheWrite: 0,
|
|
30
|
+
totalTokens: 2,
|
|
31
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const compaction = (id, parentId, withSystem) => ({
|
|
35
|
+
...base(id, parentId),
|
|
36
|
+
type: "compaction",
|
|
37
|
+
summary: "summary text",
|
|
38
|
+
firstKeptEntryId: "",
|
|
39
|
+
tokensBefore: 100,
|
|
40
|
+
...(withSystem
|
|
41
|
+
? { systemMessage: { role: "system", content: "prompt state", sections: { rules: "<rules>r</rules>" }, timestamp: 1 } }
|
|
42
|
+
: {}),
|
|
43
|
+
});
|
|
44
|
+
describe("session entries to rebuilt context", () => {
|
|
45
|
+
it("tolerates a system entry whose content is absent", () => {
|
|
46
|
+
expect(sessionEntryToContextMessages(system("s1", null, null))).toEqual([
|
|
47
|
+
{ role: "system", content: "", sections: undefined, timestamp: 1 },
|
|
48
|
+
]);
|
|
49
|
+
});
|
|
50
|
+
it("keeps a system entry's sections so a resumed session restores its prompt state", () => {
|
|
51
|
+
const [message] = sessionEntryToContextMessages(system("s1", null, "base", { rules: "<rules>x</rules>" }));
|
|
52
|
+
expect(message.role).toBe("system");
|
|
53
|
+
expect(message.sections).toEqual({ rules: "<rules>x</rules>" });
|
|
54
|
+
});
|
|
55
|
+
it("does not contribute a usage entry to the message sequence", () => {
|
|
56
|
+
expect(sessionEntryToContextMessages(usage("u1", null))).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
it("replays a compaction boundary's system message before its summary", () => {
|
|
59
|
+
const messages = sessionEntryToContextMessages(compaction("c1", null, true));
|
|
60
|
+
expect(messages.map((m) => m.role)).toEqual(["system", "compactionSummary"]);
|
|
61
|
+
});
|
|
62
|
+
it("replays only the summary for a compaction boundary without prompt state", () => {
|
|
63
|
+
const messages = sessionEntryToContextMessages(compaction("c2", null, false));
|
|
64
|
+
expect(messages.map((m) => m.role)).toEqual(["compactionSummary"]);
|
|
65
|
+
});
|
|
66
|
+
it("rebuilds resume, branch navigation and backward tolerance in one sequence", () => {
|
|
67
|
+
const entries = [
|
|
68
|
+
user("m1", null, "first"),
|
|
69
|
+
system("s1", "m1", "instructions"),
|
|
70
|
+
usage("u1", "s1"),
|
|
71
|
+
system("s2", "u1", null),
|
|
72
|
+
compaction("c1", "s2", true),
|
|
73
|
+
user("m2", "c1", "second"),
|
|
74
|
+
];
|
|
75
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
76
|
+
const context = buildSessionContext(entries, "m2", byId);
|
|
77
|
+
const roles = context.messages.map((message) => message.role);
|
|
78
|
+
// The compaction boundary replaces everything before its first-kept entry.
|
|
79
|
+
expect(roles[roles.length - 2]).toBe("compactionSummary");
|
|
80
|
+
expect(roles[roles.length - 1]).toBe("user");
|
|
81
|
+
expect(roles).toContain("system");
|
|
82
|
+
});
|
|
83
|
+
it("restores a branch's own prompt and tool state when navigating to it", () => {
|
|
84
|
+
const entries = [
|
|
85
|
+
user("a1", null, "root"),
|
|
86
|
+
system("as", "a1", "branch-a instructions"),
|
|
87
|
+
user("a2", "as", "on branch a"),
|
|
88
|
+
user("b2", "a1", "on branch b"),
|
|
89
|
+
];
|
|
90
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
91
|
+
const branchA = buildContextEntries(entries, "a2", byId);
|
|
92
|
+
const branchB = buildContextEntries(entries, "b2", byId);
|
|
93
|
+
expect(branchA.map((entry) => entry.id)).toContain("as");
|
|
94
|
+
expect(branchB.map((entry) => entry.id)).not.toContain("as");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import { type ImageContent, type Message, type TextContent, type Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type ImageContent, type Message, type SystemMessage, type TextContent, type Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
import { type BashExecutionMessage, type CustomMessage } from "./messages.ts";
|
|
4
4
|
export declare const CURRENT_SESSION_VERSION = 3;
|
|
5
5
|
export interface SessionHeader {
|
|
@@ -33,6 +33,16 @@ export interface ModelChangeEntry extends SessionEntryBase {
|
|
|
33
33
|
provider: string;
|
|
34
34
|
modelId: string;
|
|
35
35
|
}
|
|
36
|
+
export interface UsageEntry extends SessionEntryBase {
|
|
37
|
+
type: "usage";
|
|
38
|
+
/** Arbitrary usage category, such as "cache_warm". */
|
|
39
|
+
kind: string;
|
|
40
|
+
provider: string;
|
|
41
|
+
model: string;
|
|
42
|
+
usage: Usage;
|
|
43
|
+
/** Optional human-readable qualifier for usage notices. */
|
|
44
|
+
note?: string;
|
|
45
|
+
}
|
|
36
46
|
export interface CompactionEntry<T = unknown> extends SessionEntryBase {
|
|
37
47
|
type: "compaction";
|
|
38
48
|
summary: string;
|
|
@@ -44,6 +54,8 @@ export interface CompactionEntry<T = unknown> extends SessionEntryBase {
|
|
|
44
54
|
usage?: Usage;
|
|
45
55
|
/** True if generated by an extension, undefined/false if pi-generated (backward compatible) */
|
|
46
56
|
fromHook?: boolean;
|
|
57
|
+
/** Complete prompt and tool state at this compaction boundary. */
|
|
58
|
+
systemMessage?: SystemMessage;
|
|
47
59
|
}
|
|
48
60
|
export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
|
|
49
61
|
type: "branch_summary";
|
|
@@ -102,7 +114,7 @@ export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
|
|
|
102
114
|
display: boolean;
|
|
103
115
|
}
|
|
104
116
|
/** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */
|
|
105
|
-
export type SessionEntry = SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | SessionInfoEntry;
|
|
117
|
+
export type SessionEntry = SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | UsageEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | SessionInfoEntry;
|
|
106
118
|
/** Raw file entry (includes header) */
|
|
107
119
|
export type FileEntry = SessionHeader | SessionEntry;
|
|
108
120
|
/** Tree node for getTree() - defensive copy of session structure */
|
|
@@ -169,7 +181,9 @@ export declare function getDefaultSessionDir(cwd: string, agentDir?: string): st
|
|
|
169
181
|
export declare function loadEntriesFromFile(filePath: string): FileEntry[];
|
|
170
182
|
/** Exported for testing */
|
|
171
183
|
export declare function findMostRecentSession(sessionDir: string, cwd?: string): string | null;
|
|
172
|
-
export type SessionListProgress = (loaded: number, total: number
|
|
184
|
+
export type SessionListProgress = (loaded: number, total: number,
|
|
185
|
+
/** Sessions loaded so far, sorted by activity. Present on periodic updates. */
|
|
186
|
+
partialSessions?: readonly SessionInfo[]) => void;
|
|
173
187
|
/**
|
|
174
188
|
* Manages conversation sessions as append-only trees stored in JSONL files.
|
|
175
189
|
*
|
|
@@ -220,6 +234,8 @@ export declare class SessionManager {
|
|
|
220
234
|
appendThinkingLevelChange(thinkingLevel: string): string;
|
|
221
235
|
/** Append a model change as child of current leaf, then advance leaf. Returns entry id. */
|
|
222
236
|
appendModelChange(provider: string, modelId: string): string;
|
|
237
|
+
/** Append model-attributed usage that does not participate in LLM context. Returns the appended entry. */
|
|
238
|
+
appendUsage(kind: string, provider: string, model: string, usage: Usage, note?: string): UsageEntry;
|
|
223
239
|
/** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
|
|
224
240
|
appendCompaction<T = unknown>(summary: string, firstKeptEntryId: string, tokensBefore: number, details?: T, fromHook?: boolean, usage?: Usage): string;
|
|
225
241
|
/** Append a custom entry (for extensions) as child of current leaf, then advance leaf. Returns entry id. */
|
|
@@ -340,17 +356,24 @@ export declare class SessionManager {
|
|
|
340
356
|
* @param sessionDir Optional session directory. If omitted, uses default for targetCwd.
|
|
341
357
|
*/
|
|
342
358
|
static forkFrom(sourcePath: string, targetCwd: string, sessionDir?: string, options?: NewSessionOptions): SessionManager;
|
|
359
|
+
/**
|
|
360
|
+
* Find an exact session ID without loading transcript bodies.
|
|
361
|
+
* @param cwd Working directory (used to compute default session directory)
|
|
362
|
+
* @param id Exact session ID
|
|
363
|
+
* @param sessionDir Optional session directory. If omitted, uses default (~/.selesai/agent/sessions/<encoded-cwd>/).
|
|
364
|
+
*/
|
|
365
|
+
static findById(cwd: string, id: string, sessionDir?: string): string | undefined;
|
|
343
366
|
/**
|
|
344
367
|
* List all sessions for a directory.
|
|
345
368
|
* @param cwd Working directory (used to compute default session directory)
|
|
346
369
|
* @param sessionDir Optional session directory. If omitted, uses the configured agent sessions directory.
|
|
347
370
|
* @param onProgress Optional callback for progress updates (loaded, total)
|
|
348
371
|
*/
|
|
349
|
-
static list(cwd: string, sessionDir?: string, onProgress?: SessionListProgress): Promise<SessionInfo[]>;
|
|
372
|
+
static list(cwd: string, sessionDir?: string, onProgress?: SessionListProgress, signal?: AbortSignal): Promise<SessionInfo[]>;
|
|
350
373
|
/**
|
|
351
374
|
* List all sessions across all project directories.
|
|
352
375
|
* @param onProgress Optional callback for progress updates (loaded, total)
|
|
353
376
|
*/
|
|
354
|
-
static listAll(onProgress?: SessionListProgress): Promise<SessionInfo[]>;
|
|
355
|
-
static listAll(sessionDir?: string, onProgress?: SessionListProgress): Promise<SessionInfo[]>;
|
|
377
|
+
static listAll(onProgress?: SessionListProgress, signal?: AbortSignal): Promise<SessionInfo[]>;
|
|
378
|
+
static listAll(sessionDir?: string, onProgress?: SessionListProgress, signal?: AbortSignal): Promise<SessionInfo[]>;
|
|
356
379
|
}
|