Merge remote-tracking branch 'origin/master' into codex/simp-prune-workflow-worker-surface
This commit is contained in:
@@ -41,6 +41,7 @@ declare module 'cordis' {
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
|
||||
* receive only sessions entered through that agent's context.
|
||||
* @param session - the session just entered and announced.
|
||||
* @dshScopeScan unsupported
|
||||
* @mode emit
|
||||
*/
|
||||
'session/created'(this: Scoped<Session>, session: Session): void
|
||||
@@ -50,6 +51,7 @@ declare module 'cordis' {
|
||||
* did not begin. Listener failures are logged and contained.
|
||||
* Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope.
|
||||
* @param session - the session that is no longer live in the store.
|
||||
* @dshScopeScan unsupported
|
||||
* @mode emit
|
||||
*/
|
||||
'session/disposed'(this: Scoped<Session>, session: Session): void
|
||||
@@ -61,6 +63,7 @@ declare module 'cordis' {
|
||||
* receive only events from sessions entered through that agent's context.
|
||||
* @param session - the session whose log grew.
|
||||
* @param event - the appended event, exactly as recorded.
|
||||
* @dshScopeScan unsupported
|
||||
* @mode emit
|
||||
*/
|
||||
'session/event'(this: Scoped<Session>, session: Session, event: SessionEvent): void
|
||||
@@ -70,6 +73,7 @@ declare module 'cordis' {
|
||||
* {@link SessionStore.flush}. Scope-filtered dispatch
|
||||
* (`@deepseek-ai/dsh-scope`) reuses the session's owner scope.
|
||||
* @param session - the session whose buffered events must reach durable storage.
|
||||
* @dshScopeScan unsupported
|
||||
* @mode parallel
|
||||
*/
|
||||
'session/flush'(this: Scoped<Session>, session: Session): Promise<void> | void
|
||||
|
||||
@@ -77,6 +77,7 @@ declare module 'cordis' {
|
||||
* parent-scoped listener observes only its own delegations. Paired with
|
||||
* `subagent/end`.
|
||||
* @param info - the provider and ready child identity.
|
||||
* @dshScopeScan unsupported
|
||||
* @mode emit
|
||||
*/
|
||||
'subagent/start'(this: Scoped<SubagentService>, info: SubagentRunInfo): void
|
||||
@@ -85,6 +86,7 @@ declare module 'cordis' {
|
||||
* parent carrier as `subagent/start`, so the lifecycle pair reaches the
|
||||
* same scoped audience.
|
||||
* @param info - the run identity and terminal outcome.
|
||||
* @dshScopeScan unsupported
|
||||
* @mode emit
|
||||
*/
|
||||
'subagent/end'(this: Scoped<SubagentService>, info: SubagentRunEndInfo): void
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-scope": "workspace:^",
|
||||
"@deepseek-ai/dsh-session": "workspace:^",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||
"@deepseek-ai/dsh-system-prompt": "workspace:^",
|
||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||
"cordis": "^4.0.0-rc.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { CallId, GenerateOptions } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent, AgentStatus } from '@deepseek-ai/dsh-agent'
|
||||
import { Session, SessionId, foldRequestHeader } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SurfaceEventType } from '@deepseek-ai/dsh-session'
|
||||
import { scopedSubjectResolverFor } from './scoped-events.generated.ts'
|
||||
|
||||
export const name = 'invariants'
|
||||
export const inject = ['sessions']
|
||||
@@ -75,17 +76,6 @@ interface SessionTraceTransition {
|
||||
seq: number
|
||||
}
|
||||
|
||||
/** Event payload prefix for scoped seams whose first argument names its agent. */
|
||||
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) {
|
||||
@@ -410,40 +400,12 @@ export function apply(ctx: Context): void {
|
||||
// (agent-scoped listeners over-hear foreign agents), and a mis-keyed one
|
||||
// delivers to the wrong agent's listeners. `internal/dispatch` fires
|
||||
// synchronously before listener delivery, so a violation throws at the
|
||||
// dispatching call site. The table maps each family to how its subject is
|
||||
// read from the event arguments; `null` = the subject is not recoverable
|
||||
// from the arguments (session events key by the OWNING agent; subagent
|
||||
// lifecycle events key by the delegating parent), so only carrier
|
||||
// PRESENCE is asserted there.
|
||||
const scopedSubject: Record<string, ((args: unknown[]) => unknown) | null> = {
|
||||
'agent/created': args => args[0],
|
||||
'agent/disposed': args => args[0],
|
||||
'agent/status': args => args[0],
|
||||
'agent/queued': args => args[0],
|
||||
'agent/session-start': args => args[0],
|
||||
'agent/pre-step': args => args[0],
|
||||
'agent/prompt-submit': args => args[0],
|
||||
'agent/request': args => args[0],
|
||||
'agent/session-prefix': args => args[0],
|
||||
'agent/step-result': args => args[0],
|
||||
'agent/turn-continuation': args => args[0],
|
||||
'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 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,
|
||||
'session/flush': null,
|
||||
'subagent/start': null,
|
||||
'subagent/end': null,
|
||||
}
|
||||
// dispatching call site. The generated table maps each family to the unique
|
||||
// payload path whose Program type matches the real scopeTarget routing key;
|
||||
// `null` means the key is external to the payload, so only carrier presence
|
||||
// can be asserted.
|
||||
ctx.on('internal/dispatch', (_mode, name, args, thisArg) => {
|
||||
const subjectOf = scopedSubject[name]
|
||||
const subjectOf = scopedSubjectResolverFor(name)
|
||||
if (subjectOf === undefined) return
|
||||
if (!isScopeCarrier(thisArg)) {
|
||||
throw new InvariantError(
|
||||
|
||||
69
packages/support/invariants/src/scoped-events.generated.ts
Normal file
69
packages/support/invariants/src/scoped-events.generated.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Generated scoped-event routing-subject resolvers for dsh-invariants.
|
||||
* Do not edit by hand; run `pnpm run gen-scoped-events`.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-invariants/scoped-events.generated
|
||||
*/
|
||||
|
||||
import type { Events } from 'cordis'
|
||||
import type { Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import type {} from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-subagent'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import type {} from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-user-approval'
|
||||
|
||||
type ScopedEventName = {
|
||||
[K in keyof Events]: ThisParameterType<Events[K]> extends Scoped<object> ? K : never
|
||||
}[keyof Events]
|
||||
|
||||
type ScopedSubjectResolver = (args: readonly unknown[]) => unknown
|
||||
|
||||
function adapt<K extends ScopedEventName>(
|
||||
resolver: (args: Parameters<Events[K]>) => unknown,
|
||||
): ScopedSubjectResolver {
|
||||
return args => resolver(args as Parameters<Events[K]>)
|
||||
}
|
||||
|
||||
const scopedSubjectResolvers = Object.freeze({
|
||||
'agent/created': adapt<'agent/created'>(args => args[0]),
|
||||
'agent/disposed': adapt<'agent/disposed'>(args => args[0]),
|
||||
'agent/error': adapt<'agent/error'>(args => args[0]),
|
||||
'agent/pre-step': adapt<'agent/pre-step'>(args => args[0]),
|
||||
'agent/prompt-submit': adapt<'agent/prompt-submit'>(args => args[0]),
|
||||
'agent/queued': adapt<'agent/queued'>(args => args[0]),
|
||||
'agent/request': adapt<'agent/request'>(args => args[0]),
|
||||
'agent/session-prefix': adapt<'agent/session-prefix'>(args => args[0]),
|
||||
'agent/session-start': adapt<'agent/session-start'>(args => args[0]),
|
||||
'agent/status': adapt<'agent/status'>(args => args[0]),
|
||||
'agent/step-result': adapt<'agent/step-result'>(args => args[0]),
|
||||
'agent/turn-continuation': adapt<'agent/turn-continuation'>(args => args[0]),
|
||||
'agent/turn-stop': adapt<'agent/turn-stop'>(args => args[0]),
|
||||
'approval/request': adapt<'approval/request'>(args => args[0].agent),
|
||||
'session/created': null,
|
||||
'session/disposed': null,
|
||||
'session/event': null,
|
||||
'session/flush': null,
|
||||
'subagent/end': null,
|
||||
'subagent/start': null,
|
||||
'system-prompt/assemble': adapt<'system-prompt/assemble'>(args => args[1].scope),
|
||||
'tools/execute': adapt<'tools/execute'>(args => args[0].agent),
|
||||
'tools/post-execute': adapt<'tools/post-execute'>(args => args[0].agent),
|
||||
'tools/pre-execute': adapt<'tools/pre-execute'>(args => args[0].agent),
|
||||
'tools/result': adapt<'tools/result'>(args => args[0].agent),
|
||||
} as const satisfies Readonly<Record<ScopedEventName, ScopedSubjectResolver | null>>)
|
||||
|
||||
const scopedSubjectResolverIndex: Readonly<Record<string, ScopedSubjectResolver | null>> = scopedSubjectResolvers
|
||||
|
||||
/**
|
||||
* Resolve the routing key named by one scoped event payload. A null
|
||||
* resolver means the payload cannot expose its external routing key, so the
|
||||
* invariant checks carrier presence only.
|
||||
* @param event - runtime Cordis event name.
|
||||
* @returns the generated subject resolver, null for presence-only,
|
||||
* or undefined when the event is not scope-filtered.
|
||||
*/
|
||||
export function scopedSubjectResolverFor(event: string): ScopedSubjectResolver | null | undefined {
|
||||
return scopedSubjectResolverIndex[event]
|
||||
}
|
||||
@@ -834,7 +834,7 @@ describe('scoped-dispatch invariants', () => {
|
||||
|
||||
it('accepts a matching carrier and rejects a mismatched one for EVERY agent-subject event', async () => {
|
||||
const ctx = await scopedCtx()
|
||||
// Real Session objects: the session-start tracker WeakSet-keys them.
|
||||
// Real Session objects keep the synthetic Agent handles structurally valid.
|
||||
const agent = { id: 'a1', session: new Session(SessionId('a1-s')) } as unknown as Agent
|
||||
const other = { id: 'a2', session: new Session(SessionId('a2-s')) } as unknown as Agent
|
||||
// One dispatch per table row keeps every subject extractor covered: the
|
||||
|
||||
@@ -25,6 +25,18 @@
|
||||
},
|
||||
{
|
||||
"path": "../../core/scope"
|
||||
},
|
||||
{
|
||||
"path": "../../core/system-prompt"
|
||||
},
|
||||
{
|
||||
"path": "../../ui/user-approval"
|
||||
},
|
||||
{
|
||||
"path": "../../core/tools"
|
||||
},
|
||||
{
|
||||
"path": "../../subagent/subagent"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user