Module: AgentHarness::Extensions::Adapters::Pi
- Defined in:
- lib/agent_harness/extensions.rb
Class Method Summary collapse
- .convention_extension_paths(root) ⇒ Object
- .direct_extension_entry_paths(path) ⇒ Object
- .discover_entry_paths(root, package) ⇒ Object
- .discover_tools(entry_paths) ⇒ Object
- .expand_manifest_entry(root, entry) ⇒ Object
- .extension_script?(path) ⇒ Boolean
-
.has_non_inline_register_tool_calls?(entry_paths) ⇒ Boolean
Detect whether any entry path contains registerTool calls that could not be statically extracted as inline object literals.
- .infer_unsupported_features(entry_paths) ⇒ Object
- .load(path) ⇒ Object
- .load_package_json(root) ⇒ Object
- .normalize_mcp_server(server) ⇒ Object
- .normalize_tool(tool) ⇒ Object
- .resolve_root(path) ⇒ Object
Class Method Details
.convention_extension_paths(root) ⇒ Object
438 439 440 441 442 443 |
# File 'lib/agent_harness/extensions.rb', line 438 def convention_extension_paths(root) extensions_dir = File.join(root, "extensions") return direct_extension_entry_paths(root) unless File.directory?(extensions_dir) direct_extension_entry_paths(extensions_dir) end |
.direct_extension_entry_paths(path) ⇒ Object
453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 |
# File 'lib/agent_harness/extensions.rb', line 453 def direct_extension_entry_paths(path) if File.file?(path) return [path] if extension_script?(path) return [] end return [] unless File.directory?(path) entries = [] entries.concat(Dir.glob(File.join(path, "*.{ts,js}"))) Dir.glob(File.join(path, "*")).sort.each do |child| next unless File.directory?(child) %w[index.ts index.js].each do |entry| entry_path = File.join(child, entry) entries << entry_path if File.file?(entry_path) end end entries end |
.discover_entry_paths(root, package) ⇒ Object
424 425 426 427 428 429 430 431 432 433 434 435 436 |
# File 'lib/agent_harness/extensions.rb', line 424 def discover_entry_paths(root, package) manifest_entries = Array(package.dig("pi", "extensions")) candidates = if manifest_entries.empty? convention_extension_paths(root) else manifest_entries.flat_map { |entry| (root, entry) } end paths = candidates.select { |candidate| File.file?(candidate) } raise ConfigurationError, "No pi extension entry points found in #{root}" if paths.empty? paths.uniq.sort end |
.discover_tools(entry_paths) ⇒ Object
478 479 480 481 482 483 484 485 486 487 488 489 490 |
# File 'lib/agent_harness/extensions.rb', line 478 def discover_tools(entry_paths) entry_paths.flat_map do |entry_path| source = File.read(entry_path) source.scan(/registerTool\s*\(\s*\{(.*?)\}\s*\)/m).filter_map do |match| block = match.first name = block[/name:\s*["']([^"']+)["']/, 1] next unless name description = block[/description:\s*["']([^"']+)["']/, 1] {name: name, description: description}.compact end end.uniq end |
.expand_manifest_entry(root, entry) ⇒ Object
445 446 447 448 449 450 451 |
# File 'lib/agent_harness/extensions.rb', line 445 def (root, entry) absolute = File.(entry, root) return direct_extension_entry_paths(absolute) if File.directory?(absolute) return Dir.glob(absolute).flat_map { |match| direct_extension_entry_paths(match) } unless File.exist?(absolute) direct_extension_entry_paths(absolute) end |
.extension_script?(path) ⇒ Boolean
474 475 476 |
# File 'lib/agent_harness/extensions.rb', line 474 def extension_script?(path) %w[.ts .js].include?(File.extname(path)) end |
.has_non_inline_register_tool_calls?(entry_paths) ⇒ Boolean
Detect whether any entry path contains registerTool calls that could not be statically extracted as inline object literals. When true, the extension should conservatively require :tool_use even if discover_tools returned an empty list.
496 497 498 499 500 501 502 503 |
# File 'lib/agent_harness/extensions.rb', line 496 def has_non_inline_register_tool_calls?(entry_paths) entry_paths.any? do |entry_path| source = File.read(entry_path) total_calls = source.scan(/registerTool\s*\(/).length inline_calls = source.scan(/registerTool\s*\(\s*\{/).length total_calls > inline_calls end end |
.infer_unsupported_features(entry_paths) ⇒ Object
505 506 507 508 509 510 511 512 513 514 515 516 517 |
# File 'lib/agent_harness/extensions.rb', line 505 def infer_unsupported_features(entry_paths) features = [] entry_paths.each do |entry_path| source = File.read(entry_path) features << :commands if source.include?("registerCommand") features << :shortcuts if source.include?("registerShortcut") features << :ui if source.match?(/ctx\.ui\.|setWidget|setStatus|setTitle/) features << :session_persistence if source.match?(/appendEntry|session_start|session_end/) end features.uniq end |
.load(path) ⇒ Object
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 |
# File 'lib/agent_harness/extensions.rb', line 359 def load(path) resolved_path = File.(path) single_file = File.file?(resolved_path) && %w[.ts .js].include?(File.extname(resolved_path)) && File.basename(resolved_path) != "package.json" root = resolve_root(resolved_path) package = load_package_json(root) entry_paths = if single_file # When a specific script file is requested, scope to that file only # instead of discovering all siblings in the parent directory. [resolved_path] else discover_entry_paths(root, package) end ext_config = package.fetch("agent_harness", {}) tools = ext_config["tools"] || discover_tools(entry_paths) system_prompt_additions = Array(ext_config["system_prompt_additions"]) mcp_servers = Array(ext_config["mcp_servers"]) required_provider_capabilities = Array(ext_config["required_provider_capabilities"]) # Conservatively require :tool_use when registerTool is called with non-inline # arguments that static extraction cannot parse. if !ext_config["tools"] && has_non_inline_register_tool_calls?(entry_paths) required_provider_capabilities |= ["tool_use"] end unsupported_features = Array(ext_config["unsupported_features"]) unsupported_features |= infer_unsupported_features(entry_paths) default_name = single_file ? File.basename(resolved_path, File.extname(resolved_path)) : File.basename(root) [ PiExtension.new( name: ext_config["name"] || package["name"] || default_name, description: ext_config["description"] || package["description"], version: ext_config["version"] || package["version"], tools: tools.map { |tool| normalize_tool(tool) }, system_prompt_additions: system_prompt_additions, mcp_servers: mcp_servers.map { |server| normalize_mcp_server(server) }, required_provider_capabilities: required_provider_capabilities.map(&:to_sym), unsupported_features: unsupported_features.map(&:to_sym), source_path: root, entry_paths: entry_paths ) ] end |
.load_package_json(root) ⇒ Object
415 416 417 418 419 420 421 422 |
# File 'lib/agent_harness/extensions.rb', line 415 def load_package_json(root) package_path = File.join(root, "package.json") return {} unless File.exist?(package_path) JSON.parse(File.read(package_path)) rescue JSON::ParserError => e raise ConfigurationError, "Invalid package.json for pi extension at #{root}: #{e.}" end |
.normalize_mcp_server(server) ⇒ Object
530 531 532 533 534 535 536 537 |
# File 'lib/agent_harness/extensions.rb', line 530 def normalize_mcp_server(server) case server when Hash server.transform_keys(&:to_sym) else raise ConfigurationError, "Unsupported MCP server definition in pi adapter: #{server.inspect}" end end |
.normalize_tool(tool) ⇒ Object
519 520 521 522 523 524 525 526 527 528 |
# File 'lib/agent_harness/extensions.rb', line 519 def normalize_tool(tool) case tool when Hash tool.transform_keys(&:to_sym) when String, Symbol {name: tool.to_s} else raise ConfigurationError, "Unsupported tool definition in pi adapter: #{tool.inspect}" end end |
.resolve_root(path) ⇒ Object
404 405 406 407 408 409 410 411 412 413 |
# File 'lib/agent_harness/extensions.rb', line 404 def resolve_root(path) if File.file?(path) return File.dirname(path) if File.basename(path) == "package.json" return File.dirname(path) if %w[.ts .js].include?(File.extname(path)) end return path if File.directory?(path) raise ConfigurationError, "Unsupported pi extension source: #{path}" end |