support.ts 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. /** Shared fixtures: temporary git repositories, logged turns, and event readers. */
  2. import { execFileSync } from 'node:child_process'
  3. import { mkdtemp, rm } from 'node:fs/promises'
  4. import { tmpdir } from 'node:os'
  5. import { join } from 'node:path'
  6. import type { Context } from '@deepseek-ai/cordis'
  7. import { ToolCallId, createAssistantMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
  8. import type { Session } from '@deepseek-ai/dsh-session'
  9. import type {} from '@deepseek-ai/dsh-tools'
  10. import type { WorkspaceChangesData } from '../src/types.ts'
  11. let callNumber = 0
  12. /** Run git synchronously inside a fixture repository. */
  13. export function git(cwd: string, ...args: string[]): string {
  14. return execFileSync('git', ['-c', 'user.email=t@example.com', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], {
  15. cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
  16. })
  17. }
  18. /** A temporary directory removed by the returned cleanup. */
  19. export async function scratchDir(prefix: string, cleanups: Array<() => Promise<unknown>>): Promise<string> {
  20. const dir = await mkdtemp(join(tmpdir(), prefix))
  21. cleanups.push(() => rm(dir, { recursive: true, force: true }))
  22. return dir
  23. }
  24. /** Open a turn with its first step. */
  25. export function startTurn(session: Session, turn: number): void {
  26. session.append('turn/start', { turn })
  27. session.append('step/start', { turn, step: 1 })
  28. }
  29. /** Log one settled tool call with an optional result `meta`. */
  30. export function toolCall(session: Session, turn: number, name: string, args: unknown, result: { meta?: unknown; isError?: boolean } = {}) {
  31. const callId = ToolCallId(`call-${++callNumber}`)
  32. const serialized = JSON.stringify(args)
  33. session.append('assistant/message', {
  34. stream: [], turn, step: 1,
  35. message: createAssistantMessage({
  36. content: [{ type: 'tool-call', id: callId, name, arguments: serialized }],
  37. source: { provider: 'deepseek-official', model: 'deepseek-v4-flash' },
  38. }),
  39. }, { surfaceOp: 'append' })
  40. const source = session.append('tool/call', { turn, step: 1, callId, name, arguments: serialized })
  41. return session.append('tool/result', {
  42. turn, step: 1,
  43. message: createToolResultMessage({ callId, content: [{ type: 'text', text: 'ok' }], isError: result.isError ?? false }),
  44. ...result.meta === undefined ? {} : { meta: result.meta as never },
  45. }, { surfaceOp: 'append', sourceEventSeqs: [source.seq] })
  46. }
  47. /** Close the step and the turn. */
  48. export function endTurn(session: Session, turn: number, reason: 'completed' | 'blocked' = 'completed'): void {
  49. session.append('step/end', { turn, step: 1 })
  50. session.append('turn/end', { turn, reason: { kind: reason } })
  51. }
  52. /** Wait for the plugin's queued git work through the same waterfall tool execution uses. */
  53. export async function settle(ctx: Context, session: Session): Promise<void> {
  54. await ctx.waterfall('tools/pre-execute', { agent: { session } } as never, () => Promise.resolve(undefined as never))
  55. }
  56. /** Recorded change events of one session. */
  57. export function changes(session: Session): WorkspaceChangesData[] {
  58. return session.snapshotEvents().filter(event => event.type === 'workspace/changes').map(event => event.data)
  59. }