Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -526,12 +526,15 @@ classDiagram

### Phase 2 — AI drives the ecosystem

- [ ] Register `@xnetjs/labs/agent-tools` (`lab_run`/`lab_list`/`lab_create`/
- [~] Register `@xnetjs/labs/agent-tools` (`lab_run`/`lab_list`/`lab_create`/
`lab_get`/`lab_run_saved`) with `mcp-server.ts` and the in-app agent runtime,
backed by an injected `LabAgentBackend`. _(deferred — the labs `LabAgentTool`
shape is already MCP-shaped; the remaining work is the cross-package adapter
(lives in labs, which already depends on plugins) + the app/electron MCP
registration.)_
backed by an injected `LabAgentBackend`. _As-built: `labs/agent-tools-ai.ts`'s
`labAgentToolsToAiTools()` adapts the (already MCP-shaped) `LabAgentTool`s to
the `AiCallableTool` shape `@xnetjs/plugins` uses — execution tools (`lab_run`
etc.) flagged `high` risk, reads `low`, input schemas pass straight through
(`LabToolPropertySchema ⊆ AiJsonSchema`). Lives in labs (which already
depends on plugins, avoiding the cycle). The app/electron MCP-server +
agent-runtime registration of the result is the remaining wiring._
- [x] Add `contributionsAsAiTools()` + an `aiExposed`/`inputSchema` opt-in on
`CommandContribution`; expose them as capability-scoped, callable
`AiToolDefinition`s. _As-built: `AiCommandExposure` adds
Expand Down
59 changes: 59 additions & 0 deletions packages/labs/src/__tests__/agent-tools-ai.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Tests for adapting Lab agent tools to AI tools (0194 Phase 2).
*/

import type { LabAgentTool } from '../agent-tools'
import { describe, it, expect, vi } from 'vitest'
import { labAgentToolsToAiTools } from '../agent-tools-ai'

function labTool(name: string, invoke = vi.fn().mockResolvedValue('ok')): LabAgentTool {
return {
name,
description: `The ${name} tool`,
inputSchema: { type: 'object', properties: { id: { type: 'string' } } },
invoke
}
}

describe('labAgentToolsToAiTools', () => {
it('adapts name/title/description and passes the input schema through', () => {
const [tool] = labAgentToolsToAiTools([labTool('lab_run')])
expect(tool.name).toBe('lab_run')
expect(tool.title).toBe('Lab run')
expect(tool.description).toBe('The lab_run tool')
expect(tool.inputSchema.properties.id).toEqual({ type: 'string' })
expect(tool.requiredScopes).toEqual(['workspace.read'])
})

it('marks execution tools high risk and read tools low risk', () => {
const tools = labAgentToolsToAiTools([
labTool('lab_run'),
labTool('lab_create'),
labTool('lab_run_saved'),
labTool('lab_get'),
labTool('lab_list')
])
const risk = Object.fromEntries(tools.map((t) => [t.name, t.risk]))
expect(risk).toEqual({
lab_run: 'high',
lab_create: 'high',
lab_run_saved: 'high',
lab_get: 'low',
lab_list: 'low'
})
})

it('invoke calls the underlying tool and wraps the result as text content', async () => {
const invoke = vi.fn().mockResolvedValue({ rows: 3 })
const [tool] = labAgentToolsToAiTools([labTool('lab_run', invoke)])
const result = await tool.invoke({ code: 'x' })
expect(invoke).toHaveBeenCalledWith({ code: 'x' })
expect(result.content).toEqual([{ type: 'text', text: '{"rows":3}' }])
})

it('passes a string result through unchanged', async () => {
const [tool] = labAgentToolsToAiTools([labTool('lab_get', vi.fn().mockResolvedValue('hello'))])
const result = await tool.invoke({ id: '1' })
expect(result.content[0].text).toBe('hello')
})
})
59 changes: 59 additions & 0 deletions packages/labs/src/agent-tools-ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @xnetjs/labs — Lab agent tools as AI tools (exploration 0194 Phase 2).
*
* `createLabAgentTools` already produces MCP-shaped tools (`lab_run`/`lab_list`/
* …) but they aren't in the workspace AI's tool surface. This adapts them to the
* `AiCallableTool` shape `@xnetjs/plugins` uses, so the agent can discover, run,
* and author Labs alongside the workspace tools — the AI→Lab→Plugin loop's first
* hop. It lives in labs because labs already depends on `@xnetjs/plugins` (the
* reverse would cycle).
*
* `LabToolPropertySchema` is structurally a subset of `AiJsonSchema`, so the
* input schema passes straight through. Execution tools (`lab_run`/`lab_create`/
* `lab_run_saved`) are marked `high` risk so the agent/consent layer can gate
* them; read tools (`lab_get`/`lab_list`) are `low`.
*/

import type { LabAgentTool } from './agent-tools'
import type { AiCallableTool } from '@xnetjs/plugins'

/** Lab tools that execute code (vs. read metadata) — surfaced as higher risk. */
const EXECUTION_TOOLS = new Set(['lab_run', 'lab_create', 'lab_run_saved'])

/** `lab_run` → `Lab run`. */
function titleize(name: string): string {
return name.replace(/_/g, ' ').replace(/^\w/, (c) => c.toUpperCase())
}

/** Stringify a tool result for the text content block. */
function resultText(value: unknown): string {
if (typeof value === 'string') return value
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}

function toAiTool(tool: LabAgentTool): AiCallableTool {
return {
name: tool.name,
title: titleize(tool.name),
description: tool.description,
risk: EXECUTION_TOOLS.has(tool.name) ? 'high' : 'low',
requiredScopes: ['workspace.read'],
inputSchema: tool.inputSchema,
invoke: async (args) => ({
content: [{ type: 'text', text: resultText(await tool.invoke(args)) }]
})
}
}

/**
* Adapt Lab agent tools to the `AiCallableTool` shape so the workspace AI can
* call them. Pair with `createLabAgentTools(...)` and register the result with
* the MCP server / in-app agent runtime.
*/
export function labAgentToolsToAiTools(tools: readonly LabAgentTool[]): AiCallableTool[] {
return tools.map(toAiTool)
}
3 changes: 3 additions & 0 deletions packages/labs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export type {
LabToolPropertySchema
} from './agent-tools'

// Lab agent tools as AI tools (0194 Phase 2) — for the MCP server / agent runtime.
export { labAgentToolsToAiTools } from './agent-tools-ai'

// Extension publishing
export { buildLabExtensionManifest, publishLabAsExtension, slugifyForId } from './extension'
export type {
Expand Down
Loading