agent-model-evaluator.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. /**
  2. * AgentModelEvaluator - Logs agent and model information for test transparency
  3. *
  4. * This evaluator provides visibility into which agent and model were used during test execution.
  5. * It extracts actual agent metadata from eval-runner.md and logs it prominently.
  6. *
  7. * Features:
  8. * - Reads eval-runner.md to extract actual agent metadata (id, name, description)
  9. * - Shows agent prompt snippet (first 200 chars) to confirm correct agent loaded
  10. * - Logs expected vs actual agent/model for comparison
  11. * - Always passes (informational only, not validation)
  12. *
  13. * Note: This is INFORMATIONAL ONLY. Actual agent/model configuration happens at test setup time
  14. * via test-runner.ts setupEvalRunner(). Session data doesn't store agent/model metadata.
  15. */
  16. import { BaseEvaluator } from './base-evaluator.js';
  17. import {
  18. TimelineEvent,
  19. SessionInfo,
  20. EvaluationResult,
  21. Evidence,
  22. Violation
  23. } from '../types/index.js';
  24. import { readFileSync, existsSync } from 'fs';
  25. import { join } from 'path';
  26. import { homedir } from 'os';
  27. export interface AgentModelExpectations {
  28. /** Expected agent name (e.g., "openagent", "opencoder", "coder-agent") */
  29. expectedAgent?: string;
  30. /** Expected model (e.g., "opencode/grok-code", "anthropic/claude-3-5-sonnet-20241022") */
  31. expectedModel?: string;
  32. /** Project path to find eval-runner.md */
  33. projectPath?: string;
  34. }
  35. interface AgentMetadata {
  36. id?: string;
  37. name?: string;
  38. description?: string;
  39. category?: string;
  40. type?: string;
  41. version?: string;
  42. mode?: string;
  43. promptSnippet?: string;
  44. }
  45. export class AgentModelEvaluator extends BaseEvaluator {
  46. name = 'agent-model';
  47. description = 'Logs agent and model information for test transparency';
  48. private expectations: AgentModelExpectations;
  49. constructor(expectations: AgentModelExpectations = {}) {
  50. super();
  51. this.expectations = expectations;
  52. }
  53. /**
  54. * Extract agent metadata from eval-runner.md frontmatter
  55. */
  56. private extractAgentMetadata(projectPath: string): AgentMetadata {
  57. const metadata: AgentMetadata = {};
  58. try {
  59. const evalRunnerPath = join(projectPath, '.opencode', 'agent', 'eval-runner.md');
  60. if (!existsSync(evalRunnerPath)) {
  61. return metadata;
  62. }
  63. const content = readFileSync(evalRunnerPath, 'utf-8');
  64. // Extract frontmatter (between --- markers)
  65. const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
  66. if (!frontmatterMatch) {
  67. return metadata;
  68. }
  69. const frontmatter = frontmatterMatch[1];
  70. // Extract key fields
  71. const idMatch = frontmatter.match(/^id:\s*(.+)$/m);
  72. const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
  73. const descMatch = frontmatter.match(/^description:\s*["'](.+)["']$/m);
  74. const categoryMatch = frontmatter.match(/^category:\s*(.+)$/m);
  75. const typeMatch = frontmatter.match(/^type:\s*(.+)$/m);
  76. const versionMatch = frontmatter.match(/^version:\s*(.+)$/m);
  77. const modeMatch = frontmatter.match(/^mode:\s*(.+)$/m);
  78. if (idMatch) metadata.id = idMatch[1].trim();
  79. if (nameMatch) metadata.name = nameMatch[1].trim();
  80. if (descMatch) metadata.description = descMatch[1].trim();
  81. if (categoryMatch) metadata.category = categoryMatch[1].trim();
  82. if (typeMatch) metadata.type = typeMatch[1].trim();
  83. if (versionMatch) metadata.version = versionMatch[1].trim();
  84. if (modeMatch) metadata.mode = modeMatch[1].trim();
  85. // Extract prompt snippet (first 200 chars after frontmatter)
  86. const promptStart = content.indexOf('---', 4) + 3; // Skip first ---
  87. const promptContent = content.substring(promptStart).trim();
  88. const snippet = promptContent.substring(0, 200).replace(/\n/g, ' ').trim();
  89. metadata.promptSnippet = snippet + (promptContent.length > 200 ? '...' : '');
  90. } catch (error) {
  91. // Silently fail - this is informational only
  92. }
  93. return metadata;
  94. }
  95. /**
  96. * Extract model from timeline events
  97. * Model is passed in API calls but not stored in session metadata
  98. */
  99. private extractModelFromTimeline(timeline: TimelineEvent[]): string | undefined {
  100. // Look for session.created or message events that might contain model info
  101. // In practice, model isn't in timeline either, but we try
  102. return undefined; // Model not available in timeline
  103. }
  104. async evaluate(timeline: TimelineEvent[], sessionInfo: SessionInfo): Promise<EvaluationResult> {
  105. const violations: Violation[] = [];
  106. const evidence: Evidence[] = [];
  107. // Extract actual agent metadata from eval-runner.md
  108. const projectPath = this.expectations.projectPath || process.cwd();
  109. const actualAgent = this.extractAgentMetadata(projectPath);
  110. const actualModel = this.extractModelFromTimeline(timeline);
  111. // Log actual agent information
  112. if (actualAgent.id || actualAgent.name) {
  113. evidence.push(this.createEvidence(
  114. 'actual-agent-info',
  115. `Actual agent loaded: ${actualAgent.name || actualAgent.id || 'unknown'}`,
  116. {
  117. id: actualAgent.id,
  118. name: actualAgent.name,
  119. description: actualAgent.description,
  120. category: actualAgent.category,
  121. type: actualAgent.type,
  122. version: actualAgent.version,
  123. mode: actualAgent.mode,
  124. }
  125. ));
  126. }
  127. // Log agent prompt snippet for verification
  128. if (actualAgent.promptSnippet) {
  129. evidence.push(this.createEvidence(
  130. 'agent-prompt-snippet',
  131. `Agent prompt snippet: "${actualAgent.promptSnippet}"`,
  132. { snippet: actualAgent.promptSnippet }
  133. ));
  134. }
  135. // Log expected agent/model if set
  136. if (this.expectations.expectedAgent) {
  137. evidence.push(this.createEvidence(
  138. 'expected-agent',
  139. `Expected agent: ${this.expectations.expectedAgent}`,
  140. { expectedAgent: this.expectations.expectedAgent }
  141. ));
  142. }
  143. if (this.expectations.expectedModel) {
  144. evidence.push(this.createEvidence(
  145. 'expected-model',
  146. `Expected model: ${this.expectations.expectedModel}`,
  147. { expectedModel: this.expectations.expectedModel }
  148. ));
  149. }
  150. // Log informational note
  151. evidence.push(this.createEvidence(
  152. 'info-note',
  153. 'ℹ️ This evaluator is INFORMATIONAL ONLY - it logs agent/model info but does not validate',
  154. {
  155. note: 'Agent/model configuration happens at test setup time (test-runner.ts)',
  156. actualAgentId: actualAgent.id,
  157. expectedAgent: this.expectations.expectedAgent,
  158. expectedModel: this.expectations.expectedModel,
  159. }
  160. ));
  161. // Always pass - this is informational only
  162. const passed = true;
  163. const score = 100;
  164. return {
  165. evaluator: this.name,
  166. passed,
  167. score,
  168. violations,
  169. evidence,
  170. metadata: {
  171. actualAgent: {
  172. id: actualAgent.id,
  173. name: actualAgent.name,
  174. description: actualAgent.description,
  175. category: actualAgent.category,
  176. type: actualAgent.type,
  177. version: actualAgent.version,
  178. mode: actualAgent.mode,
  179. promptSnippet: actualAgent.promptSnippet,
  180. },
  181. expectedAgent: this.expectations.expectedAgent,
  182. expectedModel: this.expectations.expectedModel,
  183. mode: 'informational',
  184. }
  185. };
  186. }
  187. }