scope.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. /**
  2. * Client Agent-scope primitive: mint a Cordis context tagged with the owning
  3. * Agent's identity. The mechanism mirrors the host `dsh-scope` architecture
  4. * (no-op plugin fiber + context tag + `Context.filter` routing predicate);
  5. * the shape deliberately diverges: the filter lives on the actx itself
  6. * instead of a separate carrier object, so scoped dispatch is plain cordis —
  7. * `actx.bail(actx, event, payload)` / `actx.emit(actx, ...)` — with no
  8. * wrapper. The host needs a detached carrier because its dispatch subject is
  9. * the business Agent object; client scope events carry only ids, so the
  10. * actx is the natural subject. The second divergence stands: the scope key
  11. * is the branded `SessionId` (value compared), not an object identity — the
  12. * agent and its session share one id (1:1, same axis; no separate AgentId
  13. * brand), and a client scope's identity IS that wire id. Third divergence,
  14. * deliberate: the client scopes the Agent IDENTITY, not a live Agent object
  15. * — a cold session's host Agent is already disposed while its client actx
  16. * stays alive for history viewing.
  17. */
  18. import { Context as CordisContext } from '@deepseek-ai/cordis'
  19. import type { Context, Fiber } from '@deepseek-ai/cordis'
  20. import type { ClientRemote } from '@deepseek-ai/dsh-api-gateway/client'
  21. import type { SessionId } from '@deepseek-ai/dsh-session/types'
  22. import type { TypertRemoteScopeApi } from '@deepseek-ai/dsh-typert-protocol'
  23. /** Client Cordis Context carrying one Agent identity and its scoped Remote namespaces. */
  24. export type AgentContext = Omit<Context, 'remote'> & {
  25. readonly remote: ClientRemote & TypertRemoteScopeApi<'agent'>
  26. }
  27. /** Context tag written by {@link createScope}. */
  28. const kScope = Symbol('dsh.client.scope')
  29. /** A minted Agent scope and its disposal boundary. */
  30. export interface AgentScopeHandle {
  31. /**
  32. * Tagged context: scope-owned registrations and scoped dispatch both go
  33. * through it (passing it as the dispatch subject routes to this agent's
  34. * tagged listeners plus every untagged one).
  35. */
  36. ctx: AgentContext
  37. /** Backing fiber (dispose tears down every scope-owned registration). */
  38. fiber: Fiber
  39. }
  40. /** Shared no-op plugin backing each Agent scope fiber. */
  41. function agentScope(): void {}
  42. /**
  43. * Mint an Agent scope under `ctx`: a no-op plugin fiber whose context
  44. * carries the agent tag and the dispatch filter — untagged listeners are
  45. * admitted globally, tagged listeners only for a matching agent.
  46. * Registrations through the returned ctx dispose with the fiber.
  47. * @param ctx - client root context the scope fiber mounts under.
  48. * @param key - owning agent identity (the routing tag; agent id === session id).
  49. * @returns the tagged context and its backing fiber.
  50. */
  51. export function createScope(ctx: Context, key: SessionId): AgentScopeHandle {
  52. const fiber = ctx.plugin(agentScope)
  53. const scoped = fiber.ctx.extend({
  54. [kScope]: key,
  55. [CordisContext.filter](listenerCtx: Context): boolean {
  56. const tag = scopeOf(listenerCtx)
  57. return tag === undefined || tag === key
  58. },
  59. }) as AgentContext
  60. return {
  61. fiber,
  62. ctx: scoped,
  63. }
  64. }
  65. /**
  66. * Read the nearest agent tag inherited by a context.
  67. * @param ctx - any client context.
  68. * @returns its agent identity (the session id), or undefined for root contexts.
  69. */
  70. export function scopeOf(ctx: Context): SessionId | undefined {
  71. return (ctx as Context & { [kScope]?: SessionId })[kScope]
  72. }