@dreb/coding-agent 2.60.0 → 2.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/core/agent-session.d.ts +49 -7
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +136 -28
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +74 -3
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -4
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.d.ts +7 -2
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +19 -12
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +27 -12
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +1 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +2 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/models.md +26 -8
- package/docs/rpc.md +16 -6
- package/docs/settings.md +11 -3
- package/docs/tree.md +1 -1
- package/package.json +1 -1
|
@@ -1217,6 +1217,17 @@ export class AgentSession {
|
|
|
1217
1217
|
_getFilteredSkills() {
|
|
1218
1218
|
return this.getFilteredSkills();
|
|
1219
1219
|
}
|
|
1220
|
+
_resolveModelPromptSettings(model) {
|
|
1221
|
+
if (!model)
|
|
1222
|
+
return undefined;
|
|
1223
|
+
const modelRef = `${model.provider}/${model.id}`;
|
|
1224
|
+
const modelsJsonSettings = this._modelRegistry.getModelPromptSettings(model.provider, model.id);
|
|
1225
|
+
const settingsJsonSettings = this.settingsManager.getModelPromptSettings(model.provider, model.id);
|
|
1226
|
+
if (modelsJsonSettings && settingsJsonSettings) {
|
|
1227
|
+
throw new Error(`System prompt behavior for ${modelRef} is configured in both models.json and settings.json; remove one source`);
|
|
1228
|
+
}
|
|
1229
|
+
return modelsJsonSettings ?? settingsJsonSettings;
|
|
1230
|
+
}
|
|
1220
1231
|
_rebuildSystemPrompt(toolNames) {
|
|
1221
1232
|
const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
|
|
1222
1233
|
const toolSnippets = {};
|
|
@@ -1233,9 +1244,7 @@ export class AgentSession {
|
|
|
1233
1244
|
}
|
|
1234
1245
|
const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
|
|
1235
1246
|
const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
|
|
1236
|
-
const modelPromptSettings = this.model
|
|
1237
|
-
? this.settingsManager.getModelPromptSettings(this.model.provider, this.model.id)
|
|
1238
|
-
: undefined;
|
|
1247
|
+
const modelPromptSettings = this._resolveModelPromptSettings(this.model);
|
|
1239
1248
|
const customPrompt = loaderSystemPrompt ?? modelPromptSettings?.systemPrompt;
|
|
1240
1249
|
const appendPromptParts = [...loaderAppendSystemPrompt];
|
|
1241
1250
|
if (modelPromptSettings?.appendSystemPrompt) {
|
|
@@ -1753,9 +1762,9 @@ export class AgentSession {
|
|
|
1753
1762
|
_refreshThinkingDisplay(model) {
|
|
1754
1763
|
this.agent.thinkingDisplay = resolveThinkingDisplay(model, this.settingsManager.getModelThinkingDisplay(model.id));
|
|
1755
1764
|
}
|
|
1756
|
-
/** Reject malformed target-model prompt settings before
|
|
1765
|
+
/** Reject malformed or conflicting target-model prompt settings before mutating session state. */
|
|
1757
1766
|
_validateModelPromptSettings(model) {
|
|
1758
|
-
this.
|
|
1767
|
+
this._resolveModelPromptSettings(model);
|
|
1759
1768
|
}
|
|
1760
1769
|
/**
|
|
1761
1770
|
* Cycle to next/previous model.
|
|
@@ -2720,9 +2729,19 @@ export class AgentSession {
|
|
|
2720
2729
|
});
|
|
2721
2730
|
}
|
|
2722
2731
|
async reload() {
|
|
2732
|
+
// Refresh and validate prompt configuration before tearing down the active runtime.
|
|
2733
|
+
// A bad external edit must leave the current prompt and extension runtime usable.
|
|
2734
|
+
this.settingsManager.reload();
|
|
2735
|
+
this._modelRegistry.refresh();
|
|
2736
|
+
const modelRegistryError = this._modelRegistry.getError();
|
|
2737
|
+
if (modelRegistryError) {
|
|
2738
|
+
throw new Error(modelRegistryError);
|
|
2739
|
+
}
|
|
2740
|
+
if (this.model) {
|
|
2741
|
+
this._validateModelPromptSettings(this.model);
|
|
2742
|
+
}
|
|
2723
2743
|
const previousFlagValues = this._extensionRunner?.getFlagValues();
|
|
2724
2744
|
await this._extensionRunner?.emit({ type: "session_shutdown" });
|
|
2725
|
-
this.settingsManager.reload();
|
|
2726
2745
|
resetApiProviders();
|
|
2727
2746
|
await this._resourceLoader.reload();
|
|
2728
2747
|
this._buildRuntime({
|
|
@@ -3043,21 +3062,71 @@ export class AgentSession {
|
|
|
3043
3062
|
this._emit({ type: "session_name_changed", name: this.sessionName ?? "" });
|
|
3044
3063
|
}
|
|
3045
3064
|
/**
|
|
3046
|
-
* Create a fork from a specific entry.
|
|
3065
|
+
* Create a fork from a specific entry. The fork point may be any user or
|
|
3066
|
+
* assistant message in the transcript; branch semantics depend on the role:
|
|
3067
|
+
*
|
|
3068
|
+
* - **Assistant message** -> the new branch *includes* the selected response
|
|
3069
|
+
* (and everything before it); no editor pre-fill. "Continue from this answer."
|
|
3070
|
+
* Forking at the last assistant message keeps the entire current state.
|
|
3071
|
+
* - **User message** -> rewind to *before* the selected message (branch from its
|
|
3072
|
+
* parent, dropping the message and everything after it) and offer its text as
|
|
3073
|
+
* editor pre-fill. "Edit / re-ask this question."
|
|
3074
|
+
*
|
|
3047
3075
|
* Emits before_fork/fork session events to extensions.
|
|
3048
3076
|
*
|
|
3049
|
-
* @param entryId ID of the entry to fork from
|
|
3077
|
+
* @param entryId ID of the message entry to fork from
|
|
3050
3078
|
* @returns Object with:
|
|
3051
|
-
* - selectedText: The
|
|
3079
|
+
* - selectedText: The selected user message text for editor pre-fill (empty
|
|
3080
|
+
* when forking at an assistant message).
|
|
3052
3081
|
* - cancelled: True if an extension cancelled the fork
|
|
3053
3082
|
*/
|
|
3054
3083
|
async fork(entryId) {
|
|
3055
|
-
const previousSessionFile = this.sessionFile;
|
|
3056
3084
|
const selectedEntry = this.sessionManager.getEntry(entryId);
|
|
3057
|
-
if (!selectedEntry ||
|
|
3085
|
+
if (!selectedEntry ||
|
|
3086
|
+
selectedEntry.type !== "message" ||
|
|
3087
|
+
(selectedEntry.message.role !== "user" && selectedEntry.message.role !== "assistant")) {
|
|
3058
3088
|
throw new Error("Invalid entry ID for forking");
|
|
3059
3089
|
}
|
|
3060
|
-
|
|
3090
|
+
if (selectedEntry.message.role === "assistant") {
|
|
3091
|
+
// Continue-from-answer: branch from the assistant entry itself so it (and
|
|
3092
|
+
// everything before it) is retained. No editor pre-fill.
|
|
3093
|
+
//
|
|
3094
|
+
// Reject turns that can't be safely branched from (interrupted, or waiting
|
|
3095
|
+
// on tool results) — branching there would silently produce a branch that
|
|
3096
|
+
// doesn't match the selected turn. See _isForkableAssistant.
|
|
3097
|
+
if (!this._isForkableAssistant(selectedEntry.message)) {
|
|
3098
|
+
throw new Error("Cannot fork at this assistant turn: it was interrupted or is still waiting on tool results");
|
|
3099
|
+
}
|
|
3100
|
+
const { cancelled } = await this._performFork(entryId, () => {
|
|
3101
|
+
this.sessionManager.createBranchedSession(entryId);
|
|
3102
|
+
});
|
|
3103
|
+
return { selectedText: "", cancelled };
|
|
3104
|
+
}
|
|
3105
|
+
const selectedText = this._extractMessageText(selectedEntry.message.content);
|
|
3106
|
+
// Rewind to *before* the selected user message by branching from its parent,
|
|
3107
|
+
// so the selected message (and everything after it) is dropped and its text is
|
|
3108
|
+
// offered as editor pre-fill.
|
|
3109
|
+
const { cancelled } = await this._performFork(entryId, (previousSessionFile) => {
|
|
3110
|
+
if (!selectedEntry.parentId) {
|
|
3111
|
+
this.sessionManager.newSession({ parentSession: previousSessionFile });
|
|
3112
|
+
}
|
|
3113
|
+
else {
|
|
3114
|
+
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
3115
|
+
}
|
|
3116
|
+
});
|
|
3117
|
+
return { selectedText, cancelled };
|
|
3118
|
+
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Shared fork machinery: emit the cancellable session_before_fork event,
|
|
3121
|
+
* clear pending state, create the branch via the supplied strategy, reload
|
|
3122
|
+
* the conversation, and emit session_fork.
|
|
3123
|
+
*
|
|
3124
|
+
* @param entryId Entry the fork is anchored to (reported to extensions).
|
|
3125
|
+
* @param branch Strategy that creates the branched/new session. Receives the
|
|
3126
|
+
* previous session file so callers can set it as the parent when needed.
|
|
3127
|
+
*/
|
|
3128
|
+
async _performFork(entryId, branch) {
|
|
3129
|
+
const previousSessionFile = this.sessionFile;
|
|
3061
3130
|
let skipConversationRestore = false;
|
|
3062
3131
|
// Emit session_before_fork event (can be cancelled)
|
|
3063
3132
|
if (this._extensionRunner?.hasHandlers("session_before_fork")) {
|
|
@@ -3066,18 +3135,13 @@ export class AgentSession {
|
|
|
3066
3135
|
entryId,
|
|
3067
3136
|
}));
|
|
3068
3137
|
if (result?.cancel) {
|
|
3069
|
-
return {
|
|
3138
|
+
return { cancelled: true };
|
|
3070
3139
|
}
|
|
3071
3140
|
skipConversationRestore = result?.skipConversationRestore ?? false;
|
|
3072
3141
|
}
|
|
3073
3142
|
// Clear pending messages (bound to old session state)
|
|
3074
3143
|
this._pendingNextTurnMessages = [];
|
|
3075
|
-
|
|
3076
|
-
this.sessionManager.newSession({ parentSession: previousSessionFile });
|
|
3077
|
-
}
|
|
3078
|
-
else {
|
|
3079
|
-
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
3080
|
-
}
|
|
3144
|
+
branch(previousSessionFile);
|
|
3081
3145
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
3082
3146
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
3083
3147
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
@@ -3092,7 +3156,7 @@ export class AgentSession {
|
|
|
3092
3156
|
if (!skipConversationRestore) {
|
|
3093
3157
|
this.agent.replaceMessages(sessionContext.messages);
|
|
3094
3158
|
}
|
|
3095
|
-
return {
|
|
3159
|
+
return { cancelled: false };
|
|
3096
3160
|
}
|
|
3097
3161
|
// =========================================================================
|
|
3098
3162
|
// Tree Navigation
|
|
@@ -3226,7 +3290,7 @@ export class AgentSession {
|
|
|
3226
3290
|
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
|
|
3227
3291
|
// User message: leaf = parent (null if root), text goes to editor
|
|
3228
3292
|
newLeafId = targetEntry.parentId;
|
|
3229
|
-
editorText = this.
|
|
3293
|
+
editorText = this._extractMessageText(targetEntry.message.content);
|
|
3230
3294
|
}
|
|
3231
3295
|
else if (targetEntry.type === "custom_message") {
|
|
3232
3296
|
// Custom message: leaf = parent (null if root), text goes to editor
|
|
@@ -3288,24 +3352,68 @@ export class AgentSession {
|
|
|
3288
3352
|
}
|
|
3289
3353
|
}
|
|
3290
3354
|
/**
|
|
3291
|
-
* Get all
|
|
3355
|
+
* Get all forkable messages (user *and* assistant) for the fork selector.
|
|
3356
|
+
*
|
|
3357
|
+
* Each entry carries its role so callers can label it and choose the right
|
|
3358
|
+
* fork semantics (assistant = continue-from-answer, user = rewind + re-ask).
|
|
3359
|
+
* A forkable assistant turn with no renderable text (e.g. a thinking-only
|
|
3360
|
+
* turn) still appears as a fork point, with a generic label.
|
|
3361
|
+
*
|
|
3362
|
+
* Assistant turns that cannot be safely branched from (interrupted turns, or
|
|
3363
|
+
* turns containing a tool call whose result lives in a descendant entry) are
|
|
3364
|
+
* excluded — see _isForkableAssistant.
|
|
3292
3365
|
*/
|
|
3293
|
-
|
|
3366
|
+
getForkableMessages() {
|
|
3294
3367
|
const entries = this.sessionManager.getEntries();
|
|
3295
3368
|
const result = [];
|
|
3296
3369
|
for (const entry of entries) {
|
|
3297
3370
|
if (entry.type !== "message")
|
|
3298
3371
|
continue;
|
|
3299
|
-
|
|
3372
|
+
const role = entry.message.role;
|
|
3373
|
+
if (role !== "user" && role !== "assistant")
|
|
3300
3374
|
continue;
|
|
3301
|
-
const text = this.
|
|
3302
|
-
if (
|
|
3303
|
-
|
|
3375
|
+
const text = this._extractMessageText(entry.message.content);
|
|
3376
|
+
if (role === "user") {
|
|
3377
|
+
// Preserve existing behavior: skip empty user messages.
|
|
3378
|
+
if (text)
|
|
3379
|
+
result.push({ entryId: entry.id, text, role });
|
|
3380
|
+
}
|
|
3381
|
+
else {
|
|
3382
|
+
// Only offer assistant turns that can be safely branched from.
|
|
3383
|
+
if (!this._isForkableAssistant(entry.message))
|
|
3384
|
+
continue;
|
|
3385
|
+
result.push({ entryId: entry.id, text: text || "(assistant response)", role });
|
|
3304
3386
|
}
|
|
3305
3387
|
}
|
|
3306
3388
|
return result;
|
|
3307
3389
|
}
|
|
3308
|
-
|
|
3390
|
+
/**
|
|
3391
|
+
* Whether an assistant turn can be safely used as a fork point.
|
|
3392
|
+
*
|
|
3393
|
+
* Forking anchors on the entry's ancestors only (SessionManager.getBranch
|
|
3394
|
+
* walks parentId upward), and errored/aborted turns are dropped by
|
|
3395
|
+
* transformMessages() before every request. Two kinds of assistant turn
|
|
3396
|
+
* therefore produce a branch that silently does NOT match what was selected:
|
|
3397
|
+
*
|
|
3398
|
+
* - stopReason "error"/"aborted": transformMessages() skips the turn, so the
|
|
3399
|
+
* reply vanishes from context on the next request (defeating "continue from
|
|
3400
|
+
* this answer", and risking back-to-back user messages on strict providers).
|
|
3401
|
+
* - turns containing tool calls: their tool results are *descendant* entries a
|
|
3402
|
+
* branch cannot include, so transformMessages() substitutes a fabricated
|
|
3403
|
+
* "No result provided" (isError) result — telling the model a successful
|
|
3404
|
+
* tool call failed.
|
|
3405
|
+
*
|
|
3406
|
+
* A completed answer (the intended "continue from here" target) has a terminal
|
|
3407
|
+
* stopReason and no unresolved tool calls, so it passes.
|
|
3408
|
+
*/
|
|
3409
|
+
_isForkableAssistant(message) {
|
|
3410
|
+
if (message.stopReason === "error" || message.stopReason === "aborted")
|
|
3411
|
+
return false;
|
|
3412
|
+
if (Array.isArray(message.content) && message.content.some((c) => c.type === "toolCall"))
|
|
3413
|
+
return false;
|
|
3414
|
+
return true;
|
|
3415
|
+
}
|
|
3416
|
+
_extractMessageText(content) {
|
|
3309
3417
|
if (typeof content === "string")
|
|
3310
3418
|
return content;
|
|
3311
3419
|
if (Array.isArray(content)) {
|