feat(agent-teams): add read_inbox and read_config tools
- Add simple read_inbox tool as thin wrapper over readInbox store function - Add simple read_config tool as thin wrapper over readTeamConfig store function - Both tools support basic filtering (unread_only for inbox, none for config) - Comprehensive test coverage with TDD approach - Tools are separate from registered read_inbox/read_config (which have authorization)
This commit is contained in:
parent
4a38e09a33
commit
88be194805
87
src/tools/agent-teams/config-tools.test.ts
Normal file
87
src/tools/agent-teams/config-tools.test.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/// <reference types="bun-types" />
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { mkdtempSync, rmSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { createTeamConfig, deleteTeamData } from "./team-config-store"
|
||||
import { createReadConfigTool } from "./config-tools"
|
||||
|
||||
describe("read_config tool", () => {
|
||||
let originalCwd: string
|
||||
let tempProjectDir: string
|
||||
let teamName: string
|
||||
const TEST_SESSION_ID = "test-session-123"
|
||||
const TEST_ABORT_CONTROLLER = new AbortController()
|
||||
const TEST_CONTEXT = {
|
||||
sessionID: TEST_SESSION_ID,
|
||||
messageID: "test-message-123",
|
||||
agent: "test-agent",
|
||||
abort: TEST_ABORT_CONTROLLER.signal,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
originalCwd = process.cwd()
|
||||
tempProjectDir = mkdtempSync(join(tmpdir(), "agent-teams-config-tools-"))
|
||||
process.chdir(tempProjectDir)
|
||||
teamName = `test-team-${randomUUID()}`
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
deleteTeamData(teamName)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
process.chdir(originalCwd)
|
||||
rmSync(tempProjectDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe("read config action", () => {
|
||||
test("returns team config when team exists", async () => {
|
||||
//#given
|
||||
const config = createTeamConfig(teamName, "Test team", TEST_SESSION_ID, "/tmp", "claude-opus-4-6")
|
||||
const tool = createReadConfigTool()
|
||||
|
||||
//#when
|
||||
const resultStr = await tool.execute({
|
||||
team_name: teamName,
|
||||
}, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result.name).toBe(teamName)
|
||||
expect(result.description).toBe("Test team")
|
||||
expect(result.members).toHaveLength(1)
|
||||
expect(result.members[0].name).toBe("team-lead")
|
||||
expect(result.members[0].agentType).toBe("team-lead")
|
||||
})
|
||||
|
||||
test("returns error for non-existent team", async () => {
|
||||
//#given
|
||||
const tool = createReadConfigTool()
|
||||
|
||||
//#when
|
||||
const resultStr = await tool.execute({
|
||||
team_name: "nonexistent-team-12345",
|
||||
}, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("error")
|
||||
expect(result.error).toBe("team_not_found")
|
||||
})
|
||||
|
||||
test("requires team_name parameter", async () => {
|
||||
//#given
|
||||
const tool = createReadConfigTool()
|
||||
|
||||
//#when
|
||||
const resultStr = await tool.execute({}, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("error")
|
||||
})
|
||||
})
|
||||
})
|
||||
24
src/tools/agent-teams/config-tools.ts
Normal file
24
src/tools/agent-teams/config-tools.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import { readTeamConfig } from "./team-config-store"
|
||||
import { ReadConfigInputSchema } from "./types"
|
||||
|
||||
export function createReadConfigTool(): ToolDefinition {
|
||||
return tool({
|
||||
description: "Read team configuration and member list.",
|
||||
args: {
|
||||
team_name: tool.schema.string().describe("Team name"),
|
||||
},
|
||||
execute: async (args: Record<string, unknown>): Promise<string> => {
|
||||
try {
|
||||
const input = ReadConfigInputSchema.parse(args)
|
||||
const config = readTeamConfig(input.team_name)
|
||||
if (!config) {
|
||||
return JSON.stringify({ error: "team_not_found" })
|
||||
}
|
||||
return JSON.stringify(config)
|
||||
} catch (error) {
|
||||
return JSON.stringify({ error: error instanceof Error ? error.message : "read_config_failed" })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
182
src/tools/agent-teams/inbox-tools.test.ts
Normal file
182
src/tools/agent-teams/inbox-tools.test.ts
Normal file
@ -0,0 +1,182 @@
|
||||
/// <reference types="bun-types" />
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
import { mkdtempSync, rmSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { appendInboxMessage, ensureInbox } from "./inbox-store"
|
||||
import { deleteTeamData } from "./team-config-store"
|
||||
import { createReadInboxTool } from "./inbox-tools"
|
||||
|
||||
describe("read_inbox tool", () => {
|
||||
let originalCwd: string
|
||||
let tempProjectDir: string
|
||||
let teamName: string
|
||||
const TEST_SESSION_ID = "test-session-123"
|
||||
const TEST_ABORT_CONTROLLER = new AbortController()
|
||||
const TEST_CONTEXT = {
|
||||
sessionID: TEST_SESSION_ID,
|
||||
messageID: "test-message-123",
|
||||
agent: "test-agent",
|
||||
abort: TEST_ABORT_CONTROLLER.signal,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
originalCwd = process.cwd()
|
||||
tempProjectDir = mkdtempSync(join(tmpdir(), "agent-teams-inbox-tools-"))
|
||||
process.chdir(tempProjectDir)
|
||||
teamName = `test-team-${randomUUID()}`
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
try {
|
||||
deleteTeamData(teamName)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
process.chdir(originalCwd)
|
||||
rmSync(tempProjectDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(originalCwd)
|
||||
rmSync(tempProjectDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe("read inbox action", () => {
|
||||
test("returns all messages when no filters", async () => {
|
||||
//#given
|
||||
ensureInbox(teamName, "team-lead")
|
||||
appendInboxMessage(teamName, "team-lead", {
|
||||
id: "msg-1",
|
||||
type: "message",
|
||||
sender: "user",
|
||||
recipient: "team-lead",
|
||||
content: "Hello",
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
})
|
||||
appendInboxMessage(teamName, "team-lead", {
|
||||
id: "msg-2",
|
||||
type: "message",
|
||||
sender: "user",
|
||||
recipient: "team-lead",
|
||||
content: "World",
|
||||
timestamp: new Date().toISOString(),
|
||||
read: true,
|
||||
})
|
||||
|
||||
const tool = createReadInboxTool()
|
||||
|
||||
//#when
|
||||
const resultStr = await tool.execute({
|
||||
team_name: teamName,
|
||||
agent_name: "team-lead",
|
||||
}, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].id).toBe("msg-1")
|
||||
expect(result[1].id).toBe("msg-2")
|
||||
})
|
||||
|
||||
test("returns only unread messages when unread_only is true", async () => {
|
||||
//#given
|
||||
ensureInbox(teamName, "team-lead")
|
||||
appendInboxMessage(teamName, "team-lead", {
|
||||
id: "msg-1",
|
||||
type: "message",
|
||||
sender: "user",
|
||||
recipient: "team-lead",
|
||||
content: "Hello",
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
})
|
||||
appendInboxMessage(teamName, "team-lead", {
|
||||
id: "msg-2",
|
||||
type: "message",
|
||||
sender: "user",
|
||||
recipient: "team-lead",
|
||||
content: "World",
|
||||
timestamp: new Date().toISOString(),
|
||||
read: true,
|
||||
})
|
||||
|
||||
const tool = createReadInboxTool()
|
||||
|
||||
//#when
|
||||
const resultStr = await tool.execute({
|
||||
team_name: teamName,
|
||||
agent_name: "team-lead",
|
||||
unread_only: true,
|
||||
}, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].id).toBe("msg-1")
|
||||
})
|
||||
|
||||
test("marks messages as read when mark_as_read is true", async () => {
|
||||
//#given
|
||||
ensureInbox(teamName, "team-lead")
|
||||
appendInboxMessage(teamName, "team-lead", {
|
||||
id: "msg-1",
|
||||
type: "message",
|
||||
sender: "user",
|
||||
recipient: "team-lead",
|
||||
content: "Hello",
|
||||
timestamp: new Date().toISOString(),
|
||||
read: false,
|
||||
})
|
||||
|
||||
const tool = createReadInboxTool()
|
||||
|
||||
//#when
|
||||
await tool.execute({
|
||||
team_name: teamName,
|
||||
agent_name: "team-lead",
|
||||
mark_as_read: true,
|
||||
}, TEST_CONTEXT)
|
||||
|
||||
// Read again to check if marked as read
|
||||
const resultStr = await tool.execute({
|
||||
team_name: teamName,
|
||||
agent_name: "team-lead",
|
||||
unread_only: true,
|
||||
}, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveLength(0) // Should be marked as read
|
||||
})
|
||||
|
||||
test("returns empty array for non-existent inbox", async () => {
|
||||
//#given
|
||||
const tool = createReadInboxTool()
|
||||
|
||||
//#when
|
||||
const resultStr = await tool.execute({
|
||||
team_name: "nonexistent",
|
||||
agent_name: "team-lead",
|
||||
}, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
test("requires team_name and agent_name parameters", async () => {
|
||||
//#given
|
||||
const tool = createReadInboxTool()
|
||||
|
||||
//#when
|
||||
const resultStr = await tool.execute({}, TEST_CONTEXT)
|
||||
const result = JSON.parse(resultStr)
|
||||
|
||||
//#then
|
||||
expect(result).toHaveProperty("error")
|
||||
})
|
||||
})
|
||||
})
|
||||
29
src/tools/agent-teams/inbox-tools.ts
Normal file
29
src/tools/agent-teams/inbox-tools.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
||||
import { readInbox } from "./inbox-store"
|
||||
import { ReadInboxInputSchema } from "./types"
|
||||
|
||||
export function createReadInboxTool(): ToolDefinition {
|
||||
return tool({
|
||||
description: "Read inbox messages for a team member.",
|
||||
args: {
|
||||
team_name: tool.schema.string().describe("Team name"),
|
||||
agent_name: tool.schema.string().describe("Member name"),
|
||||
unread_only: tool.schema.boolean().optional().describe("Return only unread messages"),
|
||||
mark_as_read: tool.schema.boolean().optional().describe("Mark returned messages as read"),
|
||||
},
|
||||
execute: async (args: Record<string, unknown>): Promise<string> => {
|
||||
try {
|
||||
const input = ReadInboxInputSchema.parse(args)
|
||||
const messages = readInbox(
|
||||
input.team_name,
|
||||
input.agent_name,
|
||||
input.unread_only ?? false,
|
||||
input.mark_as_read ?? false,
|
||||
)
|
||||
return JSON.stringify(messages)
|
||||
} catch (error) {
|
||||
return JSON.stringify({ error: error instanceof Error ? error.message : "read_inbox_failed" })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user