Skip to content

Commit a743499

Browse files
committed
feat(agent-core-v2): meta-mark bundled skill parts and replace positional slicing
Bundled skill blocks are now constructed with meta { source: 'skill activation' }, matching the hook-part convention. Every former content.slice(skillActivations.length) site — turn prompt text, steer merge/echo, session and fork titles, transcript fold, TUI replay — now annotates then filters by meta, so prompt content no longer depends on block positions and the loop needs no hook-part knowledge. A shared annotate step keeps legacy wires working: leading unmarked blocks (one per bundled activation) are marked on read before filtering.
1 parent 93da96e commit a743499

14 files changed

Lines changed: 262 additions & 90 deletions

File tree

‎apps/kimi-code/src/tui/utils/message-replay.ts‎

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,44 @@ export function isUserPromptSubmitHookPart(
248248
);
249249
}
250250

251+
const SKILL_ACTIVATION_PART_SOURCE = 'skill activation';
252+
253+
export function isSkillActivationPart(part: ContentPart): boolean {
254+
return (
255+
part.type === 'text' &&
256+
(part as { meta?: { source?: unknown } }).meta?.source === SKILL_ACTIVATION_PART_SOURCE
257+
);
258+
}
259+
260+
function annotateBundledSkillParts(
261+
content: readonly ContentPart[],
262+
bundledActivations: readonly BundledSkillActivationRef[],
263+
): ContentPart[] {
264+
if (bundledActivations.length === 0 || content.some(isSkillActivationPart)) {
265+
return [...content];
266+
}
267+
let index = 0;
268+
return content.map((part) => {
269+
const activation = bundledActivations[index];
270+
if (
271+
activation !== undefined &&
272+
part.type === 'text' &&
273+
(part as { meta?: { source?: unknown } }).meta?.source === undefined
274+
) {
275+
index += 1;
276+
return {
277+
...part,
278+
meta: { source: SKILL_ACTIVATION_PART_SOURCE, activationId: activation.activationId },
279+
};
280+
}
281+
return part;
282+
});
283+
}
284+
285+
interface BundledSkillActivationRef {
286+
readonly activationId: string;
287+
}
288+
251289
export function withoutUserPromptSubmitHookParts(
252290
content: readonly ContentPart[],
253291
): ContentPart[] {
@@ -321,13 +359,15 @@ export function bundledSkillsFromOrigin(
321359
}
322360

323361
/**
324-
* Content parts the caller actually typed: the engine prepends one rendered
325-
* text part per bundled skill, so the caller's own parts start right after
326-
* them.
362+
* Content parts the caller actually typed: skill blocks are meta-marked at
363+
* construction; legacy wires carry no marks, so the leading unmarked parts
364+
* (one per bundled activation) are annotated first, then filtered out.
327365
*/
328366
export function stripBundledSkillParts(message: ContextMessage): readonly ContentPart[] {
329-
const bundledCount = bundledSkillsFromOrigin(message.origin).length;
330-
return bundledCount === 0 ? message.content : message.content.slice(bundledCount);
367+
return annotateBundledSkillParts(
368+
message.content,
369+
bundledSkillsFromOrigin(message.origin),
370+
).filter((part) => !isSkillActivationPart(part));
331371
}
332372

333373
export function pluginCommandFromOrigin(

‎apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,11 @@ describe('KimiTUI message flow', () => {
10151015
message: {
10161016
role: 'user',
10171017
content: [
1018-
{ type: 'text', text: 'skill card C body' },
1018+
{
1019+
type: 'text',
1020+
text: 'skill card C body',
1021+
meta: { source: 'skill activation', activationId: 'act-3' },
1022+
},
10191023
{ type: 'text', text: 'please /commit' },
10201024
],
10211025
toolCalls: [],

‎packages/agent-core-v2/docs/state-manifest.d.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,8 @@ export interface AgentStateSnapshot {
727727
meta?: /* TextPartMeta — packages/agent-core-v2/src/human/llm/message.ts */ {
728728
source?: string;
729729
contentType?: string;
730+
activationId?: string;
731+
[key: string]: unknown;
730732
};
731733
} | /* ThinkPart — packages/agent-core-v2/src/human/llm/message.ts */ {
732734
type: 'think';
@@ -1041,6 +1043,8 @@ export interface AgentStateSnapshot {
10411043
meta?: /* TextPartMeta — packages/agent-core-v2/src/human/llm/message.ts */ {
10421044
source?: string;
10431045
contentType?: string;
1046+
activationId?: string;
1047+
[key: string]: unknown;
10441048
};
10451049
} | /* ThinkPart — packages/agent-core-v2/src/human/llm/message.ts */ {
10461050
type: 'think';

‎packages/agent-core-v2/src/agent/loop/turnEvents.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { z } from 'zod';
33

44
import type { PromptOrigin } from '#/agent/contextMemory/types';
5-
import { isUserPromptSubmitHookPart } from '#/agent/contextMemory/hookParts';
5+
import { annotateBundledSkillParts, isSkillActivationPart } from '#human/agent/origin';
66
import { parseDaemonFileUrl } from '#/agent/media/mediaRef';
77
import { AgentEvent2, registerEvent2Class } from '#/app/event/event2';
88
import type { FinishReason } from '#human/llm/finish-reason';
@@ -50,10 +50,10 @@ export function turnPromptText(
5050
input: readonly ContentPart[],
5151
origin?: PromptOrigin,
5252
): string | undefined {
53-
const bundledBlocks = origin?.kind === 'user' ? (origin.skillActivations?.length ?? 0) : 0;
54-
const parts = input.filter((part): part is TextPart => part.type === 'text');
55-
const callerParts = parts.filter((part) => !isUserPromptSubmitHookPart(part));
56-
const text = [...parts.filter(isUserPromptSubmitHookPart), ...callerParts.slice(bundledBlocks)]
53+
const bundledActivations =
54+
origin?.kind === 'user' ? (origin.skillActivations ?? []) : [];
55+
const text = annotateBundledSkillParts(input, bundledActivations)
56+
.filter((part): part is TextPart => part.type === 'text' && !isSkillActivationPart(part))
5757
.map((part) => part.text)
5858
.join('');
5959
return text.length > 0 ? text : undefined;

‎packages/agent-core-v2/src/features/skill/skillService.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { IEventService } from '#/app/event/event';
1313
import { ITelemetryService } from '#/app/telemetry/telemetry';
1414
import { ErrorCodes, Error2 } from '#/errors';
1515
import type { ContentPart } from '#human/llm/message';
16+
import { skillActivationPart } from '#human/agent/origin';
1617
import { MAIN_AGENT_ID } from '#/session/agentLifecycle/agentLifecycle';
1718
import { ISessionContext } from '#/session/sessionContext/sessionContext';
1819
import { ISessionMetadata } from '#/session/sessionMetadata/sessionMetadata';
@@ -204,9 +205,10 @@ export class AgentSkillService implements IAgentSkillService {
204205

205206
const skillArgs = input.args ?? '';
206207
const skillContent = this.renderSkillPrompt(skill, skillArgs);
208+
const activationId = randomUUID();
207209
const origin: SkillActivationOrigin = {
208210
kind: 'skill_activation',
209-
activationId: randomUUID(),
211+
activationId,
210212
skillName: skill.name,
211213
trigger: 'user-slash',
212214
skillType: skill.metadata.type,
@@ -216,16 +218,16 @@ export class AgentSkillService implements IAgentSkillService {
216218
};
217219
return {
218220
origin,
219-
part: {
220-
type: 'text',
221-
text: renderUserSlashSkillPrompt({
221+
part: skillActivationPart(
222+
renderUserSlashSkillPrompt({
222223
skillName: skill.name,
223224
skillArgs,
224225
skillContent,
225226
skillSource: skill.source,
226227
skillDir: skill.dir,
227228
}),
228-
},
229+
activationId,
230+
),
229231
entry: {
230232
activationId: origin.activationId,
231233
skillName: origin.skillName,

‎packages/agent-core-v2/src/human/agent/historySchema.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ import type { HistoryMessage } from './turn';
66
const textPartSchema = z.object({
77
type: z.literal('text'),
88
text: z.string(),
9-
meta: z
10-
.looseObject({ source: z.string().optional(), contentType: z.string().optional() })
11-
.optional(),
9+
meta: z.record(z.string(), z.unknown()).optional(),
1210
});
1311
const thinkPartSchema = z.object({
1412
type: z.literal('think'),

‎packages/agent-core-v2/src/human/agent/origin.ts‎

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
11
import { promptDisplayTextFromContentParts } from '../../agent/prompt/promptMetadataText';
2-
import type { ContentPart } from '#/llm/message';
2+
import type { ContentPart, TextPart } from '#/llm/message';
3+
4+
export const SKILL_ACTIVATION_PART_SOURCE = 'skill activation';
5+
6+
export function skillActivationPart(text: string, activationId: string): TextPart {
7+
return { type: 'text', text, meta: { source: SKILL_ACTIVATION_PART_SOURCE, activationId } };
8+
}
9+
10+
export function isSkillActivationPart(part: ContentPart): boolean {
11+
return part.type === 'text' && part.meta?.source === SKILL_ACTIVATION_PART_SOURCE;
12+
}
13+
14+
export function annotateBundledSkillParts(
15+
content: readonly ContentPart[],
16+
bundledActivations: readonly BundledSkillActivation[],
17+
): ContentPart[] {
18+
if (bundledActivations.length === 0 || content.some(isSkillActivationPart)) {
19+
return [...content];
20+
}
21+
let index = 0;
22+
return content.map((part) => {
23+
const activation = bundledActivations[index];
24+
if (activation !== undefined && part.type === 'text' && part.meta?.source === undefined) {
25+
index += 1;
26+
return {
27+
...part,
28+
meta: { source: SKILL_ACTIVATION_PART_SOURCE, activationId: activation.activationId },
29+
};
30+
}
31+
return part;
32+
});
33+
}
334

435
export type SkillSource = 'project' | 'user' | 'extra' | 'builtin';
536

@@ -43,12 +74,14 @@ function userOriginOf(origin: PromptOrigin | undefined): UserPromptOrigin | unde
4374
return origin !== undefined && origin.kind === 'user' ? (origin as UserPromptOrigin) : undefined;
4475
}
4576

46-
function bundledSkillBlockCount(message: SteerMessage): number {
47-
return userOriginOf(message.origin)?.skillActivations?.length ?? 0;
77+
function bundledSkillActivationsOf(message: SteerMessage): readonly BundledSkillActivation[] {
78+
return userOriginOf(message.origin)?.skillActivations ?? [];
4879
}
4980

5081
export function stripBundledSkillBlocks(message: SteerMessage): ContentPart[] {
51-
return message.content.slice(bundledSkillBlockCount(message));
82+
return annotateBundledSkillParts(message.content, bundledSkillActivationsOf(message)).filter(
83+
(part) => !isSkillActivationPart(part),
84+
);
5285
}
5386

5487
export function mergeSteerMessages(messages: readonly SteerMessage[]): {
@@ -69,7 +102,11 @@ export function mergeSteerMessages(messages: readonly SteerMessage[]): {
69102
return {
70103
role: 'user',
71104
content: [
72-
...messages.flatMap((message) => message.content.slice(0, bundledSkillBlockCount(message))),
105+
...messages.flatMap((message) =>
106+
annotateBundledSkillParts(message.content, bundledSkillActivationsOf(message)).filter(
107+
isSkillActivationPart,
108+
),
109+
),
73110
...messages.flatMap((message) => stripBundledSkillBlocks(message)),
74111
],
75112
toolCalls: [],

‎packages/agent-core-v2/src/human/llm/message.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export type Role = 'system' | 'user' | 'assistant' | 'tool';
1010
export type TextPartMeta = {
1111
source?: string;
1212
contentType?: string;
13+
activationId?: string;
14+
[key: string]: unknown;
1315
};
1416

1517
export interface TextPart {

‎packages/agent-core-v2/src/human/test/agent/machine.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1236,7 +1236,7 @@ describe('agent machine input.steer', () => {
12361236
});
12371237
expect(steered).toEqual([['p1'], ['p2', 'p3']]);
12381238
expect(actor.getSnapshot().context.notifications[1]?.message.content).toEqual([
1239-
{ type: 'text', text: 'SKILLBLOCK' },
1239+
{ type: 'text', text: 'SKILLBLOCK', meta: { source: 'skill activation', activationId: 'a1' } },
12401240
{ type: 'text', text: 'p2 body' },
12411241
{ type: 'text', text: 'third' },
12421242
]);

‎packages/agent-core-v2/src/session/sessionTitle/agentTitlePromptSourceService.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
22
import { LifecycleScope } from '#/app/scopes';
33
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
44
import type { ContextMessage, PromptOrigin } from '#/agent/contextMemory/types';
5+
import { isUserPromptSubmitHookPart } from '#/agent/contextMemory/hookParts';
6+
import { annotateBundledSkillParts, isSkillActivationPart } from '#human/agent/origin';
57
import { IAgentLoopService } from '#/agent/loop/loop';
68
import {
79
promptMetadataTextFromContentParts,
@@ -110,9 +112,12 @@ function isNaturalLanguagePrompt(message: ContextMessage): boolean {
110112
}
111113

112114
function promptMetadataTextFromUserMessage(message: ContextMessage): string | undefined {
113-
const bundled = message.origin?.kind === 'user' ? (message.origin.skillActivations?.length ?? 0) : 0;
115+
const bundled =
116+
message.origin?.kind === 'user' ? (message.origin.skillActivations ?? []) : [];
114117
return promptMetadataTextFromContentParts(
115-
bundled === 0 ? message.content : message.content.slice(bundled),
118+
annotateBundledSkillParts(message.content, bundled).filter(
119+
(part) => !isSkillActivationPart(part) && !isUserPromptSubmitHookPart(part),
120+
),
116121
message.origin?.kind === 'user' ? message.origin.clientMetadata : undefined,
117122
);
118123
}

0 commit comments

Comments
 (0)