feat(coding-agent): add configurable title system prompt for sessions
- add discovery of `TITLE_SYSTEM.md` and pass it through interactive startup context - route custom title prompts to online and local tiny title generators via protocol - update session-title docs and changelog with override behavior - add tests for prompt discovery, forwarding, and fallback to bundled title prompts
This commit is contained in:
@@ -39,6 +39,11 @@ export interface TinyTitleDownloadOptions {
|
||||
onProgress?: (event: TinyTitleProgressEvent) => void;
|
||||
}
|
||||
|
||||
export interface TinyTitleGenerateOptions {
|
||||
signal?: AbortSignal;
|
||||
systemPrompt?: string;
|
||||
}
|
||||
|
||||
// Cold-starting the worker subprocess from a compiled binary (decompress + module
|
||||
// graph load) is slow on contended CI runners — the macos-15-intel release smoke
|
||||
// blew past 5s while arm64/linux/win passed. The probe only needs to prove the
|
||||
@@ -46,6 +51,14 @@ export interface TinyTitleDownloadOptions {
|
||||
// generous bound removes the flake without weakening the check.
|
||||
const SMOKE_TEST_TIMEOUT_MS = 30_000;
|
||||
|
||||
function normalizeTinyTitleGenerateOptions(
|
||||
options: AbortSignal | TinyTitleGenerateOptions | undefined,
|
||||
): TinyTitleGenerateOptions {
|
||||
if (!options) return {};
|
||||
if ("aborted" in options && "addEventListener" in options) return { signal: options };
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden subcommand on the main CLI that boots the tiny-model worker in the
|
||||
* spawned subprocess. Kept in sync with the dispatch in `cli.ts`.
|
||||
@@ -295,9 +308,16 @@ export class TinyTitleClient {
|
||||
return () => this.#progressListeners.delete(listener);
|
||||
}
|
||||
|
||||
async generate(modelKey: string, message: string, signal?: AbortSignal): Promise<string | null> {
|
||||
async generate(modelKey: string, message: string, signal?: AbortSignal): Promise<string | null>;
|
||||
async generate(modelKey: string, message: string, options?: TinyTitleGenerateOptions): Promise<string | null>;
|
||||
async generate(
|
||||
modelKey: string,
|
||||
message: string,
|
||||
optionsOrSignal?: AbortSignal | TinyTitleGenerateOptions,
|
||||
): Promise<string | null> {
|
||||
const options = normalizeTinyTitleGenerateOptions(optionsOrSignal);
|
||||
if (!isTinyTitleLocalModelKey(modelKey)) return null;
|
||||
if (signal?.aborted) return null;
|
||||
if (options.signal?.aborted) return null;
|
||||
|
||||
try {
|
||||
const worker = this.#ensureWorker();
|
||||
@@ -310,12 +330,15 @@ export class TinyTitleClient {
|
||||
this.#pending.delete(id);
|
||||
pending.resolve(null);
|
||||
};
|
||||
signal?.addEventListener("abort", abort, { once: true });
|
||||
options.signal?.addEventListener("abort", abort, { once: true });
|
||||
try {
|
||||
worker.send({ type: "generate", id, modelKey, message });
|
||||
const request: TinyTitleWorkerInbound = options.systemPrompt
|
||||
? { type: "generate", id, modelKey, message, systemPrompt: options.systemPrompt }
|
||||
: { type: "generate", id, modelKey, message };
|
||||
worker.send(request);
|
||||
return await promise;
|
||||
} finally {
|
||||
signal?.removeEventListener("abort", abort);
|
||||
options.signal?.removeEventListener("abort", abort);
|
||||
this.#pending.delete(id);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface TinyTitleProgressEvent {
|
||||
|
||||
export type TinyTitleWorkerInbound =
|
||||
| { type: "ping"; id: string }
|
||||
| { type: "generate"; id: string; modelKey: TinyTitleLocalModelKey; message: string }
|
||||
| { type: "generate"; id: string; modelKey: TinyTitleLocalModelKey; message: string; systemPrompt?: string }
|
||||
| { type: "complete"; id: string; modelKey: TinyLocalModelKey; prompt: string; maxTokens?: number }
|
||||
| { type: "download"; id: string; modelKey: TinyLocalModelKey };
|
||||
|
||||
|
||||
@@ -436,9 +436,10 @@ async function loadPipeline(
|
||||
return loaded;
|
||||
}
|
||||
|
||||
function buildPrompt(generator: TextGenerationPipeline, message: string): string {
|
||||
function buildPrompt(generator: TextGenerationPipeline, message: string, systemPrompt?: string): string {
|
||||
const selectedSystemPrompt = systemPrompt?.trim() || TINY_TITLE_SYSTEM_PROMPT;
|
||||
const chat = [
|
||||
{ role: "system", content: TINY_TITLE_SYSTEM_PROMPT },
|
||||
{ role: "system", content: selectedSystemPrompt },
|
||||
{ role: "user", content: formatTitleUserMessage(message) },
|
||||
];
|
||||
const chatTemplateOptions = {
|
||||
@@ -464,9 +465,10 @@ async function generateTitle(
|
||||
requestId: string,
|
||||
modelKey: TinyTitleLocalModelKey,
|
||||
message: string,
|
||||
systemPrompt?: string,
|
||||
): Promise<string | null> {
|
||||
const generator = await loadPipeline(modelKey, transport, requestId);
|
||||
const promptText = buildPrompt(generator, message);
|
||||
const promptText = buildPrompt(generator, message, systemPrompt);
|
||||
const transformers = await loadTransformers(transport, requestId, modelKey);
|
||||
const output = (await generator(promptText, {
|
||||
max_new_tokens: TITLE_MAX_NEW_TOKENS,
|
||||
@@ -548,7 +550,7 @@ async function handleQueuedRequest(
|
||||
transport.send({ type: "completion", id: request.id, text });
|
||||
return;
|
||||
}
|
||||
const title = await generateTitle(transport, request.id, request.modelKey, request.message);
|
||||
const title = await generateTitle(transport, request.id, request.modelKey, request.message, request.systemPrompt);
|
||||
transport.send({ type: "title", id: request.id, title });
|
||||
} catch (error) {
|
||||
transport.send({ type: "error", id: request.id, error: errorText(error) });
|
||||
|
||||
Reference in New Issue
Block a user