agent-tool-presentation.spec.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. /**
  2. * The row an agent preset carries to pick its tool presentation. What it owes
  3. * its caller: the choice reaches THIS agent and no other, it unwinds with the
  4. * agent, and a code mode composed against a deployment with no code runtime
  5. * stops at mount — where a preset's activation audit can name it — rather
  6. * than at the first prompt assembly.
  7. */
  8. import { describe, expect, it } from 'vitest'
  9. import { Context } from '@deepseek-ai/cordis'
  10. import { createScope } from '@deepseek-ai/dsh-scope'
  11. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  12. import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
  13. import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
  14. import ToolRuntime, { RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools'
  15. import type { Agent } from '@deepseek-ai/dsh-agent'
  16. import { SessionId } from '@deepseek-ai/dsh-session'
  17. import { apply, Config, inject, name } from '@deepseek-ai/dsh-agent-tool-presentation'
  18. /** A runtime that never runs anything: presentation never dispatches. */
  19. class StubRuntime extends CodeRuntime {
  20. readonly language = 'typescript'
  21. readonly isolation = 'stub'
  22. run(_request: CodeRunRequest): Promise<CodeRunResult> {
  23. return Promise.resolve({ logs: [] })
  24. }
  25. }
  26. /** A host plane with one tool, optionally carrying a code runtime. */
  27. async function host(options: { runtime?: boolean } = {}) {
  28. const ctx = new Context()
  29. await ctx.plugin(SystemPrompt, {})
  30. await ctx.plugin(ToolRuntime, {})
  31. if (options.runtime !== false) await ctx.plugin(StubRuntime)
  32. ctx.tools.register(defineTool({
  33. name: 'echo',
  34. description: 'Echo tool.',
  35. parameters: { value: { type: 'string', required: true } },
  36. output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
  37. execute: args => Promise.resolve(args.value),
  38. }))
  39. return ctx
  40. }
  41. /** Mount the row under one agent's scope, as a preset subtree does. */
  42. async function mount(ctx: Context, config: Config, id = 'agent') {
  43. const agent = { id: SessionId(id) } as Agent
  44. let inner!: Context
  45. const fiber = ctx.plugin(Object.assign((host: Context) => {
  46. inner = createScope(host, agent).ctx
  47. }, { inject: ['tools', 'systemPrompt'] }))
  48. await fiber.await()
  49. const row = inner.plugin({ name, inject: [...inject], Config, apply }, config)
  50. await row.await()
  51. return { agent, fiber, row }
  52. }
  53. describe('the tool-presentation row', () => {
  54. it('declares the services it uses without holding a code runtime hostage', () => {
  55. // A `native` row must mount where no runtime is composed, so the wait is
  56. // conditional inside apply rather than static metadata.
  57. expect(inject).toEqual(['tools'])
  58. })
  59. it('gives its own agent PTC mode and leaves the rest native', async () => {
  60. const ctx = await host()
  61. const coded = await mount(ctx, { mode: 'ptc' }, 'coded')
  62. const plain = await mount(ctx, { mode: 'native' }, 'plain')
  63. const codedAssembly = await ctx.systemPrompt.assemble({ scope: coded.agent })
  64. const plainAssembly = await ctx.systemPrompt.assemble({ scope: plain.agent })
  65. expect(codedAssembly.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
  66. expect(codedAssembly.sections.find(section => section.name === 'tools:sdk')?.text).toContain('echo')
  67. expect(plainAssembly.tools.map(tool => tool.name)).toEqual(['echo'])
  68. })
  69. it('presents both forms when asked for both', async () => {
  70. const ctx = await host()
  71. const { agent } = await mount(ctx, { mode: 'both' })
  72. const assembly = await ctx.systemPrompt.assemble({ scope: agent })
  73. expect(assembly.tools.map(tool => tool.name)).toEqual(['echo', RUN_CODE_NAME])
  74. })
  75. it('restores the deployment default when the agent unloads', async () => {
  76. const ctx = await host()
  77. const { agent, row } = await mount(ctx, { mode: 'ptc' })
  78. await row.dispose()
  79. // HMR safety: the preset subtree is torn down with its agent, and the
  80. // presentation must go with it rather than outliving the composition.
  81. const assembly = await ctx.systemPrompt.assemble({ scope: agent })
  82. expect(assembly.tools.map(tool => tool.name)).toEqual(['echo'])
  83. expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
  84. })
  85. it('waits for a code runtime the deployment does not compose', async () => {
  86. const ctx = await host({ runtime: false })
  87. const { agent, row } = await mount(ctx, { mode: 'ptc' })
  88. // Pending, not applied: `dsh-agent-presets` rejects a mount holding a row
  89. // that never reached a usable state, naming this id — so the preset fails
  90. // where the operator can act, instead of at the first request.
  91. expect(row.ctx.get('codeRuntime')).toBeUndefined()
  92. const assembly = await ctx.systemPrompt.assemble({ scope: agent })
  93. expect(assembly.tools.map(tool => tool.name)).toEqual(['echo'])
  94. })
  95. it('applies once the runtime arrives', async () => {
  96. const ctx = await host({ runtime: false })
  97. const { agent } = await mount(ctx, { mode: 'ptc' })
  98. await ctx.plugin(StubRuntime)
  99. const assembly = await ctx.systemPrompt.assemble({ scope: agent })
  100. expect(assembly.tools.map(tool => tool.name)).toEqual([RUN_CODE_NAME])
  101. })
  102. it('requires a mode rather than defaulting one', () => {
  103. // An omitted value would mean the row was composed for nothing: a preset
  104. // without this row already gets the deployment default.
  105. expect(() => Config({} as never)).toThrow()
  106. })
  107. })