[WIP] Add Agent Playground and Tool Playground for interactive debugging#1106
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/fdb1a79a-ee3d-49d8-ac23-7ef960bf001d Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/fdb1a79a-ee3d-49d8-ac23-7ef960bf001d Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
- Fixed ToolRegistry API usage (use has() and execute() instead of non-existent get() method) - Added extractOutputValue() helper to safely handle different output types - Updated tests to use correct ToolRegistry.register() signature (separate definition and handler params) - Fixed test expectations to match string output from handlers - All builds and tests now passing Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/4239d194-ecf4-4513-9729-f161ab40710d Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR adds interactive Studio “Playground” experiences for AI Agents and AI Tools, backed by a new AI service REST route for tool listing/execution, to support quick debugging/testing directly from selected metadata items.
Changes:
- Added
GET /api/v1/ai/toolsandPOST /api/v1/ai/tools/:toolName/executeroutes inservice-ai, and wired them intoAIServicePlugin. - Introduced Studio built-in Agent Playground (embedded chat) and Tool Playground (auto-generated parameter form + execution history) metadata viewers (priority 10).
- Added tests for the new tool routes and Studio plugin manifests/registration.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/services/service-ai/src/routes/tool-routes.ts | Adds tool list + tool execution REST routes for playground/testing. |
| packages/services/service-ai/src/plugin.ts | Registers the new tool routes during AI service plugin startup. |
| packages/services/service-ai/src/index.ts | Re-exports buildToolRoutes. |
| packages/services/service-ai/src/tests/tool-routes.test.ts | Adds route-level tests for listing/executing tools. |
| apps/studio/src/plugins/built-in/agent-playground-plugin.tsx | Adds agent-specific metadata viewer with embedded streaming chat + history tools. |
| apps/studio/src/plugins/built-in/tool-playground-plugin.tsx | Adds tool-specific viewer with schema-driven form, execution, and local history. |
| apps/studio/src/plugins/built-in/index.ts | Registers the two playground plugins in built-in plugin activation order. |
| apps/studio/test/playground-plugins.test.ts | Tests manifest structure and activation registration for the new plugins. |
| if (!parameters || typeof parameters !== 'object') { | ||
| return { status: 400, body: { error: 'parameters object is required' } }; | ||
| } |
There was a problem hiding this comment.
The route currently accepts any JSON value where typeof parameters === 'object', which also allows arrays. ToolRegistry.execute() then casts toolCall.input to Record<string, unknown>, so an array payload can slip through and break handlers in unexpected ways. Tighten validation to require a plain object (non-null and not an array).
| tools: tools.map(t => ({ | ||
| name: t.name, | ||
| description: t.description, | ||
| category: (t as any).category, |
There was a problem hiding this comment.
aiService.toolRegistry.getAll() returns AIToolDefinition (name/description/parameters) which does not include category, so this field will always be undefined unless you’re relying on undocumented extension fields. Either remove category from the response or extend the tool definition/registry API to return category explicitly.
| category: (t as any).category, |
| export function buildToolRoutes( | ||
| aiService: AIService, | ||
| logger: Logger, | ||
| ): RouteDefinition[] { |
There was a problem hiding this comment.
buildAIRoutes() is explicitly contract-driven (takes IAIService/IAIConversationService), but buildToolRoutes() currently depends on the concrete AIService class. To keep routes framework/service-implementation agnostic, consider accepting a narrower dependency (e.g., ToolRegistry or an interface exposing toolRegistry) instead of the concrete service type.
| <Textarea | ||
| id={name} | ||
| value={value ? JSON.stringify(value, null, 2) : ''} | ||
| onChange={(e) => { | ||
| try { | ||
| onChange(JSON.parse(e.target.value)); | ||
| } catch { | ||
| // Keep invalid JSON as string for now | ||
| } | ||
| }} |
There was a problem hiding this comment.
The array JSON textarea is effectively not editable: when the user types intermediate/invalid JSON, the catch block does nothing, but the component is controlled with value={JSON.stringify(value)}, so the UI immediately reverts to the last valid value. Consider keeping a separate raw text state for JSON inputs (and parsing/validating on blur/execute) so users can type freely and see validation errors.
| <Textarea | ||
| id={name} | ||
| value={value ? JSON.stringify(value, null, 2) : ''} | ||
| onChange={(e) => { | ||
| try { | ||
| onChange(JSON.parse(e.target.value)); | ||
| } catch { | ||
| // Keep invalid JSON as string for now | ||
| } | ||
| }} |
There was a problem hiding this comment.
Same issue as the array textarea: the object JSON textarea ignores invalid JSON in onChange, but the input is controlled by JSON.stringify(value), so partial edits can’t be entered. Use a raw string state (plus parse-on-commit) or allow storing the raw string and validate before execution.
| if (!response.ok) { | ||
| const errorData = await response.json().catch(() => ({ error: response.statusText })); | ||
| const entry: ExecutionEntry = { | ||
| id: entryId, | ||
| timestamp: Date.now(), | ||
| parameters: { ...parameters }, | ||
| status: 'error', | ||
| error: errorData.error || 'Tool execution failed', | ||
| duration, | ||
| }; | ||
| saveHistory([...history, entry]); | ||
| } else { | ||
| const result = await response.json(); | ||
| const entry: ExecutionEntry = { | ||
| id: entryId, | ||
| timestamp: Date.now(), | ||
| parameters: { ...parameters }, | ||
| status: 'success', | ||
| result: result.result || result, | ||
| duration, | ||
| }; | ||
| saveHistory([...history, entry]); | ||
| } |
There was a problem hiding this comment.
History updates use the history value captured in the executeTool closure (saveHistory([...history, entry])). If executions happen back-to-back (or React batches updates), entries can be dropped. Prefer a functional state update (e.g., derive next entries from the previous state) and persist that same computed array to localStorage.
| <button | ||
| onClick={() => setIsExpanded(!isExpanded)} | ||
| className="w-full flex items-center justify-between px-3 py-2 bg-muted/50 hover:bg-muted transition-colors text-left" | ||
| > |
There was a problem hiding this comment.
The expand/collapse control is a <button> that contains another <button> for copy, which is invalid HTML and can cause focus/keyboard/screen-reader issues. Consider changing the outer interactive element to a non-button container (or move the copy action outside the button) and wire up keyboard handling explicitly.
| // Call the tool via the AI service endpoint | ||
| // This is a dry-run execution - we're testing the tool logic |
There was a problem hiding this comment.
This comment describes the call as a “dry-run execution”, but the endpoint being called (POST /api/v1/ai/tools/:toolName/execute) executes the tool handler normally and may perform real side effects (writes, external calls, etc.). Either implement an explicit dry-run mode end-to-end, or adjust wording/UI to avoid implying that execution is side-effect free.
| // Call the tool via the AI service endpoint | |
| // This is a dry-run execution - we're testing the tool logic | |
| // Call the tool via the AI service endpoint. | |
| // This performs a normal execution of the tool handler and may trigger | |
| // real side effects depending on the tool implementation. |
| import { buildToolRoutes } from '../routes/tool-routes.js'; | ||
| import { AIService } from '../ai-service.js'; | ||
| import { InMemoryConversationService } from '../conversation/in-memory-conversation-service.js'; | ||
| import { ToolRegistry } from '../tools/tool-registry.js'; |
There was a problem hiding this comment.
ToolRegistry is imported but never used. With noUnusedLocals enabled for this package’s tsconfig, this will fail type-check/build. Remove the unused import.
| import { ToolRegistry } from '../tools/tool-registry.js'; |
Thanks for asking me to work on this. I will get started on it and keep this PR's description up to date as I form a plan and make progress.