Merge remote-tracking branch 'github/master' into feat/agent-event-payload

This commit is contained in:
_Kerman
2026-08-06 13:26:04 +08:00
354 changed files with 7317 additions and 1710 deletions

View File

@@ -199,6 +199,7 @@ export class SessionReferenceService extends Service {
const prompt = renderPrompt(rendered.map(source => source.data))
const source: SessionReferenceSource = {
kind: 'session-reference',
form: 'recall',
version: 1,
references: rendered.map((source, index) => ({
sessionId: source.data.sessionId,

View File

@@ -6,6 +6,8 @@ import type { SessionId, UserMessage } from '@deepseek-ai/dsh-session'
/** Durable provenance for one prepared cross-session context. */
export interface SessionReferenceSource {
kind: 'session-reference'
/** Material lifted out of another session's log (`recall` context form). */
form: 'recall'
version: 1
references: {
sessionId: string

View File

@@ -172,13 +172,14 @@ export function apply(ctx: Context, config: Config): void {
const previous = step === 1
? precedingMessageTime(agent)
: precedingStepContextTime(agent, turn)
const text = renderText(now, turn, step, previous, formatter, resolvedTimeZone)
return {
kind: 'enter',
messages: [
...decision.messages,
createUserMessage({
content: [{ type: 'text', text: renderText(now, turn, step, previous, formatter, resolvedTimeZone) }],
source: { kind: 'plugin', plugin: name },
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: name, form: 'snapshot', sections: [{ name, text }] },
}),
],
}

View File

@@ -56,7 +56,14 @@ describe('time-context through a real headless cordis.yml', () => {
for (let index = 0; index < contexts.length; index += 1) {
expect(contexts[index]!.seq).toBeGreaterThan(starts[index]!.seq)
expect(contexts[index]!.surfaceOp).toBe('append')
expect(contexts[index]!.data.source).toEqual({ kind: 'plugin', plugin: 'time-context' })
// `snapshot` form: one named contribution whose text is exactly what the
// model read, so a consumer attributes it without re-splitting prose.
expect(contexts[index]!.data.source).toMatchObject({
kind: 'plugin',
plugin: 'time-context',
form: 'snapshot',
sections: [{ name: 'time-context' }],
})
}
const contextText = contexts.map(event => event.data.content
.filter(block => block.type === 'text')

View File

@@ -160,7 +160,19 @@ describe('durable step context', () => {
const event = session.events.at(-1)
expect(event?.type).toBe('user/message')
if (event?.type !== 'user/message') throw new Error('missing time context')
expect(event.data.source).toEqual({ kind: 'plugin', plugin: 'time-context' })
// The reading is a `snapshot`-form context: one named contribution whose
// text is exactly what the model read, so a consumer attributes it without
// re-splitting prose.
expect(event.data.source).toEqual({
kind: 'plugin',
plugin: 'time-context',
form: 'snapshot',
sections: [{
name: 'time-context',
text: 'Time sampled while preparing turn 1, step 1: 2026-07-15T09:01:01+08:00[Asia/Shanghai]\n'
+ 'Elapsed since the preceding model-visible message: 1d 1h 1m 1s.',
}],
})
expect(event.surfaceOp).toBe('append')
})

View File

@@ -232,12 +232,13 @@ export function apply(ctx: Context, config: Config): void {
if (location === undefined) return decision
const state = renderState(location)
if (previous !== undefined && previous.state === state) return decision
const text = renderReading(location, turn)
return {
kind: 'enter',
messages: [
createUserMessage({
content: [{ type: 'text', text: renderReading(location, turn) }],
source: { kind: 'plugin', plugin: name },
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: name, form: 'snapshot', sections: [{ name, text }] },
}),
...decision.messages,
],

View File

@@ -169,7 +169,14 @@ describe('tmux-context injection', () => {
])
const event = session.events.at(-1)
if (event?.type !== 'user/message') throw new Error('missing tmux context')
expect(event.data.source).toEqual({ kind: 'plugin', plugin: 'tmux-context' })
// `snapshot` form: one named contribution carrying exactly the reading the
// model saw, so a consumer attributes it without re-splitting prose.
expect(event.data.source).toMatchObject({
kind: 'plugin',
plugin: 'tmux-context',
form: 'snapshot',
sections: [{ name: 'tmux-context' }],
})
expect(event.surfaceOp).toBe('append')
})

View File

@@ -147,6 +147,7 @@ export function apply(ctx: Context, config: Config): void {
content,
source: {
kind: 'workspace-instructions',
form: 'instructions',
...desiredBaseline ? { baseline: true } : {},
changes,
},

View File

@@ -36,6 +36,8 @@ export const name = 'workspace-context'
/** Durable provenance and reconciliation facts for one workspace context. */
export interface WorkspaceInstructionSource {
kind: 'workspace-instructions'
/** Every workspace context carries instructions read out of a file (the `instructions` context form). */
form: 'instructions'
/** Marks the complete startup/resume baseline rather than a later delta. */
baseline?: true
changes: WorkspaceInstructionChange[]
@@ -77,7 +79,7 @@ export interface ReconciledInstructionContext {
function workspaceContextHook(text: string, changes: WorkspaceInstructionChange[]): UserMessage {
return createUserMessage({
content: [{ type: 'text', text }],
source: { kind: 'workspace-instructions', changes },
source: { kind: 'workspace-instructions', form: 'instructions', changes },
})
}

View File

@@ -913,6 +913,7 @@ describe('workspace context request injection', () => {
role: 'user',
source: {
kind: 'workspace-instructions',
form: 'instructions',
baseline: true,
changes: [{ action: 'set', scope: sk('.', 'AGENTS.md'), path: 'AGENTS.md' }],
},
@@ -1104,6 +1105,7 @@ describe('workspace context request injection', () => {
content: [{ type: 'text', text: 'stale nested instructions' }],
source: {
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md'), digest: 'stale' }],
},
}), {
@@ -1137,6 +1139,7 @@ describe('workspace context request injection', () => {
content: [{ type: 'text', text: 'stale nested instructions' }],
source: {
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md'), digest: 'stale' }],
},
}), {
@@ -2206,7 +2209,7 @@ describe('dynamic nested workspace context injection', () => {
expect(result.isError).toBe(false)
expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ kind: 'workspace-instructions' })
const queuedSource = ((await syncedWorkspaceContext(ctx, agent))).source
expect(queuedSource).toMatchObject({ kind: 'workspace-instructions' })
expect(queuedSource).toMatchObject({ kind: 'workspace-instructions', form: 'instructions' })
expect(queuedSource.kind === 'workspace-instructions' && queuedSource.changes.some(change =>
change.action === 'set'
&& change.scope === sk('pkg', 'AGENTS.md')
@@ -2549,6 +2552,7 @@ describe('dynamic nested workspace context injection', () => {
expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'replace', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
})
expect(blocksText(((await syncedWorkspaceContext(ctx, agent))).content)).toBe([
@@ -2658,7 +2662,7 @@ describe('dynamic nested workspace context injection', () => {
if (previous === undefined) throw new Error('missing AGENTS.md baseline state')
const authoritative = createUserMessage({
content: [{ type: 'text', text: 'nested rule' }],
source: { kind: 'workspace-instructions', changes: [previous] },
source: { kind: 'workspace-instructions', form: 'instructions', changes: [previous] },
})
if (authority === 'visible') {
agent.session.append('user/message', authoritative, { surfaceOp: 'append' })
@@ -2684,6 +2688,7 @@ describe('dynamic nested workspace context injection', () => {
content: [{ type: 'text', text: 'pending baseline duplicate' }],
source: {
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'set', scope: sk('.', 'AGENTS.md'), path: 'AGENTS.md' }],
},
})],
@@ -2727,7 +2732,7 @@ describe('dynamic nested workspace context injection', () => {
if (previous === undefined) throw new Error('missing AGENTS.md baseline state')
const authoritative = createUserMessage({
content: [{ type: 'text', text: 'repo rule' }],
source: { kind: 'workspace-instructions', changes: [previous] },
source: { kind: 'workspace-instructions', form: 'instructions', changes: [previous] },
})
agent.session.append('user/message', authoritative, { surfaceOp: 'append' })
const resolved = resolveConfig({ dshHome: home, maxBytes: 65536, localInstructionFileCandidates: [] })
@@ -2851,6 +2856,7 @@ describe('dynamic nested workspace context injection', () => {
expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'remove', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
})
expect(blocksText(((await syncedWorkspaceContext(ctx, agent))).content)).toBe([
@@ -3253,6 +3259,7 @@ describe('dynamic nested workspace context injection', () => {
],
source: {
kind: 'workspace-instructions',
form: 'instructions',
changes: [
null,
{ action: 'unknown', scope: 'pkg', path: join('pkg', 'AGENTS.md') },
@@ -3406,6 +3413,7 @@ describe('dynamic nested workspace context injection', () => {
expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({ kind: 'workspace-instructions' })
expect(((await syncedWorkspaceContext(ctx, agent))).source).toMatchObject({
kind: 'workspace-instructions',
form: 'instructions',
changes: [{ action: 'set', scope: sk('pkg', 'AGENTS.md'), path: join('pkg', 'AGENTS.md') }],
})
expect(blocksText(((await syncedWorkspaceContext(ctx, agent))).content)).toContain('nested package rule')