feat(core): add agent execution context

This commit is contained in:
Yichen Jiang
2026-07-16 16:29:46 +08:00
parent 04df615dd6
commit 7bcae0cd64
93 changed files with 1272 additions and 462 deletions

View File

@@ -23,7 +23,7 @@ The config-driven `ctx.agentLoop.create()` path keeps its agent owned by the loo
### Injected services
`agents`, `sessions`, `llm`, `tools`, `systemPrompt` — all five interface services.
`agents`, `agentExecution`, `sessions`, `llm`, `tools`, `systemPrompt` — all six interface services. The loop cannot activate without `agentExecution`; the default bundle loads its provider before the loop.
### Configuration (schemastery)
@@ -48,7 +48,9 @@ Configured agents start automatically. `cwd` applies only to fresh sessions; `re
### Loop lifecycle (`loop.ts`)
The driver owns one agent for its lifetime. It records turn, step, request, stream, and tool boundaries in the session log; live extension events coordinate policy around those durable facts. The [architecture turn flow](../../../docs/architecture.md#turn-flow) and generated [event catalog](../../../docs/cordis-catalog/events.md) are the authoritative sequence and signatures.
The driver owns one agent for its lifetime and runs inside `ctx.agentExecution.run({ agent }, ...)`, so process-local asynchronous continuations can recover the initiating Agent. Creation, persistence load, and unpublished setup stay outside the child boundary; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. The [execution-context package](../agent-execution/README.md) owns propagation and detached-work rules.
The loop records turn, step, request, stream, and tool boundaries in the session log; live extension events coordinate policy around those durable facts. The [architecture turn flow](../../../docs/architecture.md#turn-flow) and generated [event catalog](../../../docs/cordis-catalog/events.md) are the authoritative sequence and signatures.
Plugin failure ends the current turn, not the loop. Cancellation clears pending work and aborts the current step without leaking to the next prompt. Terminal continuation stops remain authoritative through turn close and durability flush.

View File

@@ -22,6 +22,7 @@
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-agent-execution": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-scope": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
@@ -35,6 +36,7 @@
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-execution": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",

View File

@@ -327,7 +327,7 @@ export class ReactLoopAgent implements Agent {
[startDriver](): void {
if (this._status === 'disposed') return
this.driverStarted = true
this.done = runLoop(this.loopCtx, this, {
this.done = this.loopCtx.agentExecution.run({ agent: this }, () => runLoop(this.loopCtx, this, {
inbox: this.#inbox,
setStatus: (status) => { this.setStatus(status) },
setAbort: controller => void (this.currentAbort = controller),
@@ -338,7 +338,7 @@ export class ReactLoopAgent implements Agent {
clearCancel: () => { this.cancelRequested = false },
// Pre-step cancellation re-parks without emitting a status transition.
settleIdle: () => { this.settleIdleWaiters() },
})
}))
}
/**

View File

@@ -11,6 +11,7 @@ import z from 'schemastery'
import { createScope } from '@deepseek-ai/dsh-scope'
import type { Scope } from '@deepseek-ai/dsh-scope'
import { agentEvents } from '@deepseek-ai/dsh-agent'
import type {} from '@deepseek-ai/dsh-agent-execution'
import type {
AgentFactory,
AgentHandle,
@@ -333,7 +334,7 @@ export interface Config {
/** Concrete ReactLoopAgent factory and driver service. */
export class AgentLoop extends Service implements AgentFactory {
static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt']
static inject = ['agents', 'agentExecution', 'sessions', 'llm', 'tools', 'systemPrompt']
/** Runtime schema for declarative agents. */
static Config = z.object({

View File

@@ -0,0 +1,374 @@
import { describe, expect, it } from 'vitest'
import { Context, FiberState, type Fiber } from 'cordis'
import AgentRegistry, { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import type { AgentExecutionService } from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import LlmService, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
interface Harness {
ctx: Context
providerFiber: Fiber
loopFiber: Fiber
}
async function harness(adapter: LlmAdapter): Promise<Harness> {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
const providerFiber = await ctx.plugin(AgentExecutionProvider)
const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return { ctx, providerFiber, loopFiber }
}
function waitForIdle(ctx: Context, agent: ReactLoopAgent | Agent): Promise<void> {
return new Promise((resolve) => {
const dispose = ctx.on('agent/status', (subject, status) => {
if (subject === agent && status === 'idle') {
dispose()
resolve()
}
})
})
}
function send(agent: Agent, text: string): void {
agent.send([{ type: 'text', text }])
}
/** Adapter that holds both drivers at the same awaited continuation. */
class OverlapAdapter extends LlmAdapter {
private readonly bothStarted = Promise.withResolvers<boolean>()
private starts = 0
readonly observations: { sessionId: SessionId | undefined; before: Agent; after: Agent }[] = []
constructor(private readonly ctx: Context) {
super()
}
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const before = this.ctx.agentExecution.require().agent
this.starts += 1
if (this.starts === 2) this.bothStarted.resolve(true)
await this.bothStarted.promise
await Promise.resolve()
const after = this.ctx.agentExecution.require().agent
this.observations.push({ sessionId: options.sessionId, before, after })
yield* textResponse('done')
}
}
/** Test-only transport that materializes ambient identity at its request boundary. */
class TestCapabilityTransport {
readonly requests: { path: string; headers: Record<string, string> }[] = []
constructor(private readonly execution: AgentExecutionService) {}
async request(path: string): Promise<Record<string, string>> {
await Promise.resolve()
const headers = {
'X-Harness-Session-Id': this.execution.require().agent.session.id,
}
this.requests.push({ path, headers })
return headers
}
}
/** Adapter whose first call waits for cancellation and whose later calls complete. */
class ReloadAdapter extends LlmAdapter {
readonly firstStarted = Promise.withResolvers<boolean>()
firstAgentDuringAbort: Agent | undefined
laterAgent: Agent | undefined
calls = 0
execution: AgentExecutionService | undefined
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const execution = this.execution
if (execution === undefined) throw new Error('execution service missing')
this.calls += 1
if (this.calls === 1) {
this.firstStarted.resolve(true)
try {
await new Promise<void>((_resolve, reject) => {
const abort = (): void => { reject(new Error('aborted')) }
if (options.signal?.aborted === true) abort()
else options.signal?.addEventListener('abort', abort, { once: true })
})
} catch (error: unknown) {
await Promise.resolve()
this.firstAgentDuringAbort = execution.require().agent
throw error
}
return
}
await Promise.resolve()
this.laterAgent = execution.require().agent
yield* textResponse('reloaded')
}
}
describe('AgentLoop execution context', () => {
it('keeps overlapping driver continuations bound to their exact Agents', async () => {
const ctx = new Context()
const adapter = new OverlapAdapter(ctx)
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
const a = ctx.agentLoop.create(AgentId('a'), { model: 'mock' })
const b = ctx.agentLoop.create(AgentId('b'), { model: 'mock' })
const idleA = waitForIdle(ctx, a)
const idleB = waitForIdle(ctx, b)
send(a, 'a')
send(b, 'b')
await Promise.all([idleA, idleB])
expect(adapter.observations).toHaveLength(2)
expect(adapter.observations).toEqual(expect.arrayContaining([
{ sessionId: a.session.id, before: a, after: a },
{ sessionId: b.session.id, before: b, after: b },
]))
expect(ctx.agentExecution.current()).toBeUndefined()
await ctx.fiber.dispose()
})
it('keeps child setup under the parent boundary, switches for the child driver, then restores the parent', async () => {
const adapter = new MockAdapter([
toolCallResponse('spawn', 'spawn-child', {}),
toolCallResponse('observe', 'observe-child', {}),
textResponse('child done'),
textResponse('parent done'),
])
const { ctx } = await harness(adapter)
let parentDuringSetup: Agent | undefined
let explicitChild: Agent | undefined
let childDuringDriver: Agent | undefined
let parentAfterChild: Agent | undefined
let child: Agent | undefined
ctx.tools.register(defineTool({
name: 'spawn-child',
description: 'create one child agent',
parameters: {},
execute: async (_args, exec) => {
if (exec.agent === undefined) throw new Error('parent agent missing')
const handle = await exec.agent.ctx.agents.create({
agentId: AgentId('child'),
sessionId: SessionId('child-session'),
agentOptions: { model: 'mock' },
setup: (agentCtx) => {
parentDuringSetup = ctx.agentExecution.require().agent
explicitChild = agentCtx.agent
agentCtx.tools.register(defineTool({
name: 'observe-child',
description: 'observe child execution identity',
parameters: {},
execute: async () => {
await Promise.resolve()
childDuringDriver = ctx.agentExecution.require().agent
return [{ type: 'text', text: 'observed' }]
},
}))
},
})
child = handle.agent
send(handle.agent, 'run child')
await handle.agent.whenIdle()
parentAfterChild = ctx.agentExecution.require().agent
await handle.dispose()
return [{ type: 'text', text: 'child completed' }]
},
}))
const parentHandle = await ctx.agents.create({
agentId: AgentId('parent'),
sessionId: SessionId('parent-session'),
agentOptions: { model: 'mock' },
})
const idle = waitForIdle(ctx, parentHandle.agent)
send(parentHandle.agent, 'spawn')
await idle
expect(parentDuringSetup).toBe(parentHandle.agent)
expect(explicitChild).toBe(child)
expect(childDuringDriver).toBe(child)
expect(parentAfterChild).toBe(parentHandle.agent)
expect(ctx.agentExecution.current()).toBeUndefined()
await parentHandle.dispose()
await ctx.fiber.dispose()
})
it('keeps agentless direct tools ambient-free and builds trusted transport headers internally', async () => {
const adapter = new MockAdapter([
toolCallResponse('capability', 'capability-request', { path: '/v1/capability' }),
textResponse('done'),
])
const { ctx } = await harness(adapter)
const transport = new TestCapabilityTransport(ctx.agentExecution)
let directAmbient: Agent | undefined
let captured: Agent | undefined
ctx.tools.register(defineTool({
name: 'agentless-probe',
description: 'observe an agentless call',
parameters: {},
execute: async () => {
await Promise.resolve()
directAmbient = ctx.agentExecution.current()?.agent
return [{ type: 'text', text: 'ok' }]
},
}))
ctx.tools.register(defineTool({
name: 'capability-request',
description: 'call the test capability transport',
parameters: { path: { type: 'string' } },
execute: async (args) => {
captured = ctx.agentExecution.require().agent
const path = (args as { path: string }).path
const headers = await transport.request(path)
return [{ type: 'text', text: JSON.stringify(headers) }]
},
}))
const direct = await ctx.tools.execute({
callId: CallId('direct'),
name: 'agentless-probe',
arguments: {},
})
expect(direct.isError).toBe(false)
expect(directAmbient).toBeUndefined()
const handle = await ctx.agents.create({
agentId: AgentId('transport'),
sessionId: SessionId('transport-session'),
agentOptions: { model: 'mock' },
})
const idle = waitForIdle(ctx, handle.agent)
send(handle.agent, 'call transport')
await idle
expect(transport.requests).toEqual([{
path: '/v1/capability',
headers: { 'X-Harness-Session-Id': 'transport-session' },
}])
const schema = adapter.requests[0]?.tools?.find(tool => tool.name === 'capability-request')
expect(JSON.stringify(schema?.parameters)).not.toMatch(/session|harness/i)
const call = handle.agent.session.events.find(event => event.type === 'tool/call')
expect(call?.type === 'tool/call' ? call.data.arguments : undefined)
.toBe(JSON.stringify({ path: '/v1/capability' }))
expect(captured).toBe(handle.agent)
await handle.dispose()
expect(captured?.status).toBe('disposed')
expect(ctx.agentExecution.current()).toBeUndefined()
await ctx.fiber.dispose()
})
it('keeps AgentLoop inactive until the mandatory provider appears', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
const loopFiber = ctx.plugin(AgentLoop, { agents: [] })
await Promise.resolve()
expect(loopFiber.state).toBe(FiberState.PENDING)
await ctx.plugin(AgentExecutionProvider)
await loopFiber
expect(loopFiber.state).toBe(FiberState.ACTIVE)
await ctx.fiber.dispose()
})
it('drains the old driver before disabling ALS during provider restart', async () => {
const ctx = new Context()
const adapter = new ReloadAdapter()
const { providerFiber, loopFiber } = await (async (): Promise<Harness> => {
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
const mountedProvider = await ctx.plugin(AgentExecutionProvider)
const mountedLoop = await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return { ctx, providerFiber: mountedProvider, loopFiber: mountedLoop }
})()
const oldService = ctx.agentExecution
adapter.execution = oldService
const oldHandle = await ctx.agents.create({
agentId: AgentId('before-restart'),
sessionId: SessionId('before-restart-session'),
agentOptions: { model: 'mock' },
})
const oldAgent = oldHandle.agent
send(oldAgent, 'block')
await adapter.firstStarted.promise
await providerFiber.restart()
await loopFiber.await()
expect(adapter.firstAgentDuringAbort?.id).toBe(oldAgent.id)
expect(adapter.firstAgentDuringAbort?.session).toBe(oldAgent.session)
expect(oldAgent.status).toBe('disposed')
expect(() => oldService.current()).toThrow('agent execution service is disposed')
expect(ctx.agentExecution).not.toBe(oldService)
adapter.execution = ctx.agentExecution
const newHandle = await ctx.agents.create({
agentId: AgentId('after-restart'),
sessionId: SessionId('after-restart-session'),
agentOptions: { model: 'mock' },
})
const newAgent = newHandle.agent
const idle = waitForIdle(ctx, newAgent)
send(newAgent, 'continue')
await idle
expect(adapter.laterAgent?.id).toBe(newAgent.id)
expect(adapter.laterAgent?.session).toBe(newAgent.session)
await ctx.fiber.dispose()
})
it('keeps ALS readable while root disposal drains sibling AgentLoop fibers', async () => {
const ctx = new Context()
const adapter = new ReloadAdapter()
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
const service = ctx.agentExecution
adapter.execution = service
const handle = await ctx.agents.create({
agentId: AgentId('root-dispose'),
sessionId: SessionId('root-dispose-session'),
agentOptions: { model: 'mock' },
})
const agent = handle.agent
send(agent, 'block')
await adapter.firstStarted.promise
await ctx.fiber.dispose()
expect(adapter.firstAgentDuringAbort?.id).toBe(agent.id)
expect(adapter.firstAgentDuringAbort?.session).toBe(agent.session)
expect(agent.status).toBe('disposed')
expect(() => service.current()).toThrow('agent execution service is disposed')
})
})

View File

@@ -6,6 +6,7 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { bindReactLoopAgentContext, prepareReactLoopAgent } from '../src/agent.ts'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -17,6 +18,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
@@ -51,6 +53,7 @@ function send(agent: ReactLoopAgent, text: string) {
describe('ReactLoopAgent', () => {
it('rejects access before context binding and a second driver for one session', async () => {
const ctx = new Context()
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(SessionStore)
const session = ctx.sessions.create(SessionId('exclusive-driver'))
const prepared = prepareReactLoopAgent(ctx, AgentId('first-driver'), { model: 'mock' }, session)
@@ -252,6 +255,7 @@ describe('ReactLoopAgent', () => {
it('disposer is idempotent (double-stop)', async () => {
// The internal start seam exposes one idle driver's disposer for repeated invocation.
const ctx = new Context()
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(SessionStore)
const session = ctx.sessions.create(SessionId('test'))
const prepared = prepareReactLoopAgent(ctx, AgentId('bare'), { model: 'mock' }, session)
@@ -361,6 +365,7 @@ describe('ReactLoopAgent', () => {
// Queue the internal waiter while running, then dispose the bare driver. Its disposed branch
// must chain the loop's `done` promise rather than resolve before exit.
const ctx = new Context()
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(LlmService)
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt)

View File

@@ -14,6 +14,7 @@ import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -24,6 +25,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
@@ -194,6 +196,7 @@ describe('Agent.cancel()', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -319,6 +322,7 @@ describe('Agent.cancel()', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -9,6 +9,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -31,6 +32,7 @@ describe('config-driven session id', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
const loopFiber = await ctx.plugin(AgentLoop, {
agents: [{ id: AgentId('main'), model: 'mock', resumeSessionId: SessionId('deferred') }],
})
@@ -53,6 +55,7 @@ describe('config-driven session id', () => {
await ctx1.plugin(SystemPrompt)
await ctx1.plugin(ToolRegistry)
await ctx1.plugin(AgentRegistry)
await ctx1.plugin(AgentExecutionProvider)
await ctx1.plugin(AgentLoop, { agents: [{ id: AgentId('cfg'), model: 'mock' }] })
await ctx1.plugin(SessionPersistenceJsonl, { root })
ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('cfg')]))
@@ -70,6 +73,7 @@ describe('config-driven session id', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [{ id: AgentId('cfg'), model: 'mock' }] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('cfg2')]))
@@ -93,6 +97,7 @@ describe('config-driven session id', () => {
await ctx1.plugin(SystemPrompt)
await ctx1.plugin(ToolRegistry)
await ctx1.plugin(AgentRegistry)
await ctx1.plugin(AgentExecutionProvider)
await ctx1.plugin(AgentLoop, { agents: [] })
await ctx1.plugin(SessionPersistenceJsonl, { root })
ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('first')]))
@@ -109,6 +114,7 @@ describe('config-driven session id', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [{ id: AgentId('main'), model: 'mock', resumeSessionId: SessionId('sticky-1') }] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('second')]))
@@ -137,6 +143,7 @@ describe('config-driven session id', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [{ id: AgentId('main'), model: 'mock', resumeSessionId: SessionId('does-not-exist') }] })
const warn = vi.spyOn((ctx.agentLoop as unknown as { ctx: { logger: { warn: (...a: unknown[]) => void } } }).ctx.logger, 'warn')
.mockImplementation(() => undefined)

View File

@@ -5,6 +5,7 @@ import SessionStore, { Session, SessionEvent, SessionId, TurnEndReason } from '@
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId, type ContinuationDecision } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { prepareReactLoopAgent } from '../src/agent.ts'
import * as Invariants from '@deepseek-ai/dsh-invariants'
@@ -19,6 +20,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
@@ -524,6 +526,7 @@ describe('turn numbering continues across seeded sessions', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
ctx2.llm.registerAdapter(['mock'], second)
@@ -664,6 +667,7 @@ describe('turn and step boundary recovery', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1114,6 +1118,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1165,6 +1170,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1220,6 +1226,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1271,6 +1278,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)
@@ -1320,6 +1328,7 @@ describe('disposal and cancellation during pre-step assembly', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(Invariants)
ctx.llm.registerAdapter(['mock'], adapter)

View File

@@ -6,6 +6,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -16,6 +17,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx

View File

@@ -10,6 +10,7 @@ import AgentRegistry, {
type PromptDecision,
type SessionStartSource,
} from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -29,6 +30,7 @@ async function harness(adapter: MockAdapter) {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx

View File

@@ -5,6 +5,7 @@ import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -15,6 +16,7 @@ async function harness(adapter: MockAdapter, persona = '') {
await ctx.plugin(SystemPrompt, { persona })
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx
@@ -924,6 +926,7 @@ describe('agent loop', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, {
agents: [{ id: AgentId('config-agent'), model: 'mock' }],
})
@@ -947,6 +950,7 @@ describe('agent loop', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, {
agents: [{ id: AgentId('config-agent'), model: 'mock', cwd: '/work/project' }],
})

View File

@@ -13,6 +13,7 @@ import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import fc from 'fast-check'
@@ -36,6 +37,7 @@ async function harness() {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], new EchoAdapter())
return ctx

View File

@@ -5,6 +5,7 @@ import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId, type Agent } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
@@ -42,6 +43,7 @@ async function loopHarness(): Promise<Context> {
await created.plugin(SystemPrompt, { persona: SYSTEM })
await created.plugin(ToolRegistry)
await created.plugin(AgentRegistry)
await created.plugin(AgentExecutionProvider)
await created.plugin(AgentLoop, { agents: [] })
await created.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] })
created.tools.register(defineTool({

View File

@@ -14,6 +14,7 @@ import SessionStore, { Session, SessionId, foldRequestHeader } from '@deepseek-a
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -24,6 +25,7 @@ async function harness(adapter: MockAdapter, persona = 'stable base') {
await ctx.plugin(SystemPrompt, { persona })
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx

View File

@@ -10,6 +10,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -29,6 +30,7 @@ async function mountPersistentHarness(root: string, adapter: MockAdapter): Promi
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionPersistenceJsonl, { root })
ctx.llm.registerAdapter(['mock'], adapter)
@@ -138,6 +140,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
@@ -166,6 +169,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
@@ -388,6 +392,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
await ctx.plugin(SessionPersistenceJsonl, { root })
ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('next')]))
@@ -449,6 +454,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
@@ -502,6 +508,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
@@ -531,6 +538,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx2.plugin(SystemPrompt)
await ctx2.plugin(ToolRegistry)
await ctx2.plugin(AgentRegistry)
await ctx2.plugin(AgentExecutionProvider)
await ctx2.plugin(AgentLoop, { agents: [] })
await ctx2.plugin(SessionPersistenceJsonl, { root })
ctx2.llm.registerAdapter(['mock'], adapter2)
@@ -561,6 +569,7 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => {
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
await expect(ctx.agents.resume({ agentId: AgentId('m'), resumeSessionId: SessionId('nope') }))

View File

@@ -7,6 +7,7 @@ import ToolRegistry from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId, agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { scopeOf } from '@deepseek-ai/dsh-scope'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -18,6 +19,7 @@ async function harnessWithLoop(adapter: MockAdapter = new MockAdapter([textRespo
await ctx.plugin(SystemPrompt, { persona: 'You are the deployment.' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
const loopFiber = await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return { ctx, loopFiber }

View File

@@ -14,6 +14,7 @@ import SystemPrompt, { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import type { Config as SystemPromptConfig } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import { MockAdapter, textResponse } from './mock-adapter.ts'
@@ -24,6 +25,7 @@ async function harness(adapter: MockAdapter, toolOrder?: SystemPromptConfig['too
await ctx.plugin(SystemPrompt, { persona: 'stable base', ...toolOrder !== undefined ? { toolOrder } : {} })
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx

View File

@@ -5,6 +5,7 @@ import SessionStore, { type TurnEndReason } from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import AgentRegistry, { AgentId, type ContinuationStop } from '@deepseek-ai/dsh-agent'
import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution'
import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop'
import * as Invariants from '@deepseek-ai/dsh-invariants'
import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
@@ -17,6 +18,7 @@ async function harness(adapter: MockAdapter): Promise<Context> {
await ctx.plugin(ToolRegistry)
await ctx.plugin(AgentRegistry)
await ctx.plugin(Invariants)
await ctx.plugin(AgentExecutionProvider)
await ctx.plugin(AgentLoop, { agents: [] })
ctx.llm.registerAdapter(['mock'], adapter)
return ctx

View File

@@ -35,6 +35,9 @@
{
"path": "../../core/agent"
},
{
"path": "../../core/agent-execution"
},
{
"path": "../../core/scope"
}