session.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. /**
  2. * Shared session utilities for council and background managers.
  3. */
  4. import type { PluginInput } from '@opencode-ai/plugin';
  5. type OpencodeClient = PluginInput['client'];
  6. /**
  7. * Extract the short model label from a "provider/model" string.
  8. * E.g. "openai/gpt-5.5-fast" → "gpt-5.5-fast"
  9. */
  10. export function shortModelLabel(model: string): string {
  11. return model.split('/').pop() ?? model;
  12. }
  13. export type PromptBody = {
  14. messageID?: string;
  15. model?: { providerID: string; modelID: string };
  16. agent?: string;
  17. noReply?: boolean;
  18. system?: string;
  19. tools?: { [key: string]: boolean };
  20. parts: Array<{ type: 'text'; text: string }>;
  21. variant?: string;
  22. };
  23. /**
  24. * Parse a model reference string into provider and model IDs.
  25. * @param model - Model string in format "provider/model"
  26. * @returns Object with providerID and modelID, or null if invalid
  27. */
  28. export function parseModelReference(
  29. model: string,
  30. ): { providerID: string; modelID: string } | null {
  31. const slashIndex = model.indexOf('/');
  32. if (slashIndex <= 0 || slashIndex >= model.length - 1) {
  33. return null;
  34. }
  35. return {
  36. providerID: model.slice(0, slashIndex),
  37. modelID: model.slice(slashIndex + 1),
  38. };
  39. }
  40. /**
  41. * Send a prompt to a session with optional timeout.
  42. * If timeout is exceeded, the session is aborted and an error is thrown.
  43. * @param client - OpenCode client instance
  44. * @param args - Arguments for session.prompt()
  45. * @param timeoutMs - Timeout in milliseconds (0 = no timeout)
  46. * @throws Error if timeout is exceeded
  47. */
  48. export async function promptWithTimeout(
  49. client: OpencodeClient,
  50. args: Parameters<OpencodeClient['session']['prompt']>[0],
  51. timeoutMs: number,
  52. ): Promise<void> {
  53. if (timeoutMs <= 0) {
  54. await client.session.prompt(args);
  55. return;
  56. }
  57. const sessionId = args.path.id;
  58. let timer: ReturnType<typeof setTimeout> | undefined;
  59. try {
  60. const promptPromise = client.session.prompt(args);
  61. promptPromise.catch(() => {});
  62. await Promise.race([
  63. promptPromise,
  64. new Promise<never>((_, reject) => {
  65. timer = setTimeout(() => {
  66. client.session.abort({ path: { id: sessionId } }).catch(() => {});
  67. reject(new Error(`Prompt timed out after ${timeoutMs}ms`));
  68. }, timeoutMs);
  69. }),
  70. ]);
  71. } finally {
  72. clearTimeout(timer);
  73. }
  74. }
  75. /**
  76. * Result of extracting session content.
  77. * `empty` is true when the assistant produced zero text content —
  78. * the provider returned an empty response (e.g. rate-limited silently).
  79. */
  80. export interface SessionExtractionResult {
  81. text: string;
  82. empty: boolean;
  83. }
  84. /**
  85. * Extract the result text from a session.
  86. * Collects all assistant messages and concatenates their text parts.
  87. * @param client - OpenCode client instance
  88. * @param sessionId - Session ID to extract from
  89. * @param options - Optional: `includeReasoning` (default true) controls whether
  90. * reasoning/chain-of-thought parts are included.
  91. * @returns Object with extracted text and an `empty` flag for zero-content detection
  92. */
  93. export async function extractSessionResult(
  94. client: OpencodeClient,
  95. sessionId: string,
  96. options?: { includeReasoning?: boolean },
  97. ): Promise<SessionExtractionResult> {
  98. const includeReasoning = options?.includeReasoning ?? true;
  99. const messagesResult = await client.session.messages({
  100. path: { id: sessionId },
  101. });
  102. const messages = (messagesResult.data ?? []) as Array<{
  103. info?: { role: string };
  104. parts?: Array<{ type: string; text?: string }>;
  105. }>;
  106. const assistantMessages = messages.filter(
  107. (m) => m.info?.role === 'assistant',
  108. );
  109. const extractedContent: string[] = [];
  110. for (const message of assistantMessages) {
  111. for (const part of message.parts ?? []) {
  112. const allowed = includeReasoning
  113. ? part.type === 'text' || part.type === 'reasoning'
  114. : part.type === 'text';
  115. if (allowed && part.text) {
  116. extractedContent.push(part.text);
  117. }
  118. }
  119. }
  120. const text = extractedContent.filter((t) => t.length > 0).join('\n\n');
  121. return { text, empty: text.length === 0 };
  122. }