agent-worker 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,67 +1,69 @@
1
- import { a as CursorCliBackend, c as FRONTIER_MODELS, d as createModelAsync, i as SdkBackend, l as SUPPORTED_PROVIDERS, n as createBackend, o as CodexCliBackend, r as listBackends, s as ClaudeCliBackend, t as checkBackends, u as createModel } from "./backends-BZ866Ij9.mjs";
2
- import { a as createSkillsTool, c as createTools, i as parseImportSpec, n as buildGitUrl, o as SkillsProvider, r as getSpecDisplayName, s as AgentSession, t as SkillImporter } from "./skills-CVdxwuvV.mjs";
1
+ import { _ as FRONTIER_MODELS, a as createMockBackend, b as createModelAsync, c as CodexBackend, i as MockAIBackend, l as ClaudeCodeBackend, n as createBackend, o as SdkBackend, r as listBackends, s as CursorBackend, t as checkBackends, v as SUPPORTED_PROVIDERS, y as createModel } from "./backends-DenGdkrj.mjs";
2
+ import { a as createSkillsTool, c as createFeedbackTool, i as parseImportSpec, l as AgentSession, n as buildGitUrl, o as SkillsProvider, r as getSpecDisplayName, s as FEEDBACK_PROMPT, t as SkillImporter } from "./skills-VyC7eQyK.mjs";
3
+ import { jsonSchema, tool } from "ai";
3
4
  import { createBashTool } from "bash-tool";
4
5
 
5
- //#region src/tools/bash.ts
6
+ //#region src/agent/tools/bash.ts
6
7
  /**
7
8
  * Integration with Vercel's bash-tool for file system operations
8
9
  *
9
10
  * Provides bash, readFile, writeFile tools for AI agents in a sandboxed environment
10
11
  */
11
12
  /**
12
- * Create bash tools as ToolDefinition array for use with AgentSession
13
+ * Create bash tools as AI SDK tool() objects for use with AgentSession
13
14
  *
14
15
  * @example
15
16
  * ```typescript
16
- * const bashTools = await createBashTools({
17
+ * const { tools } = await createBashTools({
17
18
  * files: { 'src/index.ts': 'console.log("hello")' }
18
19
  * })
19
20
  *
20
21
  * const session = new AgentSession({
21
22
  * model: 'anthropic/claude-sonnet-4-5',
22
23
  * system: 'You are a coding assistant.',
23
- * tools: bashTools
24
+ * tools
24
25
  * })
25
26
  * ```
26
27
  */
