Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions packages/core/src/agent/conversation-memory-checkpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,71 @@ describe("normalizeCheckpoint extras", () => {
});

describe("buildCheckpointFromMessages", () => {
it("extracts objective from user messages and tool outcomes", () => {
it("does not promote ordinary chat into SUMMARY objectives", () => {
const messages: ChatMessage[] = [
{ role: "user", content: "你是谁?" },
{ role: "assistant", content: "我是 Step CLI 助手。" },
{ role: "user", content: "你去读取一下README的文件内容。" },
];

const result = buildCheckpointFromMessages(messages, {
fromIndex: 0,
toIndex: 3,
});

expect(result.objective).toEqual([]);
const summary = renderCheckpointText(
mergeCheckpoints(createEmptyCheckpoint(), result),
);
expect(summary).not.toMatch(/Current Objective|Superseded Objectives/);
});

it("extracts explicit /goal and labeled objective signals", () => {
const messages: ChatMessage[] = [
{ role: "user", content: "你是谁?" },
{ role: "user", content: "/goal 读取 README 并总结" },
{ role: "user", content: "Goal: ship the release tonight" },
];

const result = buildCheckpointFromMessages(messages, {
fromIndex: 0,
toIndex: 3,
});

expect(result.objective?.map((entry) => entry.text)).toEqual([
"读取 README 并总结",
"ship the release tonight",
]);
expect(result.objective?.[0]!.status).toBe("superseded");
expect(result.objective?.[1]!.status).toBe("still_active");
});

it("extracts the goal text from a /goal wake prompt", () => {
const messages: ChatMessage[] = [
{
role: "user",
content: [
"You are working toward this long-running session goal:",
"",
"Migrate the auth flow",
"",
"Goal id: goal-123",
"Goal iteration: 1",
].join("\n"),
},
];

const result = buildCheckpointFromMessages(messages, {
fromIndex: 0,
toIndex: 1,
});

expect(result.objective).toEqual([
{ text: "Migrate the auth flow", status: "still_active" },
]);
});

it("extracts tool outcomes without treating the user prompt as an objective", () => {
const messages: ChatMessage[] = [
{ role: "user", content: "Please fix the login bug" },
{
Expand Down Expand Up @@ -353,7 +417,7 @@ describe("buildCheckpointFromMessages", () => {
toIndex: 5,
});

expect(result.objective?.[0]!.text).toContain("login bug");
expect(result.objective).toEqual([]);
// Planned tools + assistant preview + tool summaries
const actionTexts = (result.attemptedActions ?? []).map((a) => a.text);
expect(actionTexts.some((t) => t.includes("Planned tools: Read"))).toBe(
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/agent/conversation-memory-checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
import { parseToolResult } from "./conversation-memory-tool-result.js";
import {
dedupeMessagesKeepingNewest,
extractExplicitObjectiveText,
renderTranscriptEntrySummary,
} from "./conversation-memory-transcript.js";

Expand Down Expand Up @@ -581,8 +582,9 @@ function extractObjectiveCandidates(
}

const preview = userMessagePreviewText(message).trim();
if (preview.length > 0) {
candidates.push(preview);
const objective = extractExplicitObjectiveText(preview);
if (objective) {
candidates.push(objective);
}
}

Expand Down
70 changes: 70 additions & 0 deletions packages/core/src/agent/conversation-memory-transcript.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, expect, it } from "vitest";
import type { ChatMessage } from "@step-cli/protocol";
import {
buildTranscriptSaveArtifact,
extractExplicitObjectiveText,
} from "./conversation-memory-transcript.js";

describe("extractExplicitObjectiveText", () => {
it("ignores ordinary chat prompts from issue #73", () => {
expect(extractExplicitObjectiveText("你是谁?")).toBeUndefined();
expect(
extractExplicitObjectiveText("你去读取一下README的文件内容。"),
).toBeUndefined();
expect(extractExplicitObjectiveText("what's next?")).toBeUndefined();
});

it("extracts /goal text and ignores control verbs", () => {
expect(extractExplicitObjectiveText("/goal 读取 README")).toBe(
"读取 README",
);
expect(extractExplicitObjectiveText("/goal status")).toBeUndefined();
expect(extractExplicitObjectiveText("/goal pause waiting")).toBeUndefined();
expect(
extractExplicitObjectiveText("/goal start sess-1 ship the feature"),
).toBe("sess-1 ship the feature");
});

it("extracts labeled and wake-prompt objectives", () => {
expect(extractExplicitObjectiveText("Goal: ship the release")).toBe(
"ship the release",
);
expect(extractExplicitObjectiveText("当前目标:迁移鉴权")).toBe("迁移鉴权");
expect(
extractExplicitObjectiveText(
[
"You are working toward this long-running session goal:",
"",
"Keep the SUMMARY panel accurate",
"",
"Goal id: goal-1",
"Goal iteration: 0",
].join("\n"),
),
).toBe("Keep the SUMMARY panel accurate");
});
});

describe("buildTranscriptSaveArtifact", () => {
it("does not list ordinary user chat under Goals", () => {
const messages: ChatMessage[] = [
{ role: "user", content: "你是谁?" },
{ role: "assistant", content: "助手" },
{ role: "user", content: "/goal 读取 README" },
];

const artifact = buildTranscriptSaveArtifact({
workspaceRoot: "/tmp",
sessionId: "sess",
summarizedFrom: 0,
summarizedTo: 3,
savedAt: "2026-06-19T10:12:37.000Z",
messages,
});

expect(artifact.entry.summaryPreview).toMatch(/Goals:\n- 读取 README/u);
expect(artifact.entry.summaryPreview).toContain("User turns:");
expect(artifact.entry.summaryPreview).toContain("你是谁?");
expect(artifact.entry.summaryPreview).not.toMatch(/Goals:\n- 你是谁?/u);
});
});
70 changes: 69 additions & 1 deletion packages/core/src/agent/conversation-memory-transcript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,82 @@ export function buildTranscriptSaveArtifact(
};
}

const GOAL_WAKE_INTRO =
"You are working toward this long-running session goal:";
const GOAL_CONTROL_VERBS = new Set(["status", "pause", "resume", "stop"]);

export function extractExplicitObjectiveText(text: string): string | undefined {
const raw = text.trim();
if (raw.length === 0) {
return undefined;
}

return (
extractSlashGoalText(raw) ??
extractGoalWakeText(raw) ??
extractLabeledObjectiveText(raw)
);
}

function extractSlashGoalText(text: string): string | undefined {
const firstLine = text.split(/\n/u, 1)[0]?.trim() ?? "";
const match = firstLine.match(/^\/goal(?:\s+(.+))?$/iu);
const rest = match?.[1]?.trim();
if (!rest) {
return undefined;
}

const [verb, ...remainder] = rest.split(/\s+/u);
if (!verb || GOAL_CONTROL_VERBS.has(verb.toLowerCase())) {
return undefined;
}

if (verb.toLowerCase() === "start") {
const started = remainder.join(" ").trim();
return started.length > 0 ? started : undefined;
}

return rest;
}

function extractGoalWakeText(text: string): string | undefined {
const introIndex = text.toLowerCase().indexOf(GOAL_WAKE_INTRO.toLowerCase());
if (introIndex < 0) {
return undefined;
}

const afterIntro = text.slice(introIndex + GOAL_WAKE_INTRO.length).trim();
const goalBody = afterIntro.split(/\n\s*Goal id:/iu)[0]?.trim();
return goalBody && goalBody.length > 0
? normalizeWhitespace(goalBody)
: undefined;
}

function extractLabeledObjectiveText(text: string): string | undefined {
const labeled =
/(?:^|\n)\s*(?:current\s+(?:objective|goal)|objective|goal)\s*[::]\s*(.+)$/imu.exec(
text,
) ?? /(?:^|\n)\s*(?:当前目标|目标)\s*[::]\s*(.+)$/mu.exec(text);
const value = labeled?.[1]?.trim();
return value && value.length > 0 ? normalizeWhitespace(value) : undefined;
}

function summarizeMessages(messages: ChatMessage[]): string {
const goals: string[] = [];
const userTurns: string[] = [];
const actions: string[] = [];
const outcomes: string[] = [];
const issues: string[] = [];

for (const message of messages) {
if (message.role === "user") {
goals.push(shortenLine(userMessagePreviewText(message), 200));
const preview = userMessagePreviewText(message);
const objective = extractExplicitObjectiveText(preview);
if (objective) {
goals.push(shortenLine(objective, 200));
} else if (preview.trim().length > 0) {
userTurns.push(shortenLine(preview, 200));
}
continue;
}

Expand Down Expand Up @@ -227,6 +294,7 @@ function summarizeMessages(messages: ChatMessage[]): string {

const lines: string[] = [];
pushSection(lines, "Goals", goals, 6);
pushSection(lines, "User turns", userTurns, 6);
pushSection(lines, "Actions", actions, 10);
pushSection(lines, "Outcomes", outcomes, 8);
pushSection(lines, "Issues", issues, 6);
Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/agent/conversation-memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,42 @@ describe("ConversationMemory", () => {
const result = memory.forceCompact("test");
expect(result.compactedMessages).toBeGreaterThan(0);
});

it("does not promote ordinary chat into the SUMMARY objective sections", () => {
const memory = new ConversationMemory(
makeConfig({ minRecentMessages: 1 }),
);
memory.addUser("你是谁?");
memory.addAssistant("我是助手。");
memory.addUser("你去读取一下README的文件内容。");
memory.addAssistant("好的。");

const result = memory.forceCompact("issue-73");
expect(result.compactedMessages).toBeGreaterThan(0);

const summary = memory.exportState().summary;
expect(summary).not.toMatch(/Current Objective|Superseded Objectives/);
expect(summary).not.toContain("你是谁?");
expect(summary).not.toContain("你去读取一下README的文件内容。");
});

it("keeps explicit /goal text as the current SUMMARY objective", () => {
const memory = new ConversationMemory(
makeConfig({ minRecentMessages: 1 }),
);
memory.addUser("你是谁?");
memory.addAssistant("我是助手。");
memory.addUser("/goal 读取 README 并总结");
memory.addAssistant("开始读取。");

memory.forceCompact("issue-73");

const summary = memory.exportState().summary;
expect(summary).toContain("Current Objective:");
expect(summary).toContain("读取 README 并总结");
expect(summary).not.toContain("你是谁?");
expect(summary).not.toMatch(/Superseded Objectives/);
});
});

describe("loadState with checkpoint", () => {
Expand Down
Loading