Merge master into worktree-windows-runtime

This commit is contained in:
Tianyi Cui
2026-07-21 23:39:14 +08:00
936 changed files with 36271 additions and 10361 deletions

View File

@@ -26,6 +26,8 @@ Step 1 measures from the latest preceding model-visible message, including the p
A time reading records a request-preparation attempt, not a committed step or transmitted request. Because the listener runs first, its append may remain when a later pre-step listener cancels or fails the attempt; the log is append-only and the plugin performs no rollback.
The separately published `./invariant` companion checks each plugin-attributed reading against the open turn, next pre-step position, elapsed baseline, and durable event time. Its rendered timestamp must parse and cannot postdate the event; process suspension between sampling and append does not invalidate the reading.
The time reading stays in derived conversation history until a later compaction shadows it. Request headers contain no time-context state. Request reconstruction uses the complete durable surface prefix at each `step/start`, so transmitted requests need not map one-to-one to readings: a failed preparation can leave an extra reading, while interval suppression can let a request reuse existing history without adding one.
## Model Experience

View File

@@ -11,11 +11,16 @@
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
@@ -26,12 +31,15 @@
},
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-loader-smoke": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",

View File

@@ -0,0 +1,114 @@
/** Package-owned durable clock-context invariants. @module @deepseek-ai/dsh-time-context/invariant */
import type { Context } from 'cordis'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-time-context'
const SOURCE_NAME = 'time-context'
const READING = new RegExp(
'^Time sampled while preparing turn (\\d+), step (\\d+): '
+ '(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:Z|[+-]\\d{2}:\\d{2})\\[[^\\]]+\\])\\n'
+ 'Elapsed since the preceding (model-visible message|step context): '
+ '(?:unavailable|(?:(?:\\d+d )?(?:\\d+h )?(?:\\d+m )?\\d+s))\\.$',
)
/** Cordis companion plugin name. */
export const name = 'time-context-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** Derive the pre-step position at which a time-context reading may append. */
function preparationPosition(history: readonly SessionEvent[], fail: InvariantFailure): { turn: number; step: number } {
const currentTurnEvents: SessionEvent[] = []
let openTurn: number | undefined
for (const event of history.slice().reverse()) {
if (event.type === 'turn/end') {
fail('time-context reading must be appended inside an open turn')
}
if (event.type === 'turn/start') {
openTurn = event.data.turn
break
}
currentTurnEvents.push(event)
}
if (openTurn === undefined) fail('time-context reading must be appended inside an open turn')
for (const event of currentTurnEvents) {
if (event.type === 'step/start') {
fail(`time-context reading must precede step/start, but step ${event.data.step} is already open`)
}
if (event.type === 'step/end') {
return { turn: openTurn, step: event.data.step + 1 }
}
}
return { turn: openTurn, step: 1 }
}
/** Validate one plugin-attributed time reading against its session position and timestamp. */
function validateReading(
history: readonly SessionEvent[],
event: SessionEvent<'context/message'>,
fail: InvariantFailure,
): void {
const [block] = event.data.content
if (event.data.content.length !== 1 || block?.type !== 'text') {
fail('time-context messages must contain exactly one text block')
}
const match = READING.exec(block.text)
if (match === null) fail('time-context message does not match the durable reading format')
const turn = Number(match[1])
const step = Number(match[2])
if (!Number.isSafeInteger(turn) || turn < 1 || !Number.isSafeInteger(step) || step < 1) {
fail('time-context turn and step must be positive safe integers')
}
const expected = preparationPosition(history, fail)
if (turn !== expected.turn || step !== expected.step) {
fail(`time-context reading names turn ${turn}/step ${step}, expected turn ${expected.turn}/step ${expected.step}`)
}
const baseline = match[4]
if ((step === 1) !== (baseline === 'model-visible message')) {
fail(`time-context step ${step} uses the wrong elapsed-time baseline ${JSON.stringify(baseline)}`)
}
const rendered = match[3]
/* v8 ignore next -- the preceding fixed regexp always supplies capture group three. */
if (rendered === undefined) fail('time-context reading omitted its rendered timestamp')
const renderedTime = Date.parse(rendered.replace(/\[[^\]]+\]$/, ''))
if (!Number.isFinite(renderedTime) || !Number.isSafeInteger(event.time)
|| event.time < renderedTime) {
fail('time-context rendered timestamp must parse and not postdate its durable event')
}
}
/* jscpd:ignore-start -- package companions share replay and dispatch plumbing */
/** Validate all package-owned readings already present in one session. */
function validateSession(session: Session, fail: InvariantFailure): void {
for (const [index, event] of session.events.entries()) {
if (event.type !== 'context/message'
|| event.data.source.kind !== 'plugin'
|| event.data.source.plugin !== SOURCE_NAME) continue
validateReading(session.events.slice(0, index), event, fail)
}
}
/** Install validation for loaded and newly appended context readings. */
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
for (const session of ctx.sessions.list()) validateSession(session, fail)
ctx.on('internal/dispatch', (_mode, eventName, args) => {
if (eventName !== 'session/event') return
const [session, event] = args as [Session, SessionEvent]
if (event.type !== 'context/message'
|| event.data.source.kind !== 'plugin'
|| event.data.source.plugin !== SOURCE_NAME) return
validateReading(session.events, event, fail)
}, { global: true })
}, { inject: ['sessions'] })
/* jscpd:ignore-end */
/**
* Register the time-context invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))

View File

@@ -0,0 +1,177 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
import * as TimeInvariant from '@deepseek-ai/dsh-time-context/invariant'
import InvariantService from '@deepseek-ai/dsh-invariants'
const SECOND = Date.parse('2026-07-14T00:00:00Z')
async function setup(): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(InvariantService, { enabled: true })
await ctx.plugin(TimeInvariant)
return ctx
}
function event(text: string, time = SECOND + 456, content?: unknown[]): SessionEvent {
return {
type: 'context/message',
seq: 0,
time,
data: {
content: (content ?? [{ type: 'text', text }]) as ContentBlock[],
source: { kind: 'plugin', plugin: 'time-context' },
},
}
}
function reading(
turn = '1',
step = '1',
baseline = 'model-visible message',
timestamp = '2026-07-14T00:00:00+00:00[UTC]',
): string {
return `Time sampled while preparing turn ${turn}, step ${step}: ${timestamp}\n`
+ `Elapsed since the preceding ${baseline}: unavailable.`
}
function preparing(turn: number, step: number): Session {
const session = new Session(SessionId(`time-invariant-${turn}-${step}`))
for (let priorTurn = 1; priorTurn < turn; priorTurn += 1) {
session.append('turn/start', { turn: priorTurn, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('turn/end', { turn: priorTurn, reason: { kind: 'completed' } })
}
session.append('turn/start', { turn, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('user/message', {
content: [{ type: 'text', text: `turn ${turn}` }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
for (let priorStep = 1; priorStep < step; priorStep += 1) {
session.append('step/start', { turn, step: priorStep })
session.append('step/end', { turn, step: priorStep })
}
return session
}
function appendReading(session: Session, text: string): void {
session.append('context/message', {
content: [{ type: 'text', text }],
source: { kind: 'plugin', plugin: 'time-context' },
}, { surfaceOp: 'append' })
}
describe('time-context invariants', () => {
it('accepts a reading whose turn, step, baseline, and timestamp agree', async () => {
const ctx = await setup()
const text = 'Time sampled while preparing turn 2, step 3: 2026-07-14T00:00:00+00:00[UTC]\n'
+ 'Elapsed since the preceding step context: 4m 2s.'
expect(() => { ctx.emit('session/event', preparing(2, 3), event(text)) }).not.toThrow()
})
it('accepts a reading durably appended after a long process pause', async () => {
const ctx = await setup()
expect(() => {
ctx.emit('session/event', preparing(1, 1), event(reading(), SECOND + 60_000))
}).not.toThrow()
})
it('validates each existing reading against its preceding durable prefix', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const session = ctx.sessions.create(SessionId('time-invariant-late-valid'))
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('user/message', {
content: [{ type: 'text', text: 'prepare' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
appendReading(session, reading())
session.append('step/start', { turn: 1, step: 1 })
await ctx.plugin(InvariantService, { enabled: true })
await expect(ctx.plugin(TimeInvariant)).resolves.toBeDefined()
})
it('rejects an invalid existing reading on late registration', async () => {
const ctx = new Context()
await ctx.plugin(SessionStore)
const session = ctx.sessions.create(SessionId('time-invariant-late-invalid'))
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
session.append('user/message', {
content: [{ type: 'text', text: 'prepare' }],
source: { kind: 'user' },
}, { surfaceOp: 'append' })
appendReading(session, reading('1', '2', 'step context'))
await ctx.plugin(InvariantService, { enabled: true })
await expect(ctx.plugin(TimeInvariant).then(() => undefined)).rejects.toThrow(/expected turn 1\/step 1/)
})
it.each([
[reading('1', '3', 'step context'), /expected turn 2\/step 3/],
[reading('2', '2', 'step context'), /expected turn 2\/step 3/],
])('rejects a reading that disagrees with its session position', async (text, message) => {
const ctx = await setup()
expect(() => { ctx.emit('session/event', preparing(2, 3), event(text)) }).toThrow(message)
})
it('rejects a reading after cancellation closes the turn', async () => {
const ctx = await setup()
const session = preparing(1, 2)
session.append('turn/end', { turn: 1, reason: { kind: 'aborted' } })
expect(() => { ctx.emit('session/event', session, event(reading('1', '2', 'step context'))) })
.toThrow(/inside an open turn/)
})
it('rejects a reading after step/start or without any open turn', async () => {
const ctx = await setup()
const started = preparing(1, 1)
started.append('step/start', { turn: 1, step: 1 })
expect(() => { ctx.emit('session/event', started, event(reading())) }).toThrow(/must precede step\/start/)
expect(() => {
ctx.emit('session/event', new Session(SessionId('time-invariant-empty')), event(reading()))
}).toThrow(/inside an open turn/)
})
it.each([
['not a reading', SECOND, undefined, /durable reading format/],
[reading('0'), SECOND, undefined, /positive safe integers/],
[reading('999999999999999999999'), SECOND, undefined, /positive safe integers/],
[reading('1', '0', 'step context'), SECOND, undefined, /positive safe integers/],
[reading('1', '999999999999999999999', 'step context'), SECOND, undefined, /positive safe integers/],
[reading('1', '1', 'step context'), SECOND, undefined, /wrong elapsed-time baseline/],
[reading('1', '2', 'model-visible message'), SECOND, undefined, /wrong elapsed-time baseline/],
[reading('1', '1', 'model-visible message', '2026-99-99T00:00:00+00:00[UTC]'), SECOND, undefined, /must parse and not postdate/],
[reading(), Number.NaN, undefined, /must parse and not postdate/],
[reading(), SECOND - 1, undefined, /must parse and not postdate/],
['ignored', SECOND, [], /exactly one text block/],
['ignored', SECOND, [{ type: 'image', data: 'x', mimeType: 'image/png' }], /exactly one text block/],
['ignored', SECOND, [{ type: 'text', text: 'one' }, { type: 'text', text: 'two' }], /exactly one text block/],
] as const)('rejects an incoherent durable reading', async (text, time, content, message) => {
const ctx = await setup()
const preparationStep = text.includes('turn 1, step 2:') ? 2 : 1
expect(() => {
ctx.emit('session/event', preparing(1, preparationStep), event(
text,
time,
content === undefined ? undefined : [...content],
))
}).toThrow(message)
})
it('ignores context messages owned by another package', async () => {
const ctx = await setup()
const other = event('unrelated') as SessionEvent<'context/message'>
other.data.source = { kind: 'plugin', plugin: 'other' }
expect(() => { ctx.emit('session/event', preparing(1, 1), other) }).not.toThrow()
other.data.source = { kind: 'user' }
expect(() => { ctx.emit('session/event', preparing(1, 1), other) }).not.toThrow()
expect(() => {
ctx.emit('session/event', preparing(1, 1), {
type: 'turn/start', seq: 0, time: 0, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
})
ctx.emit('tools/change')
}).not.toThrow()
})
})

View File

@@ -4,8 +4,7 @@ import Loader from '@cordisjs/plugin-loader'
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import AgentRegistry, { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import { defineTool } from '@deepseek-ai/dsh-tools'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
@@ -83,7 +82,7 @@ async function fire(
step: number,
signal: AbortSignal = SIGNAL,
): Promise<void> {
await ctx.serial('agent/pre-step', agent, turn, step, signal)
await agentEvents(ctx, agent).serial('agent/pre-step', turn, step, signal)
}
function textResponse(text: string): StreamChunk[] {
@@ -368,7 +367,7 @@ describe('real agent-loop request history', () => {
ctx.on('agent/pre-step', (subject) => {
laterSawReading = contextTexts(subject.session).length === 1
if (mode === 'throws') throw new Error('later pre-step failure')
subject.cancel('later pre-step cancellation')
subject.cancel({ kind: 'user' })
})
const agent = ctx.agentLoop.create(SessionId(`late-${mode}`), { provider: 'mock', model: 'mock' })

View File

@@ -6,13 +6,35 @@
},
"include": ["src"],
"references": [
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../../vendor/schemastery" },
{ "path": "../../llm/llm" },
{ "path": "../../core/agent" },
{ "path": "../../core/system-prompt" },
{ "path": "../../core/agent" },
{ "path": "../../support/loader-smoke" }
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../llm/llm"
},
{
"path": "../../core/agent"
},
{
"path": "../../core/system-prompt"
},
{
"path": "../../core/agent"
},
{
"path": "../../support/loader-smoke"
},
{
"path": "../../support/invariants"
},
{
"path": "../../core/session"
}
]
}

View File

@@ -11,11 +11,16 @@
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
@@ -24,6 +29,7 @@
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-fs": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
@@ -39,6 +45,7 @@
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",

View File

@@ -9,7 +9,7 @@ import { lstat, stat } from 'node:fs/promises'
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
import type { FileSystem, FsInfo, FsPathInfo, FsTarget, FsVersion } from '@deepseek-ai/dsh-fs'
import { assertNever } from '@deepseek-ai/dsh-llm'
import { DEFAULT_DSH_HOME_DISPLAY, defaultDshHome } from '@deepseek-ai/dsh-paths'
import { dshHomeDisplay } from '@deepseek-ai/dsh-paths'
import { resolveConfig, resolveDiscoveryConfig, type ResolvedConfig } from './config.ts'
import { renderWorkspaceContext, type RenderedWorkspaceContext } from './render.ts'
@@ -469,5 +469,5 @@ export async function readScopeInstruction(
}
function userGlobalDisplayPath(dshHome: string): string {
return dshHome === resolve(defaultDshHome()) ? `${DEFAULT_DSH_HOME_DISPLAY}/AGENTS.md` : '$DSH_HOME/AGENTS.md'
return `${dshHomeDisplay(dshHome)}/AGENTS.md`
}

View File

@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-workspace-context`.
* @module @deepseek-ai/dsh-workspace-context/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-workspace-context'
/** Cordis companion plugin name. */
export const name = 'workspace-context-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: replay intentionally tolerates unknown or malformed workspace metadata,
* while focused pipeline tests own its private pending/cache state transitions.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */

View File

@@ -500,7 +500,7 @@ export async function dynamicInstructionContext(
{
touchedPath,
includeBaselineScopes: baselineInstructionStates.has(agent.session),
...exec.signal === undefined ? {} : { signal: exec.signal },
signal: exec.signal,
},
)
}

View File

@@ -7,8 +7,9 @@ import Loader from '@cordisjs/plugin-loader'
import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
import LlmService, { CallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent } from '@deepseek-ai/dsh-session'
import AgentRegistry, { type Agent, type HookContext } from '@deepseek-ai/dsh-agent'
import AgentRegistry, { agentEvents, type Agent, type HookContext } from '@deepseek-ai/dsh-agent'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import { scopeTarget } from '@deepseek-ai/dsh-scope'
import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
import type {
FsDirEntry,
@@ -23,7 +24,12 @@ import type {
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import type { ToolExecution, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import type {
PostToolDecision,
ToolExecution,
ToolExecutionResult,
ToolExecutionToken,
} from '@deepseek-ai/dsh-tools'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import {
discoverBaselineInstructionFiles,
@@ -40,6 +46,8 @@ import {
} from '../src/state.ts'
import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts'
const testToolSignal = new AbortController().signal
async function tempRepo(): Promise<string> {
return mkdtemp(join(tmpdir(), 'dsh-workspace-context-'))
}
@@ -227,14 +235,31 @@ const composedPrefixes = new WeakMap<object, Message[]>()
async function composeBaselinePrefix(ctx: Context, agent: Agent): Promise<Message[]> {
const empty: Message[] = []
const prefix = await ctx.waterfall(
'agent/session-prefix', agent, empty, AbortSignal.timeout(1000),
const prefix = await agentEvents(ctx, agent).waterfall(
'agent/session-prefix', empty, AbortSignal.timeout(1000),
() => Promise.resolve(empty),
)
composedPrefixes.set(agent, prefix)
return prefix
}
function toolEventCarrier(ctx: Context, exec: ToolExecution) {
return scopeTarget(ctx.get('tools') ?? ctx as unknown as ToolRegistry, exec.agent)
}
function postExecute(
ctx: Context,
exec: ToolExecution,
result: Readonly<ToolExecutionResult>,
next: () => Promise<PostToolDecision>,
): Promise<PostToolDecision> {
return ctx.waterfall(toolEventCarrier(ctx, exec), 'tools/post-execute', exec, result, next)
}
function emitToolResult(ctx: Context, exec: ToolExecution, result: Readonly<ToolExecutionResult>): void {
ctx.emit(toolEventCarrier(ctx, exec), 'tools/result', exec, result)
}
function derivedText(agent: Agent): string {
return blocksText(composedPrefixes.get(agent)?.[0]?.content)
}
@@ -800,7 +825,8 @@ describe('workspace context request injection', () => {
try {
await ctx.plugin(workspaceContext, { maxBytes: 65536 })
const decision = await ctx.waterfall('tools/post-execute', stubToolExecution({
const decision = await postExecute(ctx, stubToolExecution({
signal: testToolSignal,
callId: CallId('no-fs-post-execute'),
name: 'read',
arguments: { file_path: 'pkg/file.txt' },
@@ -836,6 +862,7 @@ describe('workspace context request injection', () => {
const agent = stubAgent(root)
const exec = stubToolExecution({
signal: testToolSignal,
callId: CallId('read-blocked-post-execute'),
name: 'read',
arguments: { file_path: 'pkg/file.txt' },
@@ -847,7 +874,7 @@ describe('workspace context request injection', () => {
}
// A later PostToolUse-style policy blocks this otherwise-successful read.
const blocked = await ctx.waterfall('tools/post-execute', exec, result, async () => ({
const blocked = await postExecute(ctx, exec, result, async () => ({
kind: 'block' as const,
feedback: [{ type: 'text' as const, text: 'blocked by policy' }],
}))
@@ -861,7 +888,7 @@ describe('workspace context request injection', () => {
// The same read, when the downstream accepts, DOES surface the nested
// instructions — proving the block branch above is what suppressed them,
// and that the block did not consume the pending nested change.
const accepted = await ctx.waterfall('tools/post-execute', exec, result, async () => ({
const accepted = await postExecute(ctx, exec, result, async () => ({
kind: 'accept' as const,
}))
expect(accepted.kind).toBe('accept')
@@ -981,6 +1008,7 @@ describe('workspace context request injection', () => {
await composeBaselinePrefix(ctx, agent)
await write(join(root, 'AGENTS.md'), 'new root rule with more detail')
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-baseline-change'), name: 'read', arguments: { file_path: 'file.txt' }, agent,
})
@@ -1009,6 +1037,7 @@ describe('workspace context request injection', () => {
await composeBaselinePrefix(ctx, agent)
await rm(join(root, 'AGENTS.md'))
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-baseline-remove'), name: 'read', arguments: { file_path: 'file.txt' }, agent,
})
@@ -1034,6 +1063,7 @@ describe('workspace context request injection', () => {
await composeBaselinePrefix(ctx, agent)
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-with-shared-global-root'), name: 'read', arguments: { file_path: 'file.txt' }, agent,
})
@@ -1112,6 +1142,25 @@ describe('workspace context request injection', () => {
}
})
it('keeps the direct provider API usable without an operation signal', async () => {
const root = '/virtual/no-signal-repo'
const home = '/virtual/no-signal-home'
const ctx = new Context()
try {
await ctx.plugin(RecordingFileSystem)
const fs = ctx.fs as RecordingFileSystem
fs.entries.set(join(root, '.git'), { type: 'directory' })
fs.entries.set(join(root, 'AGENTS.md'), { type: 'file', content: 'optional capability signal' })
const rendered = await loadBaselineInstructions({ cwd: root, dshHome: home, maxBytes: 65536 }, fs)
expect(rendered?.text).toContain('optional capability signal')
expect(fs.signals).toEqual([])
} finally {
await ctx.fiber.dispose()
}
})
it('rejects a provider-sized instruction file before reading content', async () => {
const root = join(await tempRepo(), 'virtual-repo')
const home = join(await tempRepo(), 'virtual-home')
@@ -1173,8 +1222,9 @@ describe('workspace context request injection', () => {
const controller = new AbortController()
const reason = new Error('cancel prefix')
const empty: Message[] = []
const pending = ctx.waterfall(
'agent/session-prefix', stubAgent(root), empty, controller.signal,
const agent = stubAgent(root)
const pending = agentEvents(ctx, agent).waterfall(
'agent/session-prefix', empty, controller.signal,
() => Promise.resolve(empty),
)
@@ -1601,7 +1651,7 @@ describe('dynamic nested workspace context injection', () => {
description: 'Abort the current test step.',
parameters: {},
async execute() {
;(agent as unknown as { currentAbort?: AbortController }).currentAbort?.abort('test abort')
agent.cancel({ kind: 'user' })
return [{ type: 'text', text: 'aborted' }]
},
}))
@@ -1664,7 +1714,7 @@ describe('dynamic nested workspace context injection', () => {
signal: controller.signal,
})
const pending = ctx.waterfall('tools/post-execute', exec, {
const pending = postExecute(ctx, exec, {
content: [{ type: 'text', text: 'ok' }],
isError: false,
}, () => Promise.resolve({ kind: 'accept' as const }))
@@ -1691,6 +1741,7 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-nested'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -1750,6 +1801,7 @@ describe('dynamic nested workspace context injection', () => {
})
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-configured-nested-candidate'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -1778,12 +1830,14 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-nested-1'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
agent,
})
const second = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-nested-2'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -1816,10 +1870,12 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-before-version-fast-path'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContexts(agent, first)
const second = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-with-version-fast-path'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
@@ -1851,14 +1907,17 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-before-same-digest-version-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContexts(agent, first)
fs.entries.set(instructionPath, { type: 'file', content: 'same package rule', version: FsVersion('revision-2') })
const afterVersionChange = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-same-digest-version-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
const afterRefresh = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-version-cache-refresh'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
@@ -1889,9 +1948,11 @@ describe('dynamic nested workspace context injection', () => {
await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-from-first-session'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent: stubAgent(root),
})
const second = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-from-second-session'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent: stubAgent(root),
})
@@ -1917,11 +1978,13 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-before-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContexts(agent, first)
await write(join(root, 'pkg/AGENTS.md'), 'new package rule with more detail')
const changed = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
@@ -1957,15 +2020,18 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-before-fallback'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContexts(agent, first)
await rm(join(root, 'pkg/AGENTS.md'))
const changed = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-fallback'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContexts(agent, changed)
const unchanged = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-logged-fallback'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
@@ -1996,11 +2062,13 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-before-remove'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContexts(agent, first)
await rm(join(root, 'pkg/AGENTS.md'))
const removed = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-remove'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
@@ -2034,17 +2102,20 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-before-tombstone'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContexts(agent, first)
await rm(join(root, 'pkg/AGENTS.md'))
const removed = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-to-create-tombstone'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContexts(agent, removed)
await write(join(root, 'pkg/AGENTS.md'), 'restored package rule')
const restored = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-tombstone'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
@@ -2076,11 +2147,13 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-before-provider-failure'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
appendAdditionalContexts(agent, first)
fs.throwOnStat.add(join(root, 'pkg/AGENTS.md'))
const duringFailure = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-during-provider-failure'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
})
@@ -2104,6 +2177,7 @@ describe('dynamic nested workspace context injection', () => {
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-before-resume'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2116,6 +2190,7 @@ describe('dynamic nested workspace context injection', () => {
}
const afterResume = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-resume'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2141,6 +2216,7 @@ describe('dynamic nested workspace context injection', () => {
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
const original = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-before-offline-change'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent: original,
})
appendAdditionalContexts(original, first)
@@ -2171,6 +2247,7 @@ describe('dynamic nested workspace context injection', () => {
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-before-compact'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2178,6 +2255,7 @@ describe('dynamic nested workspace context injection', () => {
})
const contextSeq = appendAdditionalContexts(agent, first)!
const visibleBeforeCompact = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-while-visible'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2193,6 +2271,7 @@ describe('dynamic nested workspace context injection', () => {
})
const afterCompact = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-compact'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2222,6 +2301,7 @@ describe('dynamic nested workspace context injection', () => {
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-package'),
name: 'read',
arguments: { file_path: 'pkg/file.txt' },
@@ -2230,6 +2310,7 @@ describe('dynamic nested workspace context injection', () => {
appendAdditionalContexts(agent, first)
const second = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-subtree'),
name: 'read',
arguments: { file_path: 'pkg/sub/file.txt' },
@@ -2257,6 +2338,7 @@ describe('dynamic nested workspace context injection', () => {
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 700 })
const agent = stubAgent(root)
const first = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-subtree-omitting-parent'),
name: 'read',
arguments: { file_path: 'pkg/sub/file.txt' },
@@ -2265,6 +2347,7 @@ describe('dynamic nested workspace context injection', () => {
appendAdditionalContexts(agent, first)
const second = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-parent-after-omit'),
name: 'read',
arguments: { file_path: 'pkg/other.txt' },
@@ -2326,6 +2409,7 @@ describe('dynamic nested workspace context injection', () => {
}, { surfaceOp: 'append' })
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-spoofed-state'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2352,12 +2436,14 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const rootResult = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-root-file'),
name: 'read',
arguments: { file_path: 'root.txt' },
agent,
})
const absoluteResult = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-absolute-nested-file'),
name: 'read',
arguments: { file_path: join(root, 'pkg/deep/file.txt') },
@@ -2390,12 +2476,14 @@ describe('dynamic nested workspace context injection', () => {
isError: false,
}
const failedStat = await ctx.waterfall('tools/post-execute', stubToolExecution({
const failedStat = await postExecute(ctx, stubToolExecution({
signal: testToolSignal,
callId: CallId('provider-stat-failure'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
}), result, async () => ({ kind: 'accept' as const }))
fs.throwOnStat.clear()
fs.entries.set(join(root, 'pkg/AGENTS.md'), { type: 'directory' })
const mismatchedStat = await ctx.waterfall('tools/post-execute', stubToolExecution({
const mismatchedStat = await postExecute(ctx, stubToolExecution({
signal: testToolSignal,
callId: CallId('provider-stat-mismatch'), name: 'read', arguments: { file_path: 'pkg/file.txt' }, agent,
}), result, async () => ({ kind: 'accept' as const }))
@@ -2426,6 +2514,7 @@ describe('dynamic nested workspace context injection', () => {
await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 })
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-with-unreadable-nested-instruction'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2461,6 +2550,7 @@ describe('dynamic nested workspace context injection', () => {
}))
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-with-downstream'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2505,6 +2595,7 @@ describe('dynamic nested workspace context injection', () => {
}))
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-blocked-downstream'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2545,6 +2636,7 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const blocked = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('outer-block-first'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2552,6 +2644,7 @@ describe('dynamic nested workspace context injection', () => {
})
shouldBlock = false
const accepted = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('outer-block-retry'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2592,7 +2685,7 @@ describe('dynamic nested workspace context injection', () => {
arguments: { file_path: 'pkg/deep/file.txt' },
...exec.agent === undefined ? {} : { agent: exec.agent },
parent: exec.token,
...exec.signal === undefined ? {} : { signal: exec.signal },
signal: exec.signal,
})
for (const context of nested.additionalContexts ?? []) exec.deferContext(context)
return nested.content
@@ -2609,10 +2702,12 @@ describe('dynamic nested workspace context injection', () => {
const agent = stubAgent(root)
const blocked = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('composite-first'), name: 'composite-read', arguments: {}, agent,
})
shouldBlock = false
const accepted = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('composite-retry'), name: 'composite-read', arguments: {}, agent,
})
@@ -2635,20 +2730,24 @@ describe('dynamic nested workspace context injection', () => {
const parent = Symbol('parent') as ToolExecutionToken
const plainResult = { callId: CallId('plain'), content: [], isError: false }
ctx.emit('tools/result', stubToolExecution({
emitToolResult(ctx, stubToolExecution({
signal: testToolSignal,
callId: CallId('agentless-child'), name: 'read', arguments: {}, parent,
}), plainResult)
ctx.emit('tools/result', stubToolExecution({
emitToolResult(ctx, stubToolExecution({
signal: testToolSignal,
callId: CallId('contextless-child'), name: 'read', arguments: {}, agent, parent,
}), { ...plainResult, additionalContexts: [{ content: [], source: { kind: 'plugin', plugin: 'workspace-context' } }] })
ctx.emit('tools/result', stubToolExecution({
emitToolResult(ctx, stubToolExecution({
signal: testToolSignal,
callId: CallId('first-child'), name: 'read', arguments: {}, agent, parent,
}), { ...plainResult, additionalContexts: [workspaceChangeContext('first', 'one')] })
ctx.emit('tools/result', stubToolExecution({
emitToolResult(ctx, stubToolExecution({
signal: testToolSignal,
callId: CallId('second-child'), name: 'read', arguments: {}, agent, parent,
}), { ...plainResult, additionalContexts: [workspaceChangeContext('second', 'two')] })
ctx.emit('tools/result', {
...stubToolExecution({ callId: CallId('agentless-parent'), name: 'composite', arguments: {} }),
emitToolResult(ctx, {
...stubToolExecution({ signal: testToolSignal, callId: CallId('agentless-parent'), name: 'composite', arguments: {} }),
token: parent,
}, plainResult)
@@ -2683,7 +2782,8 @@ describe('dynamic nested workspace context injection', () => {
]
for (const item of cases) {
const decision = await ctx.waterfall('tools/post-execute', stubToolExecution({
const decision = await postExecute(ctx, stubToolExecution({
signal: testToolSignal,
callId: CallId(`manual-${item.name}-${cases.indexOf(item)}`),
name: item.name,
arguments: item.arguments,
@@ -2708,6 +2808,7 @@ describe('dynamic nested workspace context injection', () => {
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 0 })
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-with-disabled-budget'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },
@@ -2732,6 +2833,7 @@ describe('dynamic nested workspace context injection', () => {
await mountFileToolsAndWorkspaceContext(ctx, { dshHome: home, maxBytes: 65536 })
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-missing'),
name: 'read',
arguments: { file_path: 'pkg/missing.txt' },
@@ -2758,6 +2860,7 @@ describe('dynamic nested workspace context injection', () => {
await fiber.dispose()
const result = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('read-after-dispose'),
name: 'read',
arguments: { file_path: 'pkg/deep/file.txt' },

View File

@@ -31,6 +31,9 @@
},
{
"path": "../../util/paths"
},
{
"path": "../../support/invariants"
}
]
}