27
28
  async function createBashTools(options = {}) {
28
29
  const { includeReadFile = true, includeWriteFile = true, ...bashOptions } = options;
29
30
  const toolkit = await createBashTool(bashOptions);
30
- const tools = [];
31
- tools.push({
32
- name: "bash",
31
+ const tools = {};
32
+ tools.bash = tool({
33
33
  description: "Execute bash commands in a sandboxed environment. Returns stdout, stderr, and exit code.",
34
- parameters: {
34
+ inputSchema: jsonSchema({
35
35
  type: "object",
36
36
  properties: { command: {
37
37
  type: "string",
38
38
  description: "The bash command to execute"
39
39
  } },
40
40
  required: ["command"]
41
- },
41
+ }),
42
42
  execute: async (args) => {
43
- return await toolkit.tools.bash.execute(args);
43
+ const bashTool = toolkit.tools.bash;
44
+ if (!bashTool?.execute) throw new Error("Bash tool not available");
45
+ return bashTool.execute(args, {});
44
46
  }
45
47
  });
46
- if (includeReadFile) tools.push({
47
- name: "readFile",
48
+ if (includeReadFile) tools.readFile = tool({
48
49
  description: "Read the contents of a file from the sandbox filesystem.",
49
- parameters: {
50
+ inputSchema: jsonSchema({
50
51
  type: "object",
51
52
  properties: { path: {
52
53
  type: "string",
53
54
  description: "The path to the file to read"
54
55
  } },
55
56
  required: ["path"]
56
- },
57
+ }),
57
58
  execute: async (args) => {
58
- return await toolkit.tools.readFile.execute(args);
59
+ const readFileTool = toolkit.tools.readFile;
60
+ if (!readFileTool?.execute) throw new Error("ReadFile tool not available");
61
+ return readFileTool.execute(args, {});
59
62
  }
60
63
  });
61
- if (includeWriteFile) tools.push({
62
- name: "writeFile",
64
+ if (includeWriteFile) tools.writeFile = tool({
63
65
  description: "Write content to a file in the sandbox filesystem. Creates parent directories if needed.",
64
- parameters: {
66
+ inputSchema: jsonSchema({
65
67
  type: "object",
66
68
  properties: {
67
69
  path: {
@@ -74,9 +76,11 @@ async function createBashTools(options = {}) {
74
76
  }
75
77
  },
76
78
  required: ["path", "content"]
77
- },
79
+ }),
78
80
  execute: async (args) => {
79
- return await toolkit.tools.writeFile.execute(args);
81
+ const writeFileTool = toolkit.tools.writeFile;
82
+ if (!writeFileTool?.execute) throw new Error("WriteFile tool not available");
83
+ return writeFileTool.execute(args, {});
80
84
  }
81
85
  });
82
86
  return {
@@ -86,11 +90,6 @@ async function createBashTools(options = {}) {
86
90
  }
87
91
  /**
88
92
  * Quick helper to create bash tools with a directory
89
- *
90
- * @example
91
- * ```typescript
92
- * const { tools } = await createBashToolsFromDirectory('./src')
93
- * ```
94
93
  */
95
94
  async function createBashToolsFromDirectory(source, options = {}) {
96
95
  return createBashTools({
@@ -100,14 +99,6 @@ async function createBashToolsFromDirectory(source, options = {}) {
100
99
  }
101
100
  /**
102
101
  * Quick helper to create bash tools with inline files
103
- *
104
- * @example
105
- * ```typescript
106
- * const { tools } = await createBashToolsFromFiles({
107
- * 'index.ts': 'console.log("hello")',
108
- * 'package.json': '{"name": "test"}'
109
- * })
110
- * ```
111
102
  */
112
103
  async function createBashToolsFromFiles(files, options = {}) {
113
104
  return createBashTools({
@@ -117,4 +108,4 @@ async function createBashToolsFromFiles(files, options = {}) {
117
108
  }
118
109
 
119
110
  //#endregion
120
- export { AgentSession, ClaudeCliBackend, CodexCliBackend, CursorCliBackend, FRONTIER_MODELS, SUPPORTED_PROVIDERS, SdkBackend, SkillImporter, SkillsProvider, buildGitUrl, checkBackends, createBackend, createBashTool, createBashTools, createBashToolsFromDirectory, createBashToolsFromFiles, createModel, createModelAsync, createSkillsTool, createTools, getSpecDisplayName, listBackends, parseImportSpec };
111
+ export { AgentSession, ClaudeCodeBackend, CodexBackend, CursorBackend, FEEDBACK_PROMPT, FRONTIER_MODELS, MockAIBackend, SUPPORTED_PROVIDERS, SdkBackend, SkillImporter, SkillsProvider, buildGitUrl, checkBackends, createBackend, createBashTool, createBashTools, createBashToolsFromDirectory, createBashToolsFromFiles, createFeedbackTool, createMockBackend, createModel, createModelAsync, createSkillsTool, getSpecDisplayName, listBackends, parseImportSpec };
@@ -0,0 +1,549 @@
1
+ import { o as MemoryStorage, s as ContextProviderImpl } from "./cli/index.mjs";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { z } from "zod";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+
7
+ //#region src/workflow/context/memory-provider.ts
8
+ /**
9
+ * In-memory ContextProvider for testing.
10
+ * All domain logic is inherited from ContextProviderImpl;
11
+ * this class adds test helpers for inspection and cleanup.
12
+ */
13
+ var MemoryContextProvider = class extends ContextProviderImpl {
14
+ memoryStorage;
15
+ constructor(validAgents) {
16
+ const storage = new MemoryStorage();
17
+ super(storage, validAgents);
18
+ this.memoryStorage = storage;
19
+ }
20
+ /** Get underlying MemoryStorage (for testing) */
21
+ getStorage() {
22
+ return this.memoryStorage;
23
+ }
24
+ /** Get all channel messages (for testing, unfiltered) */
25
+ async getMessages() {
26
+ return this.readChannel();
27
+ }
28
+ /** Clear all data (for testing) */
29
+ clear() {
30
+ this.memoryStorage.clear();
31
+ }
32
+ /** Get all resources (for testing) */
33
+ async getResources() {
34
+ const keys = await this.memoryStorage.list("resources/");
35
+ const map = /* @__PURE__ */ new Map();
36
+ for (const key of keys) {
37
+ const content = await this.memoryStorage.read(`resources/${key}`);
38
+ if (content !== null) {
39
+ const id = key.replace(/\.[^.]+$/, "");
40
+ map.set(id, content);
41
+ }
42
+ }
43
+ return map;
44
+ }
45
+ /** Get inbox state for an agent (for testing) */
46
+ async getInboxState(agent) {
47
+ const raw = await this.memoryStorage.read("_state/inbox.json");
48
+ if (!raw) return void 0;
49
+ try {
50
+ return JSON.parse(raw).readCursors?.[agent];
51
+ } catch {
52
+ return;
53
+ }
54
+ }
55
+ /** Get all documents (for testing) */
56
+ async getDocuments() {
57
+ const files = await this.memoryStorage.list("documents/");
58
+ const map = /* @__PURE__ */ new Map();
59
+ for (const file of files) {
60
+ const content = await this.memoryStorage.read(`documents/${file}`);
61
+ if (content !== null) map.set(file, content);
62
+ }
63
+ return map;
64
+ }
65
+ };
66
+ /**
67
+ * Create a memory context provider
68
+ */
69
+ function createMemoryContextProvider(validAgents) {
70
+ return new MemoryContextProvider(validAgents);
71
+ }
72
+
73
+ //#endregion
74
+ //#region src/workflow/context/proposals.ts
75
+ /**
76
+ * Format a proposal for display
77
+ */
78
+ function formatProposal(proposal) {
79
+ const lines = [];
80
+ lines.push(`📋 **${proposal.title}** (${proposal.id})`);
81
+ lines.push(`Type: ${proposal.type} | Status: ${proposal.status}`);
82
+ if (proposal.description) lines.push(`\n${proposal.description}`);
83
+ lines.push("\nOptions:");
84
+ for (const option of proposal.options) {
85
+ const count = proposal.result?.counts[option.id] || 0;
86
+ const marker = proposal.result?.winner === option.id ? "✓ " : " ";
87
+ lines.push(`${marker}- ${option.label} (${option.id}): ${count} votes`);
88
+ }
89
+ if (proposal.result && Object.keys(proposal.result.votes).length > 0) {
90
+ lines.push("\nVotes:");
91
+ for (const [voter, choice] of Object.entries(proposal.result.votes)) lines.push(` @${voter} → ${choice}`);
92
+ }
93
+ if (proposal.status === "active" && proposal.expiresAt) {
94
+ const remaining = new Date(proposal.expiresAt).getTime() - Date.now();
95
+ const minutes = Math.max(0, Math.floor(remaining / 6e4));
96
+ lines.push(`\nExpires in: ${minutes} minutes`);
97
+ }
98
+ if (proposal.status === "resolved" && proposal.result?.winner) {
99
+ const winningOption = proposal.options.find((o) => o.id === proposal.result?.winner);
100
+ lines.push(`\n🏆 Winner: ${winningOption?.label || proposal.result.winner}`);
101
+ }
102
+ return lines.join("\n");
103
+ }
104
+ /**
105
+ * Format multiple proposals as a summary
106
+ */
107
+ function formatProposalList(proposals) {
108
+ if (proposals.length === 0) return "(no proposals)";
109
+ return proposals.map((p) => {
110
+ const votes = Object.keys(p.result?.votes || {}).length;
111
+ return `- ${p.id}: ${p.title} [${p.status}] (${votes} votes)`;
112
+ }).join("\n");
113
+ }
114
+
115
+ //#endregion
116
+ //#region src/workflow/context/mcp-server.ts
117
+ /**
118
+ * Context MCP Server
119
+ * Provides context tools to agents via Model Context Protocol
120
+ *
121
+ * Tool Taxonomy:
122
+ * - Channel: channel_send, channel_read (public append-only log)
123
+ * - Team: team_members, team_doc_*, team_proposal_*, team_vote (shared workspace)
124
+ * - My: my_inbox, my_inbox_ack (personal agent tools)
125
+ * - Resource: resource_create, resource_read (general-purpose reference mechanism)
126
+ * - Feedback: feedback_submit (agent observations about tools/workflows, opt-in)
127
+ */
128
+ /**
129
+ * Format inbox messages for display
130
+ */
131
+ function formatInbox(messages) {
132
+ if (messages.length === 0) return JSON.stringify({
133
+ messages: [],
134
+ count: 0
135
+ });
136
+ return JSON.stringify({
137
+ messages: messages.map((m) => ({
138
+ id: m.entry.id,
139
+ from: m.entry.from,
140
+ content: m.entry.content,
141
+ timestamp: m.entry.timestamp,
142
+ priority: m.priority
143
+ })),
144
+ count: messages.length
145
+ });
146
+ }
147
+ /**
148
+ * Create an MCP server that exposes context tools
149
+ *
150
+ * Tool taxonomy:
151
+ * - Channel: channel_send, channel_read
152
+ * - Team: team_members, team_doc_read, team_doc_write, team_doc_append,
153
+ * team_doc_list, team_doc_create, team_proposal_create, team_vote,
154
+ * team_proposal_status, team_proposal_cancel
155
+ * - My: my_inbox, my_inbox_ack
156
+ * - Resource: resource_create, resource_read
157
+ */
158
+ function createContextMCPServer(options) {
159
+ const { provider, validAgents, name = "workflow-context", version = "1.0.0", onMention, proposalManager, feedback: feedbackEnabled, debugLog } = options;
160
+ const feedbackEntries = [];
161
+ const logTool = (tool, agent, params) => {
162
+ if (debugLog) debugLog(`[mcp:${agent || "anonymous"}] ${tool}(${Object.entries(params).filter(([_, v]) => v !== void 0).map(([k, v]) => {
163
+ const val = typeof v === "string" && v.length > 50 ? v.slice(0, 50) + "..." : v;
164
+ return `${k}=${JSON.stringify(val)}`;
165
+ }).join(", ")})`);
166
+ };
167
+ const server = new McpServer({
168
+ name,
169
+ version
170
+ });
171
+ const agentConnections = /* @__PURE__ */ new Map();
172
+ const CHANNEL_MSG_LIMIT = 2e3;
173
+ server.tool("channel_send", `Send a message to the shared channel. Use @agent to mention/notify. Use "to" for private DMs. Max ${CHANNEL_MSG_LIMIT} chars — for longer content, use resource_create first then reference the resource ID in your message.`, {
174
+ message: z.string().describe("Message content, can include @mentions like @reviewer or @coder"),
175
+ to: z.string().optional().describe("Send as DM to a specific agent (private, only you and recipient see it)")
176
+ }, async ({ message, to }, extra) => {
177
+ const from = getAgentId(extra) || "anonymous";
178
+ logTool("channel_send", from, {
179
+ message,
180
+ to
181
+ });
182
+ if (message.length > CHANNEL_MSG_LIMIT) return {
183
+ isError: true,
184
+ content: [{
185
+ type: "text",
186
+ text: `Message too long (${message.length} chars, max ${CHANNEL_MSG_LIMIT}). Use resource_create to store the full content, then send a short message referencing the resource ID.`
187
+ }]
188
+ };
189
+ const sendOpts = to ? { to } : void 0;
190
+ const msg = await provider.appendChannel(from, message, sendOpts);
191
+ for (const target of msg.mentions) if (onMention) onMention(from, target, msg);
192
+ if (to && !msg.mentions.includes(to) && onMention) onMention(from, to, msg);
193
+ return { content: [{
194
+ type: "text",
195
+ text: JSON.stringify({
196
+ status: "sent",
197
+ timestamp: msg.timestamp,
198
+ mentions: msg.mentions,
199
+ to: msg.to
200
+ })
201
+ }] };
202
+ });
203
+ server.tool("channel_read", "Read messages from the shared channel. DMs and logs are automatically filtered based on your identity.", {
204
+ since: z.string().optional().describe("Read entries after this timestamp (ISO format)"),
205
+ limit: z.number().optional().describe("Maximum entries to return")
206
+ }, async ({ since, limit }, extra) => {
207
+ const agent = getAgentId(extra);
208
+ logTool("channel_read", agent, {
209
+ since,
210
+ limit
211
+ });
212
+ const entries = await provider.readChannel({
213
+ since,
214
+ limit,
215
+ agent
216
+ });
217
+ return { content: [{
218
+ type: "text",
219
+ text: JSON.stringify(entries)
220
+ }] };
221
+ });
222
+ server.tool("resource_create", "Store large content as a resource. Returns a reference (resource:id) usable in channel messages or documents.", {
223
+ content: z.string().describe("Content to store as resource"),
224
+ type: z.enum([
225
+ "markdown",
226
+ "json",
227
+ "text",
228
+ "diff"
229
+ ]).optional().describe("Content type hint (default: text)")
230
+ }, async ({ content, type }, extra) => {
231
+ const createdBy = getAgentId(extra) || "anonymous";
232
+ logTool("resource_create", createdBy, {
233
+ type,
234
+ contentLen: content.length
235
+ });
236
+ const result = await provider.createResource(content, createdBy, type);
237
+ return { content: [{
238
+ type: "text",
239
+ text: JSON.stringify({
240
+ id: result.id,
241
+ ref: result.ref,
242
+ hint: `Use [description](${result.ref}) in messages or documents`
243
+ })
244
+ }] };
245
+ });
246
+ server.tool("resource_read", "Read resource content by ID. Use when you encounter resource:id references.", { id: z.string().describe("Resource ID (e.g., res_abc123)") }, async ({ id }) => {
247
+ const content = await provider.readResource(id);
248
+ if (content === null) return { content: [{
249
+ type: "text",
250
+ text: JSON.stringify({ error: `Resource not found: ${id}` })
251
+ }] };
252
+ return { content: [{
253
+ type: "text",
254
+ text: content
255
+ }] };
256
+ });
257
+ server.tool("my_inbox", "Check your unread inbox messages. Does NOT acknowledge — use my_inbox_ack after processing.", {}, async (_args, extra) => {
258
+ const agent = getAgentId(extra) || "anonymous";
259
+ logTool("my_inbox", agent, {});
260
+ const messages = await provider.getInbox(agent);
261
+ if (debugLog && messages.length > 0) debugLog(`[mcp:${agent}] my_inbox → ${messages.length} unread`);
262
+ return { content: [{
263
+ type: "text",
264
+ text: formatInbox(messages)
265
+ }] };
266
+ });
267
+ server.tool("my_inbox_ack", "Acknowledge inbox messages up to a message ID. Call after processing messages.", { until: z.string().describe("Acknowledge messages up to and including this message ID") }, async ({ until }, extra) => {
268
+ const agent = getAgentId(extra) || "anonymous";
269
+ logTool("my_inbox_ack", agent, { until });
270
+ await provider.ackInbox(agent, until);
271
+ return { content: [{
272
+ type: "text",
273
+ text: JSON.stringify({
274
+ status: "acknowledged",
275
+ until
276
+ })
277
+ }] };
278
+ });
279
+ server.tool("team_members", "List all agents in this workflow. Use to discover who you can @mention.", {}, async (_args, extra) => {
280
+ const currentAgent = getAgentId(extra) || "anonymous";
281
+ const agents = validAgents.map((name) => ({
282
+ name,
283
+ mention: `@${name}`,
284
+ isYou: name === currentAgent
285
+ }));
286
+ return { content: [{
287
+ type: "text",
288
+ text: JSON.stringify({
289
+ agents,
290
+ count: agents.length,
291
+ hint: "Use @agent in channel_send to mention other agents"
292
+ })
293
+ }] };
294
+ });
295
+ server.tool("team_doc_read", "Read a shared team document.", { file: z.string().optional().describe("Document file path (default: notes.md)") }, async ({ file }, extra) => {
296
+ logTool("team_doc_read", getAgentId(extra), { file });
297
+ return { content: [{
298
+ type: "text",
299
+ text: await provider.readDocument(file) || "(empty document)"
300
+ }] };
301
+ });
302
+ server.tool("team_doc_write", "Write/replace a shared team document.", {
303
+ content: z.string().describe("New document content (replaces existing)"),
304
+ file: z.string().optional().describe("Document file path (default: notes.md)")
305
+ }, async ({ content, file }, extra) => {
306
+ logTool("team_doc_write", getAgentId(extra), {
307
+ file,
308
+ contentLen: content.length
309
+ });
310
+ await provider.writeDocument(content, file);
311
+ return { content: [{
312
+ type: "text",
313
+ text: `Document ${file || "notes.md"} written successfully`
314
+ }] };
315
+ });
316
+ server.tool("team_doc_append", "Append content to a shared team document.", {
317
+ content: z.string().describe("Content to append to the document"),
318
+ file: z.string().optional().describe("Document file path (default: notes.md)")
319
+ }, async ({ content, file }, extra) => {
320
+ logTool("team_doc_append", getAgentId(extra), {
321
+ file,
322
+ contentLen: content.length
323
+ });
324
+ await provider.appendDocument(content, file);
325
+ return { content: [{
326
+ type: "text",
327
+ text: `Content appended to ${file || "notes.md"}`
328
+ }] };
329
+ });
330
+ server.tool("team_doc_list", "List all shared team document files.", {}, async () => {
331
+ const files = await provider.listDocuments();
332
+ return { content: [{
333
+ type: "text",
334
+ text: JSON.stringify({
335
+ files,
336
+ count: files.length
337
+ })
338
+ }] };
339
+ });
340
+ server.tool("team_doc_create", "Create a new shared team document file.", {
341
+ file: z.string().describe("Document file path (e.g., \"findings/auth.md\")"),
342
+ content: z.string().describe("Initial document content")
343
+ }, async ({ file, content }) => {
344
+ await provider.createDocument(file, content);
345
+ return { content: [{
346
+ type: "text",
347
+ text: `Document ${file} created successfully`
348
+ }] };
349
+ });
350
+ if (proposalManager) {
351
+ server.tool("team_proposal_create", "Create a new proposal for team voting. Use for decisions, elections, approvals, or assignments.", {
352
+ type: z.enum([
353
+ "election",
354
+ "decision",
355
+ "approval",
356
+ "assignment"
357
+ ]).describe("Type of proposal"),
358
+ title: z.string().describe("Brief title for the proposal"),
359
+ description: z.string().optional().describe("Detailed description"),
360
+ options: z.array(z.object({
361
+ id: z.string().describe("Unique option identifier"),
362
+ label: z.string().describe("Display label for the option")
363
+ })).optional().describe("Voting options (required except for approval type)"),
364
+ resolution: z.object({
365
+ type: z.enum([
366
+ "plurality",
367
+ "majority",
368
+ "unanimous"
369
+ ]).optional().describe("How to determine winner"),
370
+ quorum: z.number().optional().describe("Minimum votes required"),
371
+ tieBreaker: z.enum([
372
+ "first",
373
+ "random",
374
+ "creator-decides"
375
+ ]).optional().describe("How to break ties")
376
+ }).optional().describe("Resolution rules"),
377
+ binding: z.boolean().optional().describe("Whether result is binding (default: true)"),
378
+ timeoutSeconds: z.number().optional().describe("Timeout in seconds (default: 3600)")
379
+ }, async (params, extra) => {
380
+ const createdBy = getAgentId(extra) || "anonymous";
381
+ try {
382
+ const proposal = proposalManager.create({
383
+ type: params.type,
384
+ title: params.title,
385
+ description: params.description,
386
+ options: params.options,
387
+ resolution: params.resolution,
388
+ binding: params.binding,
389
+ timeoutSeconds: params.timeoutSeconds,
390
+ createdBy
391
+ });
392
+ const optionsList = proposal.options.map((o) => `${o.id}: ${o.label}`).join(", ");
393
+ const otherAgents = validAgents.filter((a) => a !== createdBy).map((a) => `@${a}`).join(" ");
394
+ await provider.appendChannel(createdBy, `Created proposal "${proposal.title}" (${proposal.id})\nOptions: ${optionsList}\nUse team_vote tool to cast your vote. ${otherAgents}`);
395
+ return { content: [{
396
+ type: "text",
397
+ text: JSON.stringify({
398
+ status: "created",
399
+ proposal: {
400
+ id: proposal.id,
401
+ title: proposal.title,
402
+ options: proposal.options,
403
+ expiresAt: proposal.expiresAt
404
+ }
405
+ })
406
+ }] };
407
+ } catch (error) {
408
+ return { content: [{
409
+ type: "text",
410
+ text: JSON.stringify({
411
+ status: "error",
412
+ error: error instanceof Error ? error.message : String(error)
413
+ })
414
+ }] };
415
+ }
416
+ });
417
+ server.tool("team_vote", "Cast your vote on a team proposal.", {
418
+ proposal: z.string().describe("Proposal ID (e.g., prop-1)"),
419
+ choice: z.string().describe("Option ID to vote for"),
420
+ reason: z.string().optional().describe("Optional reason for your vote")
421
+ }, async ({ proposal: proposalId, choice, reason }, extra) => {
422
+ const voter = getAgentId(extra) || "anonymous";
423
+ const result = proposalManager.vote({
424
+ proposalId,
425
+ voter,
426
+ choice,
427
+ reason
428
+ });
429
+ if (!result.success) return { content: [{
430
+ type: "text",
431
+ text: JSON.stringify({
432
+ status: "error",
433
+ error: result.error
434
+ })
435
+ }] };
436
+ const reasonText = reason ? ` (reason: ${reason})` : "";
437
+ await provider.appendChannel(voter, `Voted "${choice}" on ${proposalId}${reasonText}`);
438
+ if (result.resolved && result.proposal) {
439
+ const winnerOption = result.proposal.options.find((o) => o.id === result.proposal.result?.winner);
440
+ const mentions = Object.keys(result.proposal.result?.votes || {}).map((v) => `@${v}`).join(" ");
441
+ await provider.appendChannel("system", `Proposal ${proposalId} resolved! Winner: ${winnerOption?.label || result.proposal.result?.winner || "none"} ${mentions}`);
442
+ }
443
+ return { content: [{
444
+ type: "text",
445
+ text: JSON.stringify({
446
+ status: "voted",
447
+ proposal: proposalId,
448
+ choice,
449
+ resolved: result.resolved,
450
+ winner: result.proposal?.result?.winner
451
+ })
452
+ }] };
453
+ });
454
+ server.tool("team_proposal_status", "Check status of team proposals. Omit proposal ID to see all active proposals.", { proposal: z.string().optional().describe("Proposal ID (omit for all active)") }, async ({ proposal: proposalId }) => {
455
+ if (proposalId) {
456
+ const proposal = proposalManager.get(proposalId);
457
+ if (!proposal) return { content: [{
458
+ type: "text",
459
+ text: JSON.stringify({
460
+ status: "error",
461
+ error: `Proposal not found: ${proposalId}`
462
+ })
463
+ }] };
464
+ return { content: [{
465
+ type: "text",
466
+ text: formatProposal(proposal)
467
+ }] };
468
+ }
469
+ const activeProposals = proposalManager.list("active");
470
+ return { content: [{
471
+ type: "text",
472
+ text: activeProposals.length > 0 ? formatProposalList(activeProposals) : "(no active proposals)"
473
+ }] };
474
+ });
475
+ server.tool("team_proposal_cancel", "Cancel a proposal you created.", { proposal: z.string().describe("Proposal ID to cancel") }, async ({ proposal: proposalId }, extra) => {
476
+ const cancelledBy = getAgentId(extra) || "anonymous";
477
+ const result = proposalManager.cancel(proposalId, cancelledBy);
478
+ if (!result.success) return { content: [{
479
+ type: "text",
480
+ text: JSON.stringify({
481
+ status: "error",
482
+ error: result.error
483
+ })
484
+ }] };
485
+ await provider.appendChannel(cancelledBy, `Cancelled proposal ${proposalId}`);
486
+ return { content: [{
487
+ type: "text",
488
+ text: JSON.stringify({
489
+ status: "cancelled",
490
+ proposal: proposalId
491
+ })
492
+ }] };
493
+ });
494
+ }
495
+ if (feedbackEnabled) server.tool("feedback_submit", "Report a workflow improvement need. Use when you hit something inconvenient — a missing tool, an awkward step, or a capability you wished you had.", {
496
+ target: z.string().describe("The area this is about — a tool name, a workflow step, or a general area (e.g. file search, code review)."),
497
+ type: z.enum([
498
+ "missing",
499
+ "friction",
500
+ "suggestion"
501
+ ]).describe("missing: a tool or capability you needed but didn't have. friction: something that works but is awkward or slow. suggestion: a concrete improvement idea."),
502
+ description: z.string().describe("What you needed or what could be improved. Be specific."),
503
+ context: z.string().optional().describe("Optional: what you were trying to do when you hit this.")
504
+ }, async ({ target, type, description, context: ctx }, extra) => {
505
+ logTool("feedback_submit", getAgentId(extra) || "anonymous", {
506
+ target,
507
+ type
508
+ });
509
+ const entry = {
510
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
511
+ target,
512
+ type,
513
+ description,
514
+ ...ctx ? { context: ctx } : {}
515
+ };
516
+ if (feedbackEntries.length >= 50) feedbackEntries.shift();
517
+ feedbackEntries.push(entry);
518
+ return { content: [{
519
+ type: "text",
520
+ text: JSON.stringify({ status: "recorded" })
521
+ }] };
522
+ });
523
+ return {
524
+ server,
525
+ agentConnections,
526
+ validAgents,
527
+ proposalManager,
528
+ getFeedback: () => [...feedbackEntries]
529
+ };
530
+ }
531
+ /**
532
+ * Extract agent ID from MCP extra context
533
+ * The agent ID is set via X-Agent-Id header or session metadata
534
+ */
535
+ function getAgentId(extra) {
536
+ if (!extra || typeof extra !== "object") return void 0;
537
+ if ("sessionId" in extra && typeof extra.sessionId === "string") {
538
+ const sid = extra.sessionId;
539
+ const match = sid.match(/^(.+)-[0-9a-f]{8}$/);
540
+ return match ? match[1] : sid;
541
+ }
542
+ if ("meta" in extra && extra.meta && typeof extra.meta === "object") {
543
+ const meta = extra.meta;
544
+ if ("agentId" in meta && typeof meta.agentId === "string") return meta.agentId;
545
+ }
546
+ }
547
+
548
+ //#endregion
549
+ export { createMemoryContextProvider as a, MemoryContextProvider as i, formatProposal as n, formatProposalList as r, createContextMCPServer as t };