Skip to content
Open
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
14 changes: 12 additions & 2 deletions packages/agent-core-v2/docs/state-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
// references become '(circular)', and class instances collapse to a '(ClassName)'
// marker — the wire shape of an entry is the JSON projection of the type here.
//
// Index (Session: 28 keys · Agent: 68 keys)
// Index (Session: 28 keys · Agent: 70 keys)
// Session
// cron.inFlight src/session/cron/sessionCronServiceImpl.ts
// cron.lastSeenAt src/session/cron/sessionCronServiceImpl.ts
Expand Down Expand Up @@ -59,9 +59,11 @@
// activityView.lastTurn src/agent/activityView/activityViewService.ts
// activityView.lifecycle src/agent/activityView/activityViewService.ts
// activityView.turn src/agent/activityView/activityViewService.ts
// agentsMdReminder.seededContent src/agent/profile/agentsMdReminderService.ts
// contextInjector.isNewTurn src/agent/contextInjector/contextInjectorService.ts
// contextProjector.lastRepairSignature src/agent/contextProjector/contextProjectorService.ts
// contextSize.lastEmittedTokens src/agent/contextSize/contextSizeService.ts
// dateChange.seededDate src/agent/dateChange/dateChangeService.ts
// externalHooks.stopHookContinuationUsed src/agent/externalHooks/externalHooksService.ts
// faultInjection.armed src/agent/faultInjection/faultInjectionService.ts
// faultInjection.fired src/agent/faultInjection/faultInjectionService.ts
Expand Down Expand Up @@ -582,6 +584,10 @@ export interface SessionStateSnapshot {
}[];
getKimiSkillsDescription: () => string;
getModelSkillListing: () => string;
getModelSkillDisclosure: () => /* ModelSkillDisclosure — packages/agent-core-v2/src/app/skillCatalog/types.ts */ {
readonly names: readonly string[];
readonly listing: string;
};
};
// src/session/sessionToolPolicy/sessionToolPolicyService.ts
'sessionToolPolicy.state': /* SessionToolPolicyState — packages/agent-core-v2/src/session/sessionToolPolicy/sessionToolPolicyService.ts */ {
Expand Down Expand Up @@ -978,6 +984,8 @@ export interface AgentStateSnapshot {
'contextProjector.lastRepairSignature': string | null;
// src/agent/contextSize/contextSizeService.ts
'contextSize.lastEmittedTokens': number;
// src/agent/dateChange/dateChangeService.ts
'dateChange.seededDate': string | undefined;
// src/agent/externalHooks/externalHooksService.ts
'externalHooks.stopHookContinuationUsed': boolean;
// src/agent/faultInjection/faultInjectionService.ts
Expand Down Expand Up @@ -1010,7 +1018,7 @@ export interface AgentStateSnapshot {
'llmRequester.lastConfigLogSignature': string | undefined;
'llmRequester.mediaDegradedTurns': Set<number>;
'llmRequester.mediaStrippedTurns': Map<number, /* MediaStripSnapshot — packages/agent-core-v2/src/agent/contextProjector/contextProjector.ts */ {
readonly "__@mediaStripSnapshotBrand@2671": undefined;
readonly "__@mediaStripSnapshotBrand@2720": undefined;
}>;
'llmRequester.turnConfigs': Map<number, /* TurnRequestConfig — packages/agent-core-v2/src/agent/llmRequester/llmRequesterService.ts */ {
readonly resolved: /* ProfileModelContext — packages/agent-core-v2/src/agent/profile/profile.ts */ {
Expand Down Expand Up @@ -1086,6 +1094,8 @@ export interface AgentStateSnapshot {
'permissionMode.lastMode': 'manual' | 'yolo' | 'auto' | undefined;
// src/agent/plan/injection/planModeInjection.ts
'plan.wasActive': boolean;
// src/agent/profile/agentsMdReminderService.ts
'agentsMdReminder.seededContent': string | undefined;
// src/agent/profile/profileService.ts
'profile.activeToolNamesOverlay': readonly string[] | undefined;
'profile.agentsMdWarning': string | undefined;
Expand Down
33 changes: 28 additions & 5 deletions packages/agent-core-v2/docs/wire-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
// owning model offloads inline media to blob storage), cross-reducers
// (foreign models that also reduce this record on dispatch and replay).

// Index (44 record types)
// Index (46 record types)
// config.update profile persisted src/agent/profile/profileOps.ts
// context_size.measured contextSize transient src/agent/contextSize/contextSizeOps.ts
// context.append_loop_event contextMemory persisted src/agent/contextMemory/contextOps.ts
Expand Down Expand Up @@ -51,8 +51,10 @@
// plan_mode.enter plan persisted src/agent/plan/planOps.ts
// plan_mode.exit plan persisted src/agent/plan/planOps.ts
// plan.revision plan persisted src/agent/plan/planOps.ts
// plugin.session_start.set_baseline agentPlugin persisted src/agent/plugin/agentPluginOps.ts
// profile.bind profile persisted src/agent/profile/profileOps.ts
// skill.activate skill transient src/agent/skill/skillOps.ts
// skill.disclosure.set skillDisclosure persisted src/agent/skillDisclosure/skillDisclosureOps.ts
// swarm_mode.enter swarm persisted src/agent/swarm/swarmOps.ts
// swarm_mode.exit swarm persisted src/agent/swarm/swarmOps.ts
// task.started task persisted src/agent/task/taskOps.ts
Expand Down Expand Up @@ -105,7 +107,7 @@ interface ContextAppendLoopEventPayload {
}

/**
* model: contextMemory · persisted · blobs · cross-reducers: plan, goalForkNotice, task.notificationDelivery, todo
* model: contextMemory · persisted · blobs · cross-reducers: plan, goalForkNotice, task.notificationDelivery, agentPlugin, todo
* owner: src/agent/contextMemory/contextOps.ts
*/
interface ContextAppendMessagePayload {
Expand Down Expand Up @@ -140,22 +142,22 @@ interface ContextAppendMessagePayload {
}

/**
* model: contextMemory · persisted · blobs · cross-reducers: plan, task.notificationDelivery, todo
* model: contextMemory · persisted · blobs · cross-reducers: plan, task.notificationDelivery, agentPlugin, todo
* owner: src/agent/contextMemory/contextOps.ts
* shared base: ...contextCompactionBaseShape
*/
type ContextApplyCompactionPayload = { _name: 'context.apply_compaction'; } & ({ summary: string, compactedCount: number, contextSummary?: string } | { contextSummary: string, compactedCount: number, summary?: string } | { summary: ContextMessage, count: number, compactedCount?: number });

/**
* model: contextMemory · persisted · blobs · cross-reducers: plan, task.notificationDelivery, todo
* model: contextMemory · persisted · blobs · cross-reducers: plan, task.notificationDelivery, agentPlugin, todo
* owner: src/agent/contextMemory/contextOps.ts
*/
interface ContextClearPayload {
_name: 'context.clear';
}

/**
* model: contextMemory · persisted · blobs · cross-reducers: plan, task.notificationDelivery, todo
* model: contextMemory · persisted · blobs · cross-reducers: plan, task.notificationDelivery, agentPlugin, todo
* owner: src/agent/contextMemory/contextOps.ts
*/
interface ContextUndoPayload {
Expand Down Expand Up @@ -439,6 +441,16 @@ interface PlanRevisionPayload {
bytes: number;
}

/**
* model: agentPlugin · persisted
* owner: src/agent/plugin/agentPluginOps.ts
*/
interface PluginSessionStartSetBaselinePayload {
_name: 'plugin.session_start.set_baseline';
fingerprint: string;
active: boolean;
}

/**
* model: profile · persisted · cross-reducers: profile.activeTools
* owner: src/agent/profile/profileOps.ts
Expand Down Expand Up @@ -475,6 +487,15 @@ interface SkillActivatePayload {
};
}

/**
* model: skillDisclosure · persisted
* owner: src/agent/skillDisclosure/skillDisclosureOps.ts
*/
interface SkillDisclosureSetPayload {
_name: 'skill.disclosure.set';
names: string[];
}

/**
* model: swarm · persisted · toEvent
* owner: src/agent/swarm/swarmOps.ts
Expand Down Expand Up @@ -644,8 +665,10 @@ interface WirePayloadMap {
"plan_mode.enter": PlanModeEnterPayload;
"plan_mode.exit": PlanModeExitPayload;
"plan.revision": PlanRevisionPayload;
"plugin.session_start.set_baseline": PluginSessionStartSetBaselinePayload;
"profile.bind": ProfileBindPayload;
"skill.activate": SkillActivatePayload;
"skill.disclosure.set": SkillDisclosureSetPayload;
"swarm_mode.enter": SwarmModeEnterPayload;
"swarm_mode.exit": SwarmModeExitPayload;
"task.started": TaskStartedPayload;
Expand Down
10 changes: 6 additions & 4 deletions packages/agent-core-v2/scripts/check-domain-layers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@

import { readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const __dirname = import.meta.dirname;
const PKG_ROOT = resolve(__dirname, '..');
export const SRC_ROOT = join(PKG_ROOT, 'src');
const TEST_ROOT = join(PKG_ROOT, 'test');
Expand Down Expand Up @@ -142,6 +141,7 @@ const DOMAIN_LAYER = new Map([
['model', 2],
['sessionIndex', 2],
['sessionStore', 2],
['pathWatch', 2],
// L3 — registries & capabilities
['tool', 3],
['skill', 3],
Expand Down Expand Up @@ -191,6 +191,8 @@ const DOMAIN_LAYER = new Map([
['contextInjector', 4],
['agentPlugin', 4],
['systemReminder', 4],
['skillDisclosure', 4],
['dateChange', 4],
['contextProjector', 4],
['contextSize', 4],
['fullCompaction', 4],
Expand Down Expand Up @@ -343,7 +345,7 @@ function kosongInfoOf(absPath) {
const segments = rel.split(/[\\/]/);
if (segments[0] !== 'kosong') return undefined;
const sub = segments[1];
const last = segments[segments.length - 1] ?? '';
const last = segments.at(-1) ?? '';
return {
// A file directly under `src/kosong/` has no subdomain.
sub: sub === undefined || sub.endsWith('.ts') ? undefined : sub,
Expand Down Expand Up @@ -763,7 +765,7 @@ function main() {
return 1;
}

const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
const isMain = process.argv[1] && resolve(process.argv[1]) === import.meta.filename;
if (isMain) {
process.exit(main());
}
16 changes: 16 additions & 0 deletions packages/agent-core-v2/src/agent/dateChange/dateChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* `dateChange` domain (L4) — `IAgentDateChangeService` contract.
*
* Defines the Agent-scope marker service that announces calendar-date changes
* through a `date_change` context-injection reminder when a session outlives
* the date rendered into its system prompt.
*/

import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';

export interface IAgentDateChangeService {
readonly _serviceBrand: undefined;
}

export const IAgentDateChangeService: ServiceIdentifier<IAgentDateChangeService> =
createDecorator<IAgentDateChangeService>('agentDateChangeService');
116 changes: 116 additions & 0 deletions packages/agent-core-v2/src/agent/dateChange/dateChangeService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* `dateChange` domain (L4) — `IAgentDateChangeService` implementation.
*
* Owns the `date_change` context-injection provider. The system prompt is only
* re-rendered at profile (re)bind and after compaction, so a session that runs
* past midnight keeps a stale date; this provider appends a system-reminder at
* the next step boundary instead. The baseline is history-derived: the last
* `date_change` reminder in context, else the date rendered into the current
* system prompt, else the volatile `seededDate` adopted at first evaluation
* (custom profiles without a date line). Dedup and resume safety fall out of
* the ladder — nothing is persisted beyond the reminder messages themselves.
* The plain-data state (`seededDate`) is registered into `agentState`
* (`IAgentStateService`) and read/written through it. Bound at Agent scope.
*/

import { Disposable } from '#/_base/di/lifecycle';
import { LifecycleScope, ScopeActivation, registerScopedService } from '#/_base/di/scope';
import { defineState } from '#/_base/state/stateRegistry';
import {
IAgentContextInjectorService,
type ContextInjectionContext,
} from '#/agent/contextInjector/contextInjector';
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
import type { ContextMessage } from '#/agent/contextMemory/types';
import { IAgentProfileService } from '#/agent/profile/profile';
import { IAgentStateService } from '#/agent/state/agentState';

import { IAgentDateChangeService } from './dateChange';

const DATE_CHANGE_INJECTION_VARIANT = 'date_change';

const SYSTEM_PROMPT_NOW_PATTERN = /current date and time in ISO format is `([^`]+)`/;
const REMINDER_DATE_PATTERN = /Today's date is now (\d{4}-\d{2}-\d{2})/;

export const dateChangeSeededDateKey = defineState<string | undefined>(
'dateChange.seededDate',
() => undefined,
);

export class AgentDateChangeService extends Disposable implements IAgentDateChangeService {
declare readonly _serviceBrand: undefined;

constructor(
@IAgentContextInjectorService dynamicInjector: IAgentContextInjectorService,
@IAgentContextMemoryService private readonly context: IAgentContextMemoryService,
@IAgentProfileService private readonly profile: IAgentProfileService,
@IAgentStateService private readonly states: IAgentStateService,
) {
super();
this.states.register(dateChangeSeededDateKey);
this._register(
dynamicInjector.register(DATE_CHANGE_INJECTION_VARIANT, (ctx) => this.reminder(ctx)),
);
}

private get seededDate(): string | undefined {
return this.states.get(dateChangeSeededDateKey);
}

private set seededDate(value: string | undefined) {
this.states.set(dateChangeSeededDateKey, value);
}

private reminder({ lastInjectedAt }: ContextInjectionContext): string | undefined {
const today = localDateKey(new Date());
const baseline = this.baseline(lastInjectedAt) ?? this.adopt(today);
if (baseline === today) return undefined;
return `The date has changed. Today's date is now ${today}. The date and time stated in your system prompt are stale; rely on this reminder for the current date. DO NOT mention this to the user explicitly.`;
}

private baseline(lastInjectedAt: number | null): string | undefined {
return this.dateFromHistory(lastInjectedAt) ?? this.dateFromSystemPrompt() ?? this.seededDate;
}

private adopt(today: string): string {
this.seededDate = today;
return today;
}

private dateFromHistory(lastInjectedAt: number | null): string | undefined {
if (lastInjectedAt === null) return undefined;
const message: ContextMessage | undefined = this.context.get()[lastInjectedAt];
if (message === undefined) return undefined;
const match = REMINDER_DATE_PATTERN.exec(messageText(message));
return match?.[1];
}

private dateFromSystemPrompt(): string | undefined {
const match = SYSTEM_PROMPT_NOW_PATTERN.exec(this.profile.getSystemPrompt());
if (match?.[1] === undefined) return undefined;
const rendered = new Date(match[1]);
if (Number.isNaN(rendered.getTime())) return undefined;
return localDateKey(rendered);
}
}

function messageText(message: ContextMessage): string {
return message.content
.map((part) => (part.type === 'text' ? part.text : ''))
.join('');
}

function localDateKey(date: Date): string {
const year = date.getFullYear();
const month = `${date.getMonth() + 1}`.padStart(2, '0');
const day = `${date.getDate()}`.padStart(2, '0');
return `${year}-${month}-${day}`;
}

registerScopedService(
LifecycleScope.Agent,
IAgentDateChangeService,
AgentDateChangeService,
ScopeActivation.OnScopeCreated,
'dateChange',
);
48 changes: 48 additions & 0 deletions packages/agent-core-v2/src/agent/plugin/agentPluginOps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* `agentPlugin` domain (L4) — persistent plugin session-start baseline.
*
* Defines the checkpointed Agent wire model used by `agentPlugin` to keep the
* last model-facing session-start fingerprint aligned with replay, resume, and
* conversation undo.
*/

import { z } from 'zod';

import { defineCheckpointedModel } from '#/agent/contextMemory/conversationTime';

export interface AgentPluginModelState {
readonly sessionStartFingerprint?: string;
readonly sessionStartActive: boolean;
}

export const AgentPluginModel = defineCheckpointedModel<AgentPluginModelState>(
'agentPlugin',
() => ({ sessionStartActive: false }),
);

export const setPluginSessionStartBaseline = AgentPluginModel.defineOp(
'plugin.session_start.set_baseline',
{
schema: z.object({
fingerprint: z.string(),
active: z.boolean(),
}),
apply: (state, payload) =>
state.current.sessionStartFingerprint === payload.fingerprint &&
state.current.sessionStartActive === payload.active
? state
: {
...state,
current: {
sessionStartFingerprint: payload.fingerprint,
sessionStartActive: payload.active,
},
},
},
);

declare module '#/wire/types' {
interface PersistedOpMap {
'plugin.session_start.set_baseline': typeof setPluginSessionStartBaseline;
}
}
Loading
Loading