@dreb/coding-agent 2.24.0 → 2.25.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/README.md +2 -0
- package/dist/core/agent-session.d.ts +11 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +23 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +4 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/settings-manager.d.ts +14 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +41 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +4 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +16 -11
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/core/tools/web.d.ts +10 -0
- package/dist/core/tools/web.d.ts.map +1 -1
- package/dist/core/tools/web.js +51 -1
- package/dist/core/tools/web.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +36 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +195 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +24 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/tab-title.d.ts +5 -0
- package/dist/modes/interactive/tab-title.d.ts.map +1 -1
- package/dist/modes/interactive/tab-title.js +6 -0
- package/dist/modes/interactive/tab-title.js.map +1 -1
- package/docs/agent-models.md +64 -0
- package/docs/mach6.md +2 -0
- package/docs/settings.md +18 -0
- package/package.json +1 -1
|
@@ -22,6 +22,11 @@ export interface TabTitleDeps {
|
|
|
22
22
|
getModelRegistry: () => ModelRegistry;
|
|
23
23
|
/** Get the parent provider name. */
|
|
24
24
|
getProvider: () => string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Get the user's agentModels settings override for a given agent name, if any.
|
|
27
|
+
* Returns a non-empty fallback list when the user has configured an override.
|
|
28
|
+
*/
|
|
29
|
+
getAgentModelsOverride?: (agentName: string) => string[] | undefined;
|
|
25
30
|
}
|
|
26
31
|
export declare class TabTitleGenerator {
|
|
27
32
|
private readonly settings;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-title.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/tab-title.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,GAAG,EAAW,KAAK,EAAE,MAAM,UAAU,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAWvE,MAAM,WAAW,YAAY;IAC5B,0CAA0C;IAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,sDAAsD;IACtD,WAAW,EAAE,MAAM,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC9D,yEAAuE;IACvE,QAAQ,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACvC,iDAAiD;IACjD,gBAAgB,EAAE,MAAM,aAAa,CAAC;IACtC,oCAAoC;IACpC,WAAW,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"tab-title.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/tab-title.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EAAE,GAAG,EAAW,KAAK,EAAE,MAAM,UAAU,CAAC;AAGpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAWvE,MAAM,WAAW,YAAY;IAC5B,0CAA0C;IAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,sDAAsD;IACtD,WAAW,EAAE,MAAM,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC9D,yEAAuE;IACvE,QAAQ,EAAE,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IACvC,iDAAiD;IACjD,gBAAgB,EAAE,MAAM,aAAa,CAAC;IACtC,oCAAoC;IACpC,WAAW,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACtC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EAAE,GAAG,SAAS,CAAC;CACrE;AAED,qBAAa,iBAAiB;IAM5B,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANtB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,YACkB,QAAQ,EAAE,gBAAgB,GAAG,SAAS,EACtC,IAAI,EAAE,YAAY,EAGnC;IAED,yCAAyC;IACzC,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;OAGG;IACH,SAAS,IAAI,IAAI,CAShB;IAED,2DAAyD;IACzD,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,oEAAkE;IAClE,IAAI,QAAQ,IAAI,OAAO,CAEtB;YAEa,aAAa;YA8Bb,YAAY;IA4B1B,OAAO,CAAC,qBAAqB;IA4B7B,OAAO,CAAC,YAAY;IAyBpB,mDAAmD;IACnD,OAAO,CAAC,aAAa;CAsBrB","sourcesContent":["/**\n * Auto-generates a terminal tab title from session context after a threshold\n * number of tool calls. Uses a lightweight single-shot LLM call to produce a\n * concise ≤30 character title, then sets it via the terminal's OSC 0 escape.\n *\n * Fires at most once per session. Failures are swallowed silently.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { Api, Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { CONFIG_DIR_NAME, getPackageDir } from \"../../config.js\";\nimport type { ModelRegistry } from \"../../core/model-registry.js\";\nimport type { TabTitleSettings } from \"../../core/settings-manager.js\";\nimport { parseAgentFrontmatter, resolveModelForSubagentSpawn } from \"../../core/tools/subagent.js\";\n\nconst DEFAULT_TRIGGER_AFTER = 3;\nconst MAX_TITLE_LENGTH = 30;\nconst TITLE_GENERATION_TIMEOUT_MS = 60_000;\n\nconst TITLE_PROMPT =\n\t\"Summarize this session's task in ≤30 characters for a terminal tab title. \" +\n\t\"Output ONLY the title text, nothing else. No quotes, no explanation.\";\n\nexport interface TabTitleDeps {\n\t/** Set the terminal tab title (OSC 0). */\n\tsetTitle: (title: string) => void;\n\t/** Get the current session messages (for context). */\n\tgetMessages: () => Array<{ role: string; content?: unknown }>;\n\t/** Get the current model (parent session model — used as fallback). */\n\tgetModel: () => Model<Api> | undefined;\n\t/** Get model registry for API key resolution. */\n\tgetModelRegistry: () => ModelRegistry;\n\t/** Get the parent provider name. */\n\tgetProvider: () => string | undefined;\n\t/**\n\t * Get the user's agentModels settings override for a given agent name, if any.\n\t * Returns a non-empty fallback list when the user has configured an override.\n\t */\n\tgetAgentModelsOverride?: (agentName: string) => string[] | undefined;\n}\n\nexport class TabTitleGenerator {\n\tprivate toolCallCount = 0;\n\tprivate fired = false;\n\tprivate readonly threshold: number;\n\n\tconstructor(\n\t\tprivate readonly settings: TabTitleSettings | undefined,\n\t\tprivate readonly deps: TabTitleDeps,\n\t) {\n\t\tthis.threshold = settings?.triggerAfter ?? DEFAULT_TRIGGER_AFTER;\n\t}\n\n\t/** Whether this generator is enabled. */\n\tget enabled(): boolean {\n\t\treturn this.settings?.enabled !== false;\n\t}\n\n\t/**\n\t * Called on each tool_execution_end event. Increments the counter and fires\n\t * the title generation when threshold is reached.\n\t */\n\tonToolEnd(): void {\n\t\tif (this.fired || !this.enabled) return;\n\n\t\tthis.toolCallCount++;\n\t\tif (this.toolCallCount >= this.threshold) {\n\t\t\tthis.fired = true;\n\t\t\t// Fire-and-forget — never surfaces errors to the user\n\t\t\tthis.generateTitle().catch(() => {});\n\t\t}\n\t}\n\n\t/** Exposed for testing — the current tool call count. */\n\tget currentCount(): number {\n\t\treturn this.toolCallCount;\n\t}\n\n\t/** Exposed for testing — whether the title has been generated. */\n\tget hasFired(): boolean {\n\t\treturn this.fired;\n\t}\n\n\tprivate async generateTitle(): Promise<void> {\n\t\t// Single timeout bounds the entire pipeline (model probing + API key + LLM call)\n\t\tconst signal = AbortSignal.timeout(TITLE_GENERATION_TIMEOUT_MS);\n\n\t\tconst model = await this.resolveModel(signal);\n\t\tif (!model) return;\n\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst apiKey = await registry.getApiKey(model);\n\n\t\tconst userContext = this.buildContext();\n\t\tif (!userContext) return;\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: TITLE_PROMPT,\n\t\t\tmessages: [{ role: \"user\", content: userContext, timestamp: Date.now() }],\n\t\t};\n\n\t\tconst response = await completeSimple(model, context, {\n\t\t\tapiKey,\n\t\t\tmaxRetryDelayMs: 0,\n\t\t\tsignal,\n\t\t});\n\n\t\tconst title = this.sanitizeTitle(response);\n\t\tif (title) {\n\t\t\tthis.deps.setTitle(`dreb - ${title}`);\n\t\t}\n\t}\n\n\tprivate async resolveModel(signal?: AbortSignal): Promise<Model<Api> | undefined> {\n\t\t// Try to get the Explore agent's model fallback list\n\t\tconst exploreModels = this.getExploreAgentModels();\n\t\tconst parentModel = this.deps.getModel();\n\t\tconst parentProvider = this.deps.getProvider();\n\t\tconst registry = this.deps.getModelRegistry();\n\n\t\tif (exploreModels) {\n\t\t\tconst resolution = await resolveModelForSubagentSpawn(\n\t\t\t\texploreModels,\n\t\t\t\tparentProvider,\n\t\t\t\tregistry,\n\t\t\t\tparentModel?.id,\n\t\t\t\tsignal,\n\t\t\t\t\"[tab-title]\",\n\t\t\t);\n\t\t\tif (resolution.ok) {\n\t\t\t\t// Find the resolved model in registry\n\t\t\t\tconst available = registry.getAvailable();\n\t\t\t\tconst found = available.find((m) => m.id === resolution.modelId);\n\t\t\t\tif (found) return found;\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to parent session model\n\t\treturn parentModel;\n\t}\n\n\tprivate getExploreAgentModels(): string | string[] | undefined {\n\t\t// Honor the user's agentModels settings override first. The settings key must\n\t\t// match the agent name exactly (\"Explore\", as declared in explore.md frontmatter).\n\t\tconst override = this.deps.getAgentModelsOverride?.(\"Explore\");\n\t\tif (override && override.length > 0) {\n\t\t\treturn override;\n\t\t}\n\n\t\t// Resolution order mirrors discoverAgentTypes: user override > project > package.\n\t\t// First match with a valid model wins.\n\t\tconst candidates = [\n\t\t\tjoin(homedir(), CONFIG_DIR_NAME, \"agents\", \"explore.md\"),\n\t\t\tjoin(process.cwd(), \".dreb\", \"agents\", \"explore.md\"),\n\t\t\tjoin(getPackageDir(), \"agents\", \"explore.md\"),\n\t\t];\n\n\t\tfor (const agentFile of candidates) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(agentFile, \"utf-8\");\n\t\t\t\tconst parsed = parseAgentFrontmatter(content);\n\t\t\t\tif (parsed.ok && parsed.config.model) {\n\t\t\t\t\treturn parsed.config.model;\n\t\t\t\t}\n\t\t\t} catch {}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate buildContext(): string | undefined {\n\t\tconst messages = this.deps.getMessages();\n\t\tif (messages.length === 0) return undefined;\n\n\t\t// Extract the first user message for context\n\t\tconst firstUser = messages.find((m) => m.role === \"user\");\n\t\tif (!firstUser) return undefined;\n\n\t\tconst content =\n\t\t\ttypeof firstUser.content === \"string\"\n\t\t\t\t? firstUser.content\n\t\t\t\t: Array.isArray(firstUser.content)\n\t\t\t\t\t? firstUser.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t: \"\";\n\n\t\tif (!content) return undefined;\n\n\t\t// Truncate long messages — the LLM only needs a summary\n\t\tconst truncated = content.length > 500 ? `${content.slice(0, 500)}...` : content;\n\t\treturn `Session task:\\n${truncated}`;\n\t}\n\n\t/** Clean up LLM response to a usable tab title. */\n\tprivate sanitizeTitle(response: unknown): string | undefined {\n\t\tif (!response || typeof response !== \"object\") return undefined;\n\n\t\tconst msg = response as { content?: Array<{ type: string; text?: string }> };\n\t\tif (!msg.content || !Array.isArray(msg.content)) return undefined;\n\n\t\tconst textPart = msg.content.find((c) => c.type === \"text\");\n\t\tif (!textPart?.text) return undefined;\n\n\t\tlet title = textPart.text.trim();\n\t\t// Strip surrounding quotes if present\n\t\tif ((title.startsWith('\"') && title.endsWith('\"')) || (title.startsWith(\"'\") && title.endsWith(\"'\"))) {\n\t\t\ttitle = title.slice(1, -1).trim();\n\t\t}\n\t\t// Remove newlines\n\t\ttitle = title.replace(/[\\r\\n]+/g, \" \").trim();\n\t\t// Truncate to max length\n\t\tif (title.length > MAX_TITLE_LENGTH) {\n\t\t\ttitle = title.slice(0, MAX_TITLE_LENGTH);\n\t\t}\n\t\treturn title || undefined;\n\t}\n}\n"]}
|
|
@@ -98,6 +98,12 @@ export class TabTitleGenerator {
|
|
|
98
98
|
return parentModel;
|
|
99
99
|
}
|
|
100
100
|
getExploreAgentModels() {
|
|
101
|
+
// Honor the user's agentModels settings override first. The settings key must
|
|
102
|
+
// match the agent name exactly ("Explore", as declared in explore.md frontmatter).
|
|
103
|
+
const override = this.deps.getAgentModelsOverride?.("Explore");
|
|
104
|
+
if (override && override.length > 0) {
|
|
105
|
+
return override;
|
|
106
|
+
}
|
|
101
107
|
// Resolution order mirrors discoverAgentTypes: user override > project > package.
|
|
102
108
|
// First match with a valid model wins.
|
|
103
109
|
const candidates = [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tab-title.js","sourceRoot":"","sources":["../../../src/modes/interactive/tab-title.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Auto-generates a terminal tab title from session context after a threshold\n * number of tool calls. Uses a lightweight single-shot LLM call to produce a\n * concise ≤30 character title, then sets it via the terminal's OSC 0 escape.\n *\n * Fires at most once per session. Failures are swallowed silently.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { Api, Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { CONFIG_DIR_NAME, getPackageDir } from \"../../config.js\";\nimport type { ModelRegistry } from \"../../core/model-registry.js\";\nimport type { TabTitleSettings } from \"../../core/settings-manager.js\";\nimport { parseAgentFrontmatter, resolveModelForSubagentSpawn } from \"../../core/tools/subagent.js\";\n\nconst DEFAULT_TRIGGER_AFTER = 3;\nconst MAX_TITLE_LENGTH = 30;\nconst TITLE_GENERATION_TIMEOUT_MS = 60_000;\n\nconst TITLE_PROMPT =\n\t\"Summarize this session's task in ≤30 characters for a terminal tab title. \" +\n\t\"Output ONLY the title text, nothing else. No quotes, no explanation.\";\n\nexport interface TabTitleDeps {\n\t/** Set the terminal tab title (OSC 0). */\n\tsetTitle: (title: string) => void;\n\t/** Get the current session messages (for context). */\n\tgetMessages: () => Array<{ role: string; content?: unknown }>;\n\t/** Get the current model (parent session model — used as fallback). */\n\tgetModel: () => Model<Api> | undefined;\n\t/** Get model registry for API key resolution. */\n\tgetModelRegistry: () => ModelRegistry;\n\t/** Get the parent provider name. */\n\tgetProvider: () => string | undefined;\n}\n\nexport class TabTitleGenerator {\n\tprivate toolCallCount = 0;\n\tprivate fired = false;\n\tprivate readonly threshold: number;\n\n\tconstructor(\n\t\tprivate readonly settings: TabTitleSettings | undefined,\n\t\tprivate readonly deps: TabTitleDeps,\n\t) {\n\t\tthis.threshold = settings?.triggerAfter ?? DEFAULT_TRIGGER_AFTER;\n\t}\n\n\t/** Whether this generator is enabled. */\n\tget enabled(): boolean {\n\t\treturn this.settings?.enabled !== false;\n\t}\n\n\t/**\n\t * Called on each tool_execution_end event. Increments the counter and fires\n\t * the title generation when threshold is reached.\n\t */\n\tonToolEnd(): void {\n\t\tif (this.fired || !this.enabled) return;\n\n\t\tthis.toolCallCount++;\n\t\tif (this.toolCallCount >= this.threshold) {\n\t\t\tthis.fired = true;\n\t\t\t// Fire-and-forget — never surfaces errors to the user\n\t\t\tthis.generateTitle().catch(() => {});\n\t\t}\n\t}\n\n\t/** Exposed for testing — the current tool call count. */\n\tget currentCount(): number {\n\t\treturn this.toolCallCount;\n\t}\n\n\t/** Exposed for testing — whether the title has been generated. */\n\tget hasFired(): boolean {\n\t\treturn this.fired;\n\t}\n\n\tprivate async generateTitle(): Promise<void> {\n\t\t// Single timeout bounds the entire pipeline (model probing + API key + LLM call)\n\t\tconst signal = AbortSignal.timeout(TITLE_GENERATION_TIMEOUT_MS);\n\n\t\tconst model = await this.resolveModel(signal);\n\t\tif (!model) return;\n\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst apiKey = await registry.getApiKey(model);\n\n\t\tconst userContext = this.buildContext();\n\t\tif (!userContext) return;\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: TITLE_PROMPT,\n\t\t\tmessages: [{ role: \"user\", content: userContext, timestamp: Date.now() }],\n\t\t};\n\n\t\tconst response = await completeSimple(model, context, {\n\t\t\tapiKey,\n\t\t\tmaxRetryDelayMs: 0,\n\t\t\tsignal,\n\t\t});\n\n\t\tconst title = this.sanitizeTitle(response);\n\t\tif (title) {\n\t\t\tthis.deps.setTitle(`dreb - ${title}`);\n\t\t}\n\t}\n\n\tprivate async resolveModel(signal?: AbortSignal): Promise<Model<Api> | undefined> {\n\t\t// Try to get the Explore agent's model fallback list\n\t\tconst exploreModels = this.getExploreAgentModels();\n\t\tconst parentModel = this.deps.getModel();\n\t\tconst parentProvider = this.deps.getProvider();\n\t\tconst registry = this.deps.getModelRegistry();\n\n\t\tif (exploreModels) {\n\t\t\tconst resolution = await resolveModelForSubagentSpawn(\n\t\t\t\texploreModels,\n\t\t\t\tparentProvider,\n\t\t\t\tregistry,\n\t\t\t\tparentModel?.id,\n\t\t\t\tsignal,\n\t\t\t\t\"[tab-title]\",\n\t\t\t);\n\t\t\tif (resolution.ok) {\n\t\t\t\t// Find the resolved model in registry\n\t\t\t\tconst available = registry.getAvailable();\n\t\t\t\tconst found = available.find((m) => m.id === resolution.modelId);\n\t\t\t\tif (found) return found;\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to parent session model\n\t\treturn parentModel;\n\t}\n\n\tprivate getExploreAgentModels(): string | string[] | undefined {\n\t\t// Resolution order mirrors discoverAgentTypes: user override > project > package.\n\t\t// First match with a valid model wins.\n\t\tconst candidates = [\n\t\t\tjoin(homedir(), CONFIG_DIR_NAME, \"agents\", \"explore.md\"),\n\t\t\tjoin(process.cwd(), \".dreb\", \"agents\", \"explore.md\"),\n\t\t\tjoin(getPackageDir(), \"agents\", \"explore.md\"),\n\t\t];\n\n\t\tfor (const agentFile of candidates) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(agentFile, \"utf-8\");\n\t\t\t\tconst parsed = parseAgentFrontmatter(content);\n\t\t\t\tif (parsed.ok && parsed.config.model) {\n\t\t\t\t\treturn parsed.config.model;\n\t\t\t\t}\n\t\t\t} catch {}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate buildContext(): string | undefined {\n\t\tconst messages = this.deps.getMessages();\n\t\tif (messages.length === 0) return undefined;\n\n\t\t// Extract the first user message for context\n\t\tconst firstUser = messages.find((m) => m.role === \"user\");\n\t\tif (!firstUser) return undefined;\n\n\t\tconst content =\n\t\t\ttypeof firstUser.content === \"string\"\n\t\t\t\t? firstUser.content\n\t\t\t\t: Array.isArray(firstUser.content)\n\t\t\t\t\t? firstUser.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t: \"\";\n\n\t\tif (!content) return undefined;\n\n\t\t// Truncate long messages — the LLM only needs a summary\n\t\tconst truncated = content.length > 500 ? `${content.slice(0, 500)}...` : content;\n\t\treturn `Session task:\\n${truncated}`;\n\t}\n\n\t/** Clean up LLM response to a usable tab title. */\n\tprivate sanitizeTitle(response: unknown): string | undefined {\n\t\tif (!response || typeof response !== \"object\") return undefined;\n\n\t\tconst msg = response as { content?: Array<{ type: string; text?: string }> };\n\t\tif (!msg.content || !Array.isArray(msg.content)) return undefined;\n\n\t\tconst textPart = msg.content.find((c) => c.type === \"text\");\n\t\tif (!textPart?.text) return undefined;\n\n\t\tlet title = textPart.text.trim();\n\t\t// Strip surrounding quotes if present\n\t\tif ((title.startsWith('\"') && title.endsWith('\"')) || (title.startsWith(\"'\") && title.endsWith(\"'\"))) {\n\t\t\ttitle = title.slice(1, -1).trim();\n\t\t}\n\t\t// Remove newlines\n\t\ttitle = title.replace(/[\\r\\n]+/g, \" \").trim();\n\t\t// Truncate to max length\n\t\tif (title.length > MAX_TITLE_LENGTH) {\n\t\t\ttitle = title.slice(0, MAX_TITLE_LENGTH);\n\t\t}\n\t\treturn title || undefined;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tab-title.js","sourceRoot":"","sources":["../../../src/modes/interactive/tab-title.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Auto-generates a terminal tab title from session context after a threshold\n * number of tool calls. Uses a lightweight single-shot LLM call to produce a\n * concise ≤30 character title, then sets it via the terminal's OSC 0 escape.\n *\n * Fires at most once per session. Failures are swallowed silently.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { Api, Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { CONFIG_DIR_NAME, getPackageDir } from \"../../config.js\";\nimport type { ModelRegistry } from \"../../core/model-registry.js\";\nimport type { TabTitleSettings } from \"../../core/settings-manager.js\";\nimport { parseAgentFrontmatter, resolveModelForSubagentSpawn } from \"../../core/tools/subagent.js\";\n\nconst DEFAULT_TRIGGER_AFTER = 3;\nconst MAX_TITLE_LENGTH = 30;\nconst TITLE_GENERATION_TIMEOUT_MS = 60_000;\n\nconst TITLE_PROMPT =\n\t\"Summarize this session's task in ≤30 characters for a terminal tab title. \" +\n\t\"Output ONLY the title text, nothing else. No quotes, no explanation.\";\n\nexport interface TabTitleDeps {\n\t/** Set the terminal tab title (OSC 0). */\n\tsetTitle: (title: string) => void;\n\t/** Get the current session messages (for context). */\n\tgetMessages: () => Array<{ role: string; content?: unknown }>;\n\t/** Get the current model (parent session model — used as fallback). */\n\tgetModel: () => Model<Api> | undefined;\n\t/** Get model registry for API key resolution. */\n\tgetModelRegistry: () => ModelRegistry;\n\t/** Get the parent provider name. */\n\tgetProvider: () => string | undefined;\n\t/**\n\t * Get the user's agentModels settings override for a given agent name, if any.\n\t * Returns a non-empty fallback list when the user has configured an override.\n\t */\n\tgetAgentModelsOverride?: (agentName: string) => string[] | undefined;\n}\n\nexport class TabTitleGenerator {\n\tprivate toolCallCount = 0;\n\tprivate fired = false;\n\tprivate readonly threshold: number;\n\n\tconstructor(\n\t\tprivate readonly settings: TabTitleSettings | undefined,\n\t\tprivate readonly deps: TabTitleDeps,\n\t) {\n\t\tthis.threshold = settings?.triggerAfter ?? DEFAULT_TRIGGER_AFTER;\n\t}\n\n\t/** Whether this generator is enabled. */\n\tget enabled(): boolean {\n\t\treturn this.settings?.enabled !== false;\n\t}\n\n\t/**\n\t * Called on each tool_execution_end event. Increments the counter and fires\n\t * the title generation when threshold is reached.\n\t */\n\tonToolEnd(): void {\n\t\tif (this.fired || !this.enabled) return;\n\n\t\tthis.toolCallCount++;\n\t\tif (this.toolCallCount >= this.threshold) {\n\t\t\tthis.fired = true;\n\t\t\t// Fire-and-forget — never surfaces errors to the user\n\t\t\tthis.generateTitle().catch(() => {});\n\t\t}\n\t}\n\n\t/** Exposed for testing — the current tool call count. */\n\tget currentCount(): number {\n\t\treturn this.toolCallCount;\n\t}\n\n\t/** Exposed for testing — whether the title has been generated. */\n\tget hasFired(): boolean {\n\t\treturn this.fired;\n\t}\n\n\tprivate async generateTitle(): Promise<void> {\n\t\t// Single timeout bounds the entire pipeline (model probing + API key + LLM call)\n\t\tconst signal = AbortSignal.timeout(TITLE_GENERATION_TIMEOUT_MS);\n\n\t\tconst model = await this.resolveModel(signal);\n\t\tif (!model) return;\n\n\t\tconst registry = this.deps.getModelRegistry();\n\t\tconst apiKey = await registry.getApiKey(model);\n\n\t\tconst userContext = this.buildContext();\n\t\tif (!userContext) return;\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: TITLE_PROMPT,\n\t\t\tmessages: [{ role: \"user\", content: userContext, timestamp: Date.now() }],\n\t\t};\n\n\t\tconst response = await completeSimple(model, context, {\n\t\t\tapiKey,\n\t\t\tmaxRetryDelayMs: 0,\n\t\t\tsignal,\n\t\t});\n\n\t\tconst title = this.sanitizeTitle(response);\n\t\tif (title) {\n\t\t\tthis.deps.setTitle(`dreb - ${title}`);\n\t\t}\n\t}\n\n\tprivate async resolveModel(signal?: AbortSignal): Promise<Model<Api> | undefined> {\n\t\t// Try to get the Explore agent's model fallback list\n\t\tconst exploreModels = this.getExploreAgentModels();\n\t\tconst parentModel = this.deps.getModel();\n\t\tconst parentProvider = this.deps.getProvider();\n\t\tconst registry = this.deps.getModelRegistry();\n\n\t\tif (exploreModels) {\n\t\t\tconst resolution = await resolveModelForSubagentSpawn(\n\t\t\t\texploreModels,\n\t\t\t\tparentProvider,\n\t\t\t\tregistry,\n\t\t\t\tparentModel?.id,\n\t\t\t\tsignal,\n\t\t\t\t\"[tab-title]\",\n\t\t\t);\n\t\t\tif (resolution.ok) {\n\t\t\t\t// Find the resolved model in registry\n\t\t\t\tconst available = registry.getAvailable();\n\t\t\t\tconst found = available.find((m) => m.id === resolution.modelId);\n\t\t\t\tif (found) return found;\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to parent session model\n\t\treturn parentModel;\n\t}\n\n\tprivate getExploreAgentModels(): string | string[] | undefined {\n\t\t// Honor the user's agentModels settings override first. The settings key must\n\t\t// match the agent name exactly (\"Explore\", as declared in explore.md frontmatter).\n\t\tconst override = this.deps.getAgentModelsOverride?.(\"Explore\");\n\t\tif (override && override.length > 0) {\n\t\t\treturn override;\n\t\t}\n\n\t\t// Resolution order mirrors discoverAgentTypes: user override > project > package.\n\t\t// First match with a valid model wins.\n\t\tconst candidates = [\n\t\t\tjoin(homedir(), CONFIG_DIR_NAME, \"agents\", \"explore.md\"),\n\t\t\tjoin(process.cwd(), \".dreb\", \"agents\", \"explore.md\"),\n\t\t\tjoin(getPackageDir(), \"agents\", \"explore.md\"),\n\t\t];\n\n\t\tfor (const agentFile of candidates) {\n\t\t\ttry {\n\t\t\t\tconst content = readFileSync(agentFile, \"utf-8\");\n\t\t\t\tconst parsed = parseAgentFrontmatter(content);\n\t\t\t\tif (parsed.ok && parsed.config.model) {\n\t\t\t\t\treturn parsed.config.model;\n\t\t\t\t}\n\t\t\t} catch {}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate buildContext(): string | undefined {\n\t\tconst messages = this.deps.getMessages();\n\t\tif (messages.length === 0) return undefined;\n\n\t\t// Extract the first user message for context\n\t\tconst firstUser = messages.find((m) => m.role === \"user\");\n\t\tif (!firstUser) return undefined;\n\n\t\tconst content =\n\t\t\ttypeof firstUser.content === \"string\"\n\t\t\t\t? firstUser.content\n\t\t\t\t: Array.isArray(firstUser.content)\n\t\t\t\t\t? firstUser.content\n\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t.join(\"\\n\")\n\t\t\t\t\t: \"\";\n\n\t\tif (!content) return undefined;\n\n\t\t// Truncate long messages — the LLM only needs a summary\n\t\tconst truncated = content.length > 500 ? `${content.slice(0, 500)}...` : content;\n\t\treturn `Session task:\\n${truncated}`;\n\t}\n\n\t/** Clean up LLM response to a usable tab title. */\n\tprivate sanitizeTitle(response: unknown): string | undefined {\n\t\tif (!response || typeof response !== \"object\") return undefined;\n\n\t\tconst msg = response as { content?: Array<{ type: string; text?: string }> };\n\t\tif (!msg.content || !Array.isArray(msg.content)) return undefined;\n\n\t\tconst textPart = msg.content.find((c) => c.type === \"text\");\n\t\tif (!textPart?.text) return undefined;\n\n\t\tlet title = textPart.text.trim();\n\t\t// Strip surrounding quotes if present\n\t\tif ((title.startsWith('\"') && title.endsWith('\"')) || (title.startsWith(\"'\") && title.endsWith(\"'\"))) {\n\t\t\ttitle = title.slice(1, -1).trim();\n\t\t}\n\t\t// Remove newlines\n\t\ttitle = title.replace(/[\\r\\n]+/g, \" \").trim();\n\t\t// Truncate to max length\n\t\tif (title.length > MAX_TITLE_LENGTH) {\n\t\t\ttitle = title.slice(0, MAX_TITLE_LENGTH);\n\t\t}\n\t\treturn title || undefined;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Agent Model Settings
|
|
2
|
+
|
|
3
|
+
Configure per-agent model overrides for subagents via settings, without editing agent definition files.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
The `agentModels.models` setting lets you override the default model used by each subagent type (e.g., Explore, Sandbox) without modifying the agent definition `.md` files. You can specify an ordered fallback list — the first available model is used.
|
|
8
|
+
|
|
9
|
+
This applies to all subagents, including those launched by the mach6 skill workflow.
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Add to your `~/.dreb/settings.json`:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"agentModels": {
|
|
18
|
+
"models": {
|
|
19
|
+
"Explore": ["openai/gpt-4o", "anthropic/claude-sonnet-4-20250514"],
|
|
20
|
+
"Sandbox": ["anthropic/claude-haiku-3-20250422"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Each key is an agent type name, and the value is an ordered list of model IDs (in `provider/model` format). Project-level settings (`.dreb/settings.json`) are merged over global settings.
|
|
27
|
+
|
|
28
|
+
## Resolution Order
|
|
29
|
+
|
|
30
|
+
When a subagent is launched, its model is resolved in this priority:
|
|
31
|
+
|
|
32
|
+
1. **Per-invocation `model` override** — highest priority, set explicitly in the subagent tool call
|
|
33
|
+
2. **`agentModels.models` setting** — from your settings.json, per agent type
|
|
34
|
+
3. **Agent definition `model` field** — from the `.md` agent file's frontmatter
|
|
35
|
+
4. **Parent session model** — used when none of the above resolve to an available model
|
|
36
|
+
|
|
37
|
+
If the `agentModels.models` list is empty or undefined for a given agent, resolution falls through to the agent definition's model, then to the parent session model.
|
|
38
|
+
|
|
39
|
+
## TUI Usage
|
|
40
|
+
|
|
41
|
+
Open `/settings` and select the **Agent Models** submenu. Each discovered agent type gets its own entry where you can:
|
|
42
|
+
|
|
43
|
+
- **Reorder** models (move up/down to set priority)
|
|
44
|
+
- **Add** new models from the available model list
|
|
45
|
+
- **Remove** models from the fallback list
|
|
46
|
+
|
|
47
|
+
Changes are saved to your global settings immediately.
|
|
48
|
+
|
|
49
|
+
## Example
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"agentModels": {
|
|
54
|
+
"models": {
|
|
55
|
+
"Explore": [
|
|
56
|
+
"openai/gpt-4o-mini",
|
|
57
|
+
"anthropic/claude-haiku-3-20250422"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This configures the Explore agent to prefer `gpt-4o-mini`, falling back to `claude-haiku` if the first isn't available. All other agents use their default models.
|
package/docs/mach6.md
CHANGED
|
@@ -141,6 +141,8 @@ Agents run as [subagents](../README.md#subagents) — `code-reviewer`, `error-au
|
|
|
141
141
|
- **Safe git** — Never `git add -A`, never stage secrets, stage files by name.
|
|
142
142
|
- **Overridable** — Both skills and review agents can be overridden by placing files with the same name in `~/.dreb/agent/skills/` or `~/.dreb/agents/` (user-level) or `.dreb/skills/` or `.dreb/agents/` (project-level).
|
|
143
143
|
|
|
144
|
+
> The models used by mach6 subagents (e.g. `feature-dev`, the review agents) can be configured via the `agentModels` setting without editing agent definition files. See [Agent Model Settings](agent-models.md).
|
|
145
|
+
|
|
144
146
|
## Requirements
|
|
145
147
|
|
|
146
148
|
mach6 uses the [GitHub CLI](https://cli.github.com/) (`gh`) for all GitHub operations. Make sure it's installed and authenticated:
|
package/docs/settings.md
CHANGED
|
@@ -20,6 +20,24 @@ Edit directly or use `/settings` for common options.
|
|
|
20
20
|
| `defaultThinkingLevel` | string | - | `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"` |
|
|
21
21
|
| `hideThinkingBlock` | boolean | `false` | Hide thinking blocks in output |
|
|
22
22
|
| `thinkingBudgets` | object | - | Custom token budgets per thinking level |
|
|
23
|
+
| `agentModels.models` | object | - | Per-agent model fallback lists for subagents (map of agent name → ordered model IDs). See [agent-models.md](agent-models.md) |
|
|
24
|
+
|
|
25
|
+
#### agentModels.models
|
|
26
|
+
|
|
27
|
+
Override the model used by each subagent type without editing agent definition files. Each key is an agent type name; the value is an ordered fallback list of `provider/model` IDs (first available is used).
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"agentModels": {
|
|
32
|
+
"models": {
|
|
33
|
+
"Explore": ["openai/gpt-4o", "anthropic/claude-sonnet-4-20250514"],
|
|
34
|
+
"Sandbox": ["anthropic/claude-haiku-3-20250422"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Configurable in the TUI via `/settings` → **Agent Models**. See [agent-models.md](agent-models.md) for the full resolution order and details.
|
|
23
41
|
|
|
24
42
|
#### thinkingBudgets
|
|
25
43
|
|