|
|
|
@@ -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' },
|
|
|
|
|