refactor(core): simplify tools prompts and trusted services
This commit is contained in:
@@ -26,8 +26,6 @@
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-scope": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -35,8 +33,6 @@
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-scope": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { carrierKeyOf, isScopeCarrier } from '@deepseek-ai/dsh-scope'
|
||||
import type { AssembleContext } from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import { assertNever, HarnessError } from '@deepseek-ai/dsh-llm'
|
||||
import type { CallId, GenerateOptions } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
@@ -92,6 +90,12 @@ interface AgentSubject {
|
||||
agent: Agent
|
||||
}
|
||||
|
||||
/** Structural subject fields used without coupling this dev plugin to owning services. */
|
||||
interface ScopedSubjectFields {
|
||||
agent?: Agent
|
||||
scope?: object
|
||||
}
|
||||
|
||||
/** Assert that a step-scoped event names the currently open turn and step. */
|
||||
function requireOpenStep(trace: SessionTrace, kind: string, turn: number, step: number): void {
|
||||
if (trace.openTurn !== turn || trace.openStep !== step) {
|
||||
@@ -412,32 +416,6 @@ export function apply(ctx: Context): void {
|
||||
lastStatus.set(agent, status)
|
||||
}, { global: true })
|
||||
|
||||
// --- Setup-drives invariant ---------------------------------------------
|
||||
//
|
||||
// CreateAgentOptions.setup COMPOSES the agent's scoped world; it must not
|
||||
// DRIVE the agent. ReactLoopAgent rejects every driving verb structurally
|
||||
// until rollback-covered publication reaches the session-start boundary;
|
||||
// this event-level invariant remains the cross-implementation backstop for
|
||||
// alternate Agent implementations and raw session writes. A turn/start
|
||||
// candidate before agent/session-start is rejected by internal/dispatch,
|
||||
// before Session commits it. Sessions of agents that exist BEFORE this
|
||||
// plugin applies are marked started (their ordering is unknowable after the
|
||||
// fact — never a false positive on HMR). `agents` is read via ctx.get (a
|
||||
// strict, optional store lookup) rather than injected: the invariants plugin
|
||||
// must load in harnesses that carry no agent registry at all (bare session
|
||||
// tests), where this check simply never trips.
|
||||
const sessionStarted = new WeakSet<Session>()
|
||||
for (const agent of ctx.get('agents')?.list() ?? []) sessionStarted.add(agent.session)
|
||||
const assertSessionStartedBeforeTurn = (session: Session, event: SessionEvent): void => {
|
||||
if (event.type !== 'turn/start' || sessionStarted.has(session)) return
|
||||
const owner = ctx.get('agents')?.list().find(agent => agent.session === session)
|
||||
if (owner === undefined) return
|
||||
throw new InvariantError(
|
||||
`agent "${owner.id}": a turn opened before agent/session-start fired — `
|
||||
+ 'CreateAgentOptions.setup composes the scoped world, it must not drive the agent '
|
||||
+ '(send/steer/inject belong after creation returns)')
|
||||
}
|
||||
|
||||
// --- Scoped-dispatch invariants (the agent-scoping seam) ---------------
|
||||
//
|
||||
// Every scope-filtered event family must dispatch with a scope carrier
|
||||
@@ -466,11 +444,11 @@ export function apply(ctx: Context): void {
|
||||
'agent/turn-stop': args => args[0],
|
||||
'agent/error': args => args[0],
|
||||
'approval/request': args => (args[0] as AgentSubject).agent,
|
||||
'tools/pre-execute': args => (args[0] as ToolExecution).agent,
|
||||
'tools/execute': args => (args[0] as ToolExecution).agent,
|
||||
'tools/post-execute': args => (args[0] as ToolExecution).agent,
|
||||
'tools/result': args => (args[0] as ToolExecution).agent,
|
||||
'system-prompt/assemble': args => (args[1] as AssembleContext).scope,
|
||||
'tools/pre-execute': args => (args[0] as ScopedSubjectFields).agent,
|
||||
'tools/execute': args => (args[0] as ScopedSubjectFields).agent,
|
||||
'tools/post-execute': args => (args[0] as ScopedSubjectFields).agent,
|
||||
'tools/result': args => (args[0] as ScopedSubjectFields).agent,
|
||||
'system-prompt/assemble': args => (args[1] as ScopedSubjectFields).scope,
|
||||
'session/created': null,
|
||||
'session/disposed': null,
|
||||
'session/event': null,
|
||||
@@ -491,33 +469,16 @@ export function apply(ctx: Context): void {
|
||||
`"${name}" was dispatched with a scope carrier keyed to a DIFFERENT subject than its arguments name — `
|
||||
+ 'the carrier key and the event\'s subject must be the same object (use agentEvents(ctx, agent))')
|
||||
}
|
||||
if (name === 'agent/session-start') {
|
||||
// Mark before product listeners run: a prepended session-start listener is
|
||||
// explicitly allowed to inject the first turn's context synchronously.
|
||||
sessionStarted.add((args[0] as Agent).session)
|
||||
}
|
||||
if (name === 'session/event') {
|
||||
const [session, event] = args as [Session, SessionEvent]
|
||||
const trace = traceFor(session)
|
||||
const transition = validateEvent(trace, event)
|
||||
assertSessionStartedBeforeTurn(session, event)
|
||||
// The exact event identity reaches the contained post-commit listener.
|
||||
// A later internal/dispatch listener may still veto; because validation
|
||||
// is pure, abandoning this weakly keyed transition does not advance the
|
||||
// committed trace or retain the session.
|
||||
stagedTransitions.set(event, { session, trace, transition })
|
||||
}
|
||||
// The assembly context must never carry the agent DX field without the
|
||||
// scope layer selector: the assembly would silently miss the agent's
|
||||
// scoped sections/tools (use assembleContextFor(agent)).
|
||||
if (name === 'system-prompt/assemble') {
|
||||
const context = args[1] as AssembleContext
|
||||
if (context.agent !== undefined && context.scope !== context.agent) {
|
||||
throw new InvariantError(
|
||||
'an assembly context carries `agent` without `scope` (or with a mismatched scope) — '
|
||||
+ 'use assembleContextFor(agent) so the assembly resolves the agent\'s scoped layer')
|
||||
}
|
||||
}
|
||||
}, { global: true })
|
||||
|
||||
// Request-reconstruction cross-check (the reconstructability RFC): a
|
||||
|
||||
@@ -79,34 +79,6 @@ describe('session-log invariants', () => {
|
||||
expect(session.events.map(event => event.type)).toEqual(['turn/start', 'turn/end'])
|
||||
})
|
||||
|
||||
it('does not stage a substituted candidate from prepended internal instrumentation', async () => {
|
||||
const { ctx } = await setup()
|
||||
const session = ctx.sessions.create(SessionId('dispatch-substitution-rollback'))
|
||||
let substitute = true
|
||||
const replacement = {
|
||||
type: 'turn/start',
|
||||
seq: 0,
|
||||
time: 1,
|
||||
data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
|
||||
} as const
|
||||
ctx.on('internal/dispatch', (_mode, name, args) => {
|
||||
if (name !== 'session/event' || !substitute) return
|
||||
substitute = false
|
||||
args[1] = replacement
|
||||
}, { prepend: true })
|
||||
|
||||
expect(() => session.append('turn/start', {
|
||||
turn: 1,
|
||||
trigger: { kind: 'message', source: { kind: 'user' } },
|
||||
})).toThrow('session/event internal dispatch replaced the accepted callback tuple')
|
||||
expect(session.events).toEqual([])
|
||||
|
||||
expect(() => {
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('applies the committed transition after a prepended observer throws', async () => {
|
||||
const { ctx } = await setup()
|
||||
const warnings: string[] = []
|
||||
@@ -921,62 +893,4 @@ describe('scoped-dispatch invariants', () => {
|
||||
.not.toThrow()
|
||||
})
|
||||
|
||||
it('rejects an assembly context carrying agent without scope', async () => {
|
||||
const ctx = await scopedCtx()
|
||||
const agent = { id: 'a1' } as unknown as Agent
|
||||
const base = { name: 'systemPrompt' }
|
||||
const assembly = { sections: [], tools: [], variables: {} }
|
||||
const bad = { agent }
|
||||
expect(() => {
|
||||
// The carrier base stands in for the SystemPrompt service (the declared `this`); the invariant only reads the carrier marks.
|
||||
void ctx.waterfall(scopeTarget(base, undefined) as never, 'system-prompt/assemble', assembly as never, bad as never, () => Promise.resolve(assembly as never))
|
||||
}).toThrow(/agent.*without.*scope|assembleContextFor/)
|
||||
const good = { agent, scope: agent }
|
||||
expect(() => {
|
||||
void ctx.waterfall(scopeTarget(base, agent) as never, 'system-prompt/assemble', assembly as never, good as never, () => Promise.resolve(assembly as never))
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('backstops alternate agents that open a turn before agent/session-start', async () => {
|
||||
const ctx = await scopedCtx()
|
||||
// A live agent whose session is in the store but whose session-start has
|
||||
// not fired: appending turn/start must throw the teaching error.
|
||||
const session = ctx.sessions.create(SessionId('drive-s'))
|
||||
const agent = { id: 'driver', session } as unknown as Agent
|
||||
// Provide a minimal agents lookup: the invariant reads ctx.get('agents').
|
||||
const registryStub = { list: () => [agent] }
|
||||
ctx.root.provide('agents', registryStub as never)
|
||||
expect(() => {
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
}).toThrow(/turn opened before agent\/session-start/)
|
||||
expect(session.events).toEqual([])
|
||||
// The internal boundary marks the session before even a prepended product
|
||||
// listener runs, so the supported session-start injection pattern can open
|
||||
// and close its one-shot context turn synchronously.
|
||||
ctx.on('agent/session-start', () => {
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'injection', source: { kind: 'plugin', plugin: 'test' } } })
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
}, { prepend: true })
|
||||
expect(() => {
|
||||
ctx.emit(scopeTarget(agent, agent), 'agent/session-start', agent, 'startup')
|
||||
}).not.toThrow()
|
||||
expect(session.events.map(event => event.type)).toEqual(['turn/start', 'turn/end'])
|
||||
expect(() => {
|
||||
session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('marks sessions of agents that predate the plugin as started (HMR re-apply safety)', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
const session = ctx.sessions.create(SessionId('pre-s'))
|
||||
const agent = { id: 'pre', session } as unknown as Agent
|
||||
ctx.root.provide('agents', { list: () => [agent] } as never)
|
||||
// Invariants apply AFTER the agent exists: its ordering is unknowable, so
|
||||
// a turn opening without an observed session-start must NOT false-positive.
|
||||
await ctx.plugin(Invariants)
|
||||
expect(() => {
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,12 +25,6 @@
|
||||
},
|
||||
{
|
||||
"path": "../../core/scope"
|
||||
},
|
||||
{
|
||||
"path": "../../core/system-prompt"
|
||||
},
|
||||
{
|
||||
"path": "../../core/tools"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user