Archived
fix(sessions): name relay handoffs deterministically
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Name Relay handoff sessions deterministically from their relay name and leg number.
|
||||
@@ -24,7 +24,7 @@ import { SessionArchiveStore, type ArchivedSessionRecord, type ArchiveSessionInp
|
||||
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree, type SessionArchiveTreeCandidate } from "./sessionArchiveTree.js";
|
||||
import type { ActiveSession } from "./sessionRuntimeStore.js";
|
||||
import type { AuthChange } from "./authService.js";
|
||||
import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
||||
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
||||
import { computeEditPreview, type EditPreviewResult } from "./editPreview.js";
|
||||
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
||||
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
||||
@@ -1706,6 +1706,13 @@ export class PiSessionService {
|
||||
|
||||
private maybeGenerateSessionName(session: PiAgentSession, firstMessage: string): void {
|
||||
if (session.sessionName !== undefined || session.messages.length !== 0 || session.isStreaming || session.isCompacting) return;
|
||||
|
||||
const deterministicName = deterministicSessionName(firstMessage);
|
||||
if (deterministicName !== undefined) {
|
||||
this.applyGeneratedSessionName(session, deterministicName);
|
||||
return;
|
||||
}
|
||||
|
||||
const model = session.model;
|
||||
if (model === undefined) return;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
|
||||
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
||||
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { cleanSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
||||
import { cleanSessionName, deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
||||
|
||||
function fakeModel(): Model<Api> {
|
||||
return { id: "fake-model", name: "Fake Model", api: "anthropic-messages", provider: "anthropic", baseUrl: "https://example.test", reasoning: false, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 1000, maxTokens: 100 };
|
||||
@@ -69,6 +69,21 @@ describe("sessionNameGenerator", () => {
|
||||
expect(cleanSessionName('Title: "Fix Session Naming."\nextra')).toBe("Fix Session Naming");
|
||||
});
|
||||
|
||||
it("builds deterministic names for relay handoff prompts", () => {
|
||||
expect(deterministicSessionName('Relay "handoff-check" leg 2 begins now.\n\nYou are the next runner.'))
|
||||
.toBe("Relay handoff-check leg 2");
|
||||
});
|
||||
|
||||
it("preserves the relay leg when truncating deterministic relay names", () => {
|
||||
expect(deterministicSessionName('Relay "very-long-relay-name-that-would-otherwise-push-the-leg-number-out-of-view" leg 42 begins now.'))
|
||||
.toBe("Relay very-long-relay-name-that-would-otherwise-push leg 42");
|
||||
});
|
||||
|
||||
it("does not build deterministic names for non-canonical relay prompts", () => {
|
||||
expect(deterministicSessionName('You are continuing Relay "handoff-check" under the Relay method.'))
|
||||
.toBeUndefined();
|
||||
});
|
||||
|
||||
it("builds a concise fallback from the first request", () => {
|
||||
expect(fallbackSessionName("Seems like auto name for sessions is not working, I still get the first message as a name."))
|
||||
.toBe("Seems like auto name for sessions");
|
||||
|
||||
@@ -5,6 +5,13 @@ const SESSION_NAME_TIMEOUT_MS = 10_000;
|
||||
const SESSION_NAME_MAX_INPUT_CHARS = 4_000;
|
||||
const SESSION_NAME_MAX_LENGTH = 60;
|
||||
const FALLBACK_SESSION_NAME_MAX_WORDS = 6;
|
||||
const RELAY_HANDOFF_FIRST_LINE = /^Relay\s+"([^"\n]+)"\s+leg\s+(\d+)\s+begins now\.?\s*(?:\n|$)/;
|
||||
|
||||
export function deterministicSessionName(firstMessage: unknown): string | undefined {
|
||||
if (typeof firstMessage !== "string") return undefined;
|
||||
|
||||
return relayHandoffSessionName(firstMessage.trimStart());
|
||||
}
|
||||
|
||||
export async function generateShortSessionName<TApi extends Api>(streamFn: StreamFn, model: Model<TApi>, firstMessage: string): Promise<string | undefined> {
|
||||
const stream = await streamFn(
|
||||
@@ -59,6 +66,31 @@ export function cleanSessionName(value: string): string | undefined {
|
||||
return title === "" ? undefined : title;
|
||||
}
|
||||
|
||||
function relayHandoffSessionName(firstMessage: string): string | undefined {
|
||||
const match = RELAY_HANDOFF_FIRST_LINE.exec(firstMessage);
|
||||
if (match === null) return undefined;
|
||||
|
||||
const relayName = match[1]?.replace(/\s+/g, " ").trim();
|
||||
const legNumber = match[2];
|
||||
if (relayName === undefined || relayName === "" || legNumber === undefined) return undefined;
|
||||
|
||||
return cleanSessionName(formatRelaySessionName(relayName, legNumber));
|
||||
}
|
||||
|
||||
function formatRelaySessionName(relayName: string, legNumber: string): string {
|
||||
const prefix = "Relay ";
|
||||
const suffix = ` leg ${legNumber}`;
|
||||
const maxRelayNameLength = Math.max(1, SESSION_NAME_MAX_LENGTH - prefix.length - suffix.length);
|
||||
const displayedRelayName = truncateRelayName(relayName, maxRelayNameLength);
|
||||
return `${prefix}${displayedRelayName}${suffix}`;
|
||||
}
|
||||
|
||||
function truncateRelayName(relayName: string, maxLength: number): string {
|
||||
if (relayName.length <= maxLength) return relayName;
|
||||
const truncated = relayName.slice(0, maxLength).replace(/[\s._-]+$/g, "").trim();
|
||||
return truncated === "" ? relayName.slice(0, maxLength).trim() : truncated;
|
||||
}
|
||||
|
||||
function textFromAssistant(message: AssistantMessage): string {
|
||||
return message.content
|
||||
.filter((part) => part.type === "text")
|
||||
|
||||
Reference in New Issue
Block a user