Skip to content
Merged
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
4 changes: 3 additions & 1 deletion packages/agent-modules/system-reminder/src/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ export function buildSlimAgentContextBlock(env: AgentEnv): string {
lines.push(` agentName: ${env.agentName}`);
}
lines.push(` SESSION ROLE: ${sessionRole}`);
lines.push(` YOUR SESSION ID: ${env.sessionId}`);
if (!env.sessionIdInContext) {
lines.push(` YOUR SESSION ID: ${env.sessionId}`);
}
if (env.parentSessionId) {
lines.push(` PARENT SESSION: ${env.parentSessionId}`);
lines.push(buildParentResultDeliveryLine(env, isCloud));
Expand Down
7 changes: 7 additions & 0 deletions packages/agent-modules/system-reminder/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ export interface AgentEnv {
userConfiguredName?: string | undefined;
agentRole: string;
sessionId: string;
/**
* Host-verified fact that the model-visible context already carries
* `YOUR SESSION ID` (e.g. an earlier user message since the latest
* compaction). When true, the slim per-turn block omits the line to save
* tokens. Absent/false keeps the line (cloud and legacy callers).
*/
sessionIdInContext?: boolean | undefined;
/** Session type — 0=Branch, 1=Root (formerly Main). */
sessionType?: number | undefined;
/** Parent session ID — set when this is a child session. */
Expand Down
16 changes: 16 additions & 0 deletions packages/local-runtime-v2/src/compat/v1/agent-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ describe("createV1AgentHostProductCapabilities", () => {
turnId: "turn-1",
deferTelemetry: true,
});
await product.inputPreparation.reminders.buildSystem({
session: { sessionId: "session-1" },
agent: { agentName: "mavis", resourceAgentName: "main" },
agentConfig: {},
promptText: "again",
turnId: "turn-2",
sessionIdInContext: true,
} as never);
expect(hosted.reminders.buildSystem).toHaveBeenLastCalledWith({
session: { sessionId: "session-1" },
resourceAgentName: "main",
promptText: "again",
turnId: "turn-2",
deferTelemetry: true,
sessionIdInContext: true,
});
expect(product.executor.fileChanges.begin).toBe(hosted.fileChanges.begin);
expect(product.runner.contextUsage).toBe(hosted.contextUsage);
expect(product.runner.logger).toBe(hosted.runnerLogger);
Expand Down
1 change: 1 addition & 0 deletions packages/local-runtime-v2/src/compat/v1/agent-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export function createV1AgentHostProductCapabilities(
promptText: input.promptText,
turnId: input.turnId,
deferTelemetry: true,
...(input.sessionIdInContext ? { sessionIdInContext: true } : {}),
...(model ? { model } : {}),
});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class NativeLocalTurnExecutionPreparationSource<
...(input.execution.desktopCapabilities
? { desktopCapabilities: input.execution.desktopCapabilities }
: {}),
history: (input.execution.runnerHistory ?? input.execution.history)?.messages ?? [],
});
const browserAssets = [...(prepared.browserAssets ?? [])];
const context = createToolContext(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ export interface LocalTurnReminderFacts {
readonly promptText: string;
readonly turnId: string;
readonly desktopCapabilities?: AgentHostTurnCapabilityView;
/**
* True when a user message in the model-visible history since the latest
* compaction already contains this session's ID, so per-turn reminders may
* omit it. False on the first turn and after a compaction dropped it.
*/
readonly sessionIdInContext?: boolean;
}) => Promise<{
readonly content: string;
readonly diagnostic?: unknown;
Expand Down Expand Up @@ -135,6 +141,8 @@ export interface LocalTurnInputPreparationRequest {
readonly immediateSendBatch?: AgentHostExecutionRequest['immediateSendBatch'];
readonly provenance: AgentHostTurnProvenance;
readonly desktopCapabilities?: AgentHostTurnCapabilityView;
/** Provider-facing history before this turn's user message. */
readonly history?: readonly unknown[];
}

export interface PreparedLocalTurnInput {
Expand Down Expand Up @@ -203,6 +211,9 @@ export class LocalTurnInputPreparer {
promptText,
turnId: input.lease.turnId,
...(input.desktopCapabilities ? { desktopCapabilities: input.desktopCapabilities } : {}),
...(input.history && historyCarriesSessionId(input.history, input.session.sessionId)
? { sessionIdInContext: true }
: {}),
});
const system = validateSystemReminder(systemValue);
const systemBlock = optionalReminderBlock('system', system.content);
Expand Down Expand Up @@ -544,6 +555,42 @@ function isImageAttachment(attachment: AgentHostInputAttachment): boolean {
);
}

/**
* Whether a user message after the latest compaction summary (or anywhere when
* the session was never compacted) already shows the model this session's ID.
* Durable user messages carry the turn's `<system-reminder>`, so the first
* turn's full agent-context keeps the ID visible until compaction drops it.
*/
export function historyCarriesSessionId(messages: readonly unknown[], sessionId: string): boolean {
if (!sessionId) return false;
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (!isRecord(message)) continue;
// Native summaries and legacy (archon) compaction markers both start a
// fresh model-visible context; anything before them was summarized away.
if (message.role === 'compactionSummary' || Object.hasOwn(message, 'archonCompaction')) {
return false;
}
if (message.role !== 'user') continue;
if (userMessageText(message.content).includes(sessionId)) return true;
}
return false;
}

function userMessageText(content: unknown): string {
if (typeof content === 'string') return content;
if (!Array.isArray(content)) return '';
return content
.map((part: unknown) =>
isRecord(part) && part.type === 'text' && typeof part.text === 'string' ? part.text : '',
)
.join('\n');
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}

function validateBackgroundReminder(
value: Awaited<ReturnType<LocalTurnReminderFacts['buildBackground']>>,
): Awaited<ReturnType<LocalTurnReminderFacts['buildBackground']>> {
Expand Down
Loading
Loading