Skip to content

Commit f1bfe12

Browse files
committed
Load conversation history into new agent session system prompts
Update pnpm-lock.yaml lint
1 parent 8b518fb commit f1bfe12

File tree

5 files changed

+94
-12
lines changed

5 files changed

+94
-12
lines changed

apps/twig/src/main/services/agent/schemas.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ export const credentialsSchema = z.object({
1313

1414
export type Credentials = z.infer<typeof credentialsSchema>;
1515

16+
// Content block schema (shared across prompt input and conversation history)
17+
export const contentBlockSchema = z
18+
.object({
19+
type: z.string(),
20+
text: z.string().optional(),
21+
_meta: z.record(z.string(), z.unknown()).nullish(),
22+
})
23+
.passthrough();
24+
25+
export const conversationHistoryTurnSchema = z.object({
26+
role: z.enum(["user", "assistant"]),
27+
content: z.array(contentBlockSchema),
28+
});
29+
30+
export type ConversationHistoryTurn = z.infer<
31+
typeof conversationHistoryTurnSchema
32+
>;
33+
1634
// Session config schema
1735
export const sessionConfigSchema = z.object({
1836
taskId: z.string(),
@@ -27,6 +45,8 @@ export const sessionConfigSchema = z.object({
2745
additionalDirectories: z.array(z.string()).optional(),
2846
/** Permission mode to use for the session (e.g. "default", "acceptEdits", "plan", "bypassPermissions") */
2947
permissionMode: z.string().optional(),
48+
/** Conversation history to inject into the system prompt for new sessions */
49+
conversationHistory: z.array(conversationHistoryTurnSchema).optional(),
3050
});
3151

3252
export type SessionConfig = z.infer<typeof sessionConfigSchema>;
@@ -45,6 +65,7 @@ export const startSessionInput = z.object({
4565
runMode: z.enum(["local", "cloud"]).optional(),
4666
adapter: z.enum(["claude", "codex"]).optional(),
4767
additionalDirectories: z.array(z.string()).optional(),
68+
conversationHistory: z.array(conversationHistoryTurnSchema).optional(),
4869
});
4970

5071
export type StartSessionInput = z.infer<typeof startSessionInput>;
@@ -102,14 +123,6 @@ export const sessionResponseSchema = z.object({
102123
export type SessionResponse = z.infer<typeof sessionResponseSchema>;
103124

104125
// Prompt input/output
105-
export const contentBlockSchema = z
106-
.object({
107-
type: z.string(),
108-
text: z.string().optional(),
109-
_meta: z.record(z.string(), z.unknown()).nullish(),
110-
})
111-
.passthrough();
112-
113126
export const promptInput = z.object({
114127
sessionId: z.string(),
115128
prompt: z.array(contentBlockSchema),
@@ -160,6 +173,7 @@ export const reconnectSessionInput = z.object({
160173
/** Additional directories Claude can access beyond cwd (for worktree support) */
161174
additionalDirectories: z.array(z.string()).optional(),
162175
permissionMode: z.string().optional(),
176+
conversationHistory: z.array(conversationHistoryTurnSchema).optional(),
163177
});
164178

165179
export type ReconnectSessionInput = z.infer<typeof reconnectSessionInput>;

apps/twig/src/main/services/agent/service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type { SleepService } from "../sleep/service.js";
3333
import {
3434
AgentServiceEvent,
3535
type AgentServiceEvents,
36+
type ConversationHistoryTurn,
3637
type Credentials,
3738
type InterruptReason,
3839
type PromptOutput,
@@ -178,6 +179,8 @@ interface SessionConfig {
178179
additionalDirectories?: string[];
179180
/** Permission mode to use for the session */
180181
permissionMode?: string;
182+
/** Conversation history to inject into the system prompt for new sessions */
183+
conversationHistory?: ConversationHistoryTurn[];
181184
}
182185

183186
interface ManagedSession {
@@ -423,6 +426,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
423426
adapter,
424427
additionalDirectories,
425428
permissionMode,
429+
conversationHistory,
426430
} = config;
427431

428432
if (!isRetry) {
@@ -599,6 +603,7 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
599603
taskRunId,
600604
systemPrompt,
601605
...(permissionMode && { permissionMode }),
606+
...(conversationHistory?.length && { conversationHistory }),
602607
...(additionalDirectories?.length && {
603608
claudeCode: {
604609
options: { additionalDirectories },
@@ -1320,6 +1325,10 @@ For git operations while detached:
13201325
: undefined,
13211326
permissionMode:
13221327
"permissionMode" in params ? params.permissionMode : undefined,
1328+
conversationHistory:
1329+
"conversationHistory" in params
1330+
? params.conversationHistory
1331+
: undefined,
13231332
};
13241333
}
13251334

packages/agent/src/adapters/claude/claude-agent.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
153153
permissionMode,
154154
canUseTool: this.createCanUseTool(internalSessionId),
155155
logger: this.logger,
156-
systemPrompt: buildSystemPrompt(meta?.systemPrompt),
156+
systemPrompt: buildSystemPrompt(
157+
meta?.systemPrompt,
158+
meta?.conversationHistory,
159+
),
157160
userProvidedOptions: meta?.claudeCode?.options,
158161
onModeChange: this.createOnModeChange(internalSessionId),
159162
onProcessSpawned: this.processCallbacks?.onProcessSpawned,

packages/agent/src/adapters/claude/session/options.ts

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { IS_ROOT } from "../../../utils/common.js";
1212
import type { Logger } from "../../../utils/logger.js";
1313
import { createPostToolUseHook, type OnModeChange } from "../hooks.js";
1414
import type { TwigExecutionMode } from "../tools.js";
15+
import type { ConversationHistoryTurn } from "../types.js";
1516

1617
export interface ProcessSpawnedInfo {
1718
pid: number;
@@ -40,21 +41,70 @@ const BRANCH_NAMING_INSTRUCTIONS = `
4041
Before pushing a "workspace-*" branch to origin, rename it to something descriptive based on the work done. Do this automatically without asking the user.
4142
`;
4243

44+
const MAX_HISTORY_CHARS = 50_000;
45+
46+
export function condenseAndFormatHistory(
47+
turns: ConversationHistoryTurn[],
48+
): string {
49+
const lines: string[] = [];
50+
51+
// Process turns in reverse so most recent messages appear first.
52+
// This ensures that if truncation occurs, older (less relevant) context is cut.
53+
for (let i = turns.length - 1; i >= 0; i--) {
54+
const turn = turns[i];
55+
const textParts: string[] = [];
56+
for (const block of turn.content) {
57+
if (block.type === "text" && block.text) {
58+
textParts.push(block.text);
59+
}
60+
}
61+
const text = textParts.join("\n");
62+
if (!text) continue;
63+
64+
const label = turn.role === "user" ? "[User]" : "[Assistant]";
65+
lines.push(`${label}: ${text}`);
66+
}
67+
68+
if (lines.length === 0) return "";
69+
70+
let result = lines.join("\n\n");
71+
72+
// Truncate from the end (oldest messages) if over the limit
73+
if (result.length > MAX_HISTORY_CHARS) {
74+
result = result.slice(0, MAX_HISTORY_CHARS);
75+
const lastNewline = result.lastIndexOf("\n");
76+
if (lastNewline !== -1) {
77+
result = result.slice(0, lastNewline);
78+
}
79+
}
80+
81+
return `<previous_conversation>\n${result}\n</previous_conversation>`;
82+
}
83+
4384
export function buildSystemPrompt(
4485
customPrompt?: unknown,
86+
conversationHistory?: ConversationHistoryTurn[],
4587
): Options["systemPrompt"] {
88+
const historyBlock =
89+
conversationHistory && conversationHistory.length > 0
90+
? condenseAndFormatHistory(conversationHistory)
91+
: "";
92+
93+
const appendSuffix =
94+
BRANCH_NAMING_INSTRUCTIONS + (historyBlock ? `\n${historyBlock}\n` : "");
95+
4696
const defaultPrompt: Options["systemPrompt"] = {
4797
type: "preset",
4898
preset: "claude_code",
49-
append: BRANCH_NAMING_INSTRUCTIONS,
99+
append: appendSuffix,
50100
};
51101

52102
if (!customPrompt) {
53103
return defaultPrompt;
54104
}
55105

56106
if (typeof customPrompt === "string") {
57-
return customPrompt + BRANCH_NAMING_INSTRUCTIONS;
107+
return customPrompt + appendSuffix;
58108
}
59109

60110
if (
@@ -65,7 +115,7 @@ export function buildSystemPrompt(
65115
) {
66116
return {
67117
...defaultPrompt,
68-
append: customPrompt.append + BRANCH_NAMING_INSTRUCTIONS,
118+
append: customPrompt.append + appendSuffix,
69119
};
70120
}
71121

packages/agent/src/adapters/claude/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,18 @@ export type ToolUpdateMeta = {
5050
};
5151
};
5252

53+
export type ConversationHistoryTurn = {
54+
role: "user" | "assistant";
55+
content: Array<{ type: string; text?: string; [key: string]: unknown }>;
56+
};
57+
5358
export type NewSessionMeta = {
5459
taskRunId?: string;
5560
disableBuiltInTools?: boolean;
5661
systemPrompt?: unknown;
5762
sessionId?: string;
5863
permissionMode?: string;
64+
conversationHistory?: ConversationHistoryTurn[];
5965
claudeCode?: {
6066
options?: Options;
6167
};

0 commit comments

Comments
 (0)