@taico/worker 0.0.3 → 0.0.5

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/README.md CHANGED
@@ -8,9 +8,10 @@ The worker is a Node.js process that:
8
8
 
9
9
  1. Connects to the backend via REST API and WebSocket.
10
10
  2. Listens for task events that match the agent's status and tag triggers.
11
- 3. When a matching task appears, it clones the project's git repo (if configured) into a clean workspace.
12
- 4. Spins up the appropriate agent harness (Claude, OpenCode, ADK, or GitHub Copilot).
13
- 5. The agent works on the task, posting comments and status updates back to the backend.
11
+ 3. Runs a heartbeat reconcile loop that polls recent tasks and re-evaluates readiness.
12
+ 4. When a matching task appears, it clones the project's git repo (if configured) into a clean workspace.
13
+ 5. Spins up the appropriate agent harness (Claude, OpenCode, ADK, or GitHub Copilot).
14
+ 6. The agent works on the task, posting comments and status updates back to the backend.
14
15
 
15
16
  ## Architecture
16
17
 
@@ -60,6 +61,8 @@ AGENT_SLUG="claude" # Which agent this worker represent
60
61
  BASE_URL="http://localhost:2000" # URL where the backend is running
61
62
  ACCESS_TOKEN="your-token-here" # Token created in step 2
62
63
  WORK_DIR="/absolute/path/to/workspace" # Folder where work happens (absolute path)
64
+ HEARTBEAT_INTERVAL_MS="60000" # Optional: heartbeat interval (ms)
65
+ HEARTBEAT_TASK_LIMIT="100" # Optional: tasks reconciled per heartbeat
63
66
  ```
64
67
 
65
68
  ### 4. Start the Worker
@@ -2,9 +2,21 @@ export declare class Coordinator {
2
2
  private ready;
3
3
  private transport;
4
4
  private client;
5
+ private heartbeatTimer?;
6
+ private readonly concurrencyStore;
7
+ private readonly activeTaskIds;
8
+ private currentAgent?;
5
9
  constructor();
6
10
  connect(): Promise<boolean>;
7
11
  start(): Promise<boolean>;
8
12
  private handleEvent;
13
+ private runHeartbeat;
9
14
  private handleTask;
15
+ private maybeHandleInputRequest;
16
+ private canRunTaskNormally;
17
+ private dependenciesAreDone;
18
+ private getCurrentAgent;
19
+ private getConcurrencyLimit;
20
+ private buildPrompt;
21
+ private executeTask;
10
22
  }
@@ -1,5 +1,5 @@
1
1
  import { Taico } from "./Taico.js";
2
- import { ACCESS_TOKEN, AGENT_SLUG, BASE_URL } from "./helpers/config.js";
2
+ import { ACCESS_TOKEN, AGENT_SLUG, BASE_URL, HEARTBEAT_INTERVAL_MS, HEARTBEAT_TASK_LIMIT, } from "./helpers/config.js";
3
3
  import { prepareWorkspace } from "./helpers/prepareWorkspace.js";
4
4
  import { getSession, setSession } from "./helpers/sessionStore.js";
5
5
  import { ClaudeAgentRunner } from "./runners/ClaudeAgentRunner.js";
@@ -7,10 +7,15 @@ import { SocketIOTasksTransport } from "./SocketIOTasksTransport.js";
7
7
  import { OpencodeAgentRunner } from "./runners/OpenCodeAgentRunner.js";
8
8
  import { ADKAgentRunner } from "./runners/ADKAgentRunner.js";
9
9
  import { GitHubCopilotAgentRunner } from "./runners/GitHubCopilotAgentRunner.js";
10
+ import { AgentConcurrencyStore } from "./helpers/AgentConcurrencyStore.js";
10
11
  export class Coordinator {
11
12
  ready = false;
12
13
  transport;
13
14
  client;
15
+ heartbeatTimer;
16
+ concurrencyStore = new AgentConcurrencyStore();
17
+ activeTaskIds = new Set();
18
+ currentAgent;
14
19
  // Make transport
15
20
  constructor() {
16
21
  this.transport = new SocketIOTasksTransport(BASE_URL, ACCESS_TOKEN, {
@@ -36,11 +41,18 @@ export class Coordinator {
36
41
  }
37
42
  // Listen
38
43
  this.transport.onTaskEvent(this.handleEvent);
44
+ const heartbeatMs = Number.isFinite(HEARTBEAT_INTERVAL_MS) && HEARTBEAT_INTERVAL_MS > 0
45
+ ? HEARTBEAT_INTERVAL_MS
46
+ : 60_000;
47
+ this.heartbeatTimer = setInterval(() => {
48
+ void this.runHeartbeat();
49
+ }, heartbeatMs);
50
+ void this.runHeartbeat();
39
51
  return true;
40
52
  }
41
53
  handleEvent = async (evt) => {
42
54
  // For now just look at create and assign and status change
43
- if (evt.type === 'created' || evt.type === 'assigned' || evt.type === 'status_changed') {
55
+ if (evt.type === 'created' || evt.type === 'assigned' || evt.type === 'status_changed' || evt.type === 'updated') {
44
56
  console.log('--------------------------------------------------------');
45
57
  console.log('Event received');
46
58
  console.log(`- Type: ${evt.type}`);
@@ -53,9 +65,25 @@ export class Coordinator {
53
65
  console.log(`- Update caused by assignee. Ignoring as this is a self event. ❌`);
54
66
  return;
55
67
  }
56
- this.handleTask(task);
68
+ await this.maybeHandleInputRequest(task);
69
+ await this.handleTask(task);
57
70
  }
58
71
  };
72
+ async runHeartbeat() {
73
+ try {
74
+ const limit = Number.isFinite(HEARTBEAT_TASK_LIMIT) && HEARTBEAT_TASK_LIMIT > 0
75
+ ? Math.floor(HEARTBEAT_TASK_LIMIT)
76
+ : 100;
77
+ const tasks = await this.client.listTasks(1, limit);
78
+ for (const task of tasks) {
79
+ await this.maybeHandleInputRequest(task);
80
+ await this.handleTask(task);
81
+ }
82
+ }
83
+ catch (error) {
84
+ console.error('[heartbeat] failed to reconcile tasks', error);
85
+ }
86
+ }
59
87
  async handleTask(task) {
60
88
  // Get the agent
61
89
  const actor = task.assigneeActor;
@@ -73,6 +101,17 @@ export class Coordinator {
73
101
  console.log(`- We only react to @${AGENT_SLUG}. Skipping. ❌`);
74
102
  return;
75
103
  }
104
+ if (!(await this.canRunTaskNormally(task))) {
105
+ return;
106
+ }
107
+ if (Array.isArray(agent.tagTriggers) && agent.tagTriggers.length > 0) {
108
+ const taskTagNames = new Set((task.tags ?? []).map((tag) => tag.name));
109
+ const matchesTagTrigger = agent.tagTriggers.some((tagTrigger) => taskTagNames.has(tagTrigger));
110
+ if (!matchesTagTrigger) {
111
+ console.log(`- Agent @${agent.slug} requires tag trigger and task has none. Skip. ❌`);
112
+ return;
113
+ }
114
+ }
76
115
  // Do we have runners for this agent?
77
116
  if (agent.type !== "claude" && agent.type !== "opencode" && agent.type !== "adk" && agent.type !== "githubcopilot") {
78
117
  console.log(`- Agent @${actor.slug} of type "${agent.type}" not supported. Skipping. ❌`);
@@ -83,99 +122,200 @@ export class Coordinator {
83
122
  console.log(`- Agent @${agent.slug} doesn't react to status '${task.status}'. Skip. ❌`);
84
123
  return;
85
124
  }
86
- // Extract project slug from tags and get project repo URL
87
- let repoUrl = null;
88
- const projectTag = task.tags?.find((tag) => tag.name.startsWith('project:'));
89
- if (projectTag) {
90
- const projectSlug = projectTag.name.replace('project:', '');
91
- console.log(`- Found project tag: ${projectTag.name}, slug: ${projectSlug}`);
92
- const project = await this.client.getProjectBySlug(projectSlug);
93
- if (project) {
94
- console.log(`- Project found: ${project.slug}`);
95
- repoUrl = project.repoUrl ?? null;
96
- if (repoUrl) {
97
- console.log(`- Using project repo: ${repoUrl}`);
98
- }
99
- else {
100
- console.log(`- Project has no repoUrl, using default`);
101
- }
125
+ await this.executeTask(task, agent, 'normal');
126
+ }
127
+ async maybeHandleInputRequest(task) {
128
+ const agent = await this.getCurrentAgent();
129
+ if (!agent) {
130
+ return;
131
+ }
132
+ const unansweredForMe = task.inputRequests.find((inputRequest) => {
133
+ if (inputRequest.answer != null) {
134
+ return false;
102
135
  }
103
- else {
104
- console.log(`- Project not found for slug: ${projectSlug}`);
136
+ if (inputRequest.assignedToActorId !== agent.actorId) {
137
+ return false;
105
138
  }
106
- }
107
- console.log(`- ✅ Conditions met. @${agent.slug} starting to work on task "${task.name}" 🦄`);
108
- // Load session
109
- const sessionId = getSession(agent.actorId, task.id);
110
- // Prep workspace
111
- const workDir = await prepareWorkspace(task.id, agent.actorId, repoUrl);
112
- console.log(`- workspace prepped`);
113
- const run = await this.client.startRun(task.id);
114
- if (!run) {
115
- console.error(`Failed to create a run ❌`);
139
+ return task.assigneeActor?.id !== agent.actorId;
140
+ });
141
+ if (!unansweredForMe) {
116
142
  return;
117
143
  }
118
- console.log(`Started Agent Run ID ${run.id}`);
119
- // Create agent runner
120
- let runner = null;
121
- const modelConfig = {
122
- providerId: agent.providerId ?? undefined,
123
- modelId: agent.modelId ?? undefined,
124
- };
125
- if (agent.type === 'claude') {
126
- runner = new ClaudeAgentRunner(modelConfig);
144
+ console.log(`- Unanswered input request ${unansweredForMe.id} assigned to @${agent.slug}. Triggering response flow.`);
145
+ await this.executeTask(task, agent, 'input_request', unansweredForMe);
146
+ }
147
+ async canRunTaskNormally(task) {
148
+ if (!(await this.dependenciesAreDone(task))) {
149
+ return false;
127
150
  }
128
- else if (agent.type === 'opencode') {
129
- runner = new OpencodeAgentRunner(modelConfig);
151
+ const unresolvedInputRequests = task.inputRequests.filter((inputRequest) => inputRequest.answer == null);
152
+ if (unresolvedInputRequests.length > 0) {
153
+ console.log(`- Task ${task.id} has unresolved input requests. Skip until answered. ❌`);
154
+ return false;
130
155
  }
131
- else if (agent.type === 'adk') {
132
- runner = new ADKAgentRunner(modelConfig);
156
+ return true;
157
+ }
158
+ async dependenciesAreDone(task) {
159
+ if (!Array.isArray(task.dependsOnIds) || task.dependsOnIds.length === 0) {
160
+ return true;
133
161
  }
134
- else if (agent.type === 'githubcopilot') {
135
- runner = new GitHubCopilotAgentRunner(modelConfig);
162
+ for (const dependencyId of task.dependsOnIds) {
163
+ const dependencyTask = await this.client.getTask(dependencyId);
164
+ if (!dependencyTask) {
165
+ console.log(`- Dependency task ${dependencyId} not found. Skip. ❌`);
166
+ return false;
167
+ }
168
+ if (dependencyTask.status !== 'DONE') {
169
+ console.log(`- Dependency task ${dependencyId} is ${dependencyTask.status}. Skip. ❌`);
170
+ return false;
171
+ }
136
172
  }
137
- // This shouldn't happen because we checked first, but let's satisfy TypeScript
138
- if (!runner) {
139
- console.log(`- Agent @${actor.slug} of type "${agent.type}" not supported. Skipping. ❌`);
173
+ return true;
174
+ }
175
+ async getCurrentAgent() {
176
+ if (this.currentAgent) {
177
+ return this.currentAgent;
178
+ }
179
+ const agent = await this.client.getAgent(AGENT_SLUG);
180
+ if (!agent) {
181
+ console.log(`- Agent @${AGENT_SLUG} not found. ❌`);
182
+ return null;
183
+ }
184
+ this.currentAgent = agent;
185
+ return this.currentAgent;
186
+ }
187
+ getConcurrencyLimit(agent) {
188
+ if (typeof agent.concurrencyLimit === 'number' && Number.isFinite(agent.concurrencyLimit) && agent.concurrencyLimit > 0) {
189
+ return Math.floor(agent.concurrencyLimit);
190
+ }
191
+ return null;
192
+ }
193
+ buildPrompt(task, agent, mode, inputRequest) {
194
+ if (mode === 'input_request' && inputRequest) {
195
+ return [
196
+ `You got triggered by an unanswered input request in task "${task.id}".`,
197
+ 'You were asked a question and only need to answer that question.',
198
+ 'Do not go through the normal flow and do not complete the task unless explicitly requested.',
199
+ '',
200
+ `Input request id: ${inputRequest.id}`,
201
+ `Question: ${String(inputRequest.question ?? '')}`,
202
+ '',
203
+ 'Fetch the task, answer the input request assigned to you, and stop there.',
204
+ ].join('\n');
205
+ }
206
+ return `You got triggered by new activity in task "${task.id}". Fetch the task and proceed according to the following instructions.\n\n\n ${agent.systemPrompt}`;
207
+ }
208
+ async executeTask(task, agent, mode, inputRequest) {
209
+ if (this.activeTaskIds.has(task.id)) {
210
+ console.log(`- Task ${task.id} is already running in this worker. Skip. ❌`);
140
211
  return;
141
212
  }
213
+ const concurrencyLimit = this.getConcurrencyLimit(agent);
214
+ if (!this.concurrencyStore.tryAcquire(agent.actorId, concurrencyLimit)) {
215
+ console.log(`- Agent @${agent.slug} reached concurrency limit (${concurrencyLimit ?? 'unlimited'}). Skip. ❌`);
216
+ return;
217
+ }
218
+ this.activeTaskIds.add(task.id);
142
219
  try {
143
- const results = await runner.run({
144
- taskId: task.id,
145
- prompt: `You got triggered by new activity in task "${task.id}". Fetch the task and proceed according to the following instructions.\n\n\n ${agent.systemPrompt}`,
146
- cwd: workDir,
147
- runId: run.id,
148
- }, {
149
- onEvent: (message) => {
150
- console.log(`[agent message] ⤵️`);
151
- console.log(message);
152
- console.log('[end of agent message] ⤴️');
153
- this.transport.publishActivity({
154
- taskId: task.id,
155
- message,
156
- ts: Date.now(),
157
- });
158
- },
159
- onSession: (sessionId) => {
160
- if (!sessionId) {
161
- setSession(agent.actorId, task.id, sessionId);
220
+ // Extract project slug from tags and get project repo URL
221
+ let repoUrl = null;
222
+ const projectTag = task.tags?.find((tag) => tag.name.startsWith('project:'));
223
+ if (projectTag) {
224
+ const projectSlug = projectTag.name.replace('project:', '');
225
+ console.log(`- Found project tag: ${projectTag.name}, slug: ${projectSlug}`);
226
+ const project = await this.client.getProjectBySlug(projectSlug);
227
+ if (project) {
228
+ console.log(`- Project found: ${project.slug}`);
229
+ repoUrl = project.repoUrl ?? null;
230
+ if (repoUrl) {
231
+ console.log(`- Using project repo: ${repoUrl}`);
232
+ }
233
+ else {
234
+ console.log(`- Project has no repoUrl, using default`);
162
235
  }
163
- },
164
- onError: (error) => {
165
- console.log('Error detected');
166
- console.log('error message:', error.message);
167
- console.log('raw message', error.rawMessage);
168
- // Post error to task as a comment
169
- this.client.addComment(task.id, `⚠️ Error Detected ⚠️\n\n${error.message}\n\n\`\`\`json\nraw message\n${JSON.stringify(error.rawMessage, null, 2)}\n\`\`\``);
170
236
  }
171
- });
172
- console.log(results);
237
+ else {
238
+ console.log(`- Project not found for slug: ${projectSlug}`);
239
+ }
240
+ }
241
+ console.log(`- ✅ Conditions met. @${agent.slug} starting to work on task "${task.name}" 🦄`);
242
+ // Load session
243
+ const sessionId = getSession(agent.actorId, task.id);
244
+ // Prep workspace
245
+ const workDir = await prepareWorkspace(task.id, agent.actorId, repoUrl);
246
+ console.log(`- workspace prepped`);
247
+ const run = await this.client.startRun(task.id);
248
+ if (!run) {
249
+ console.error(`Failed to create a run ❌`);
250
+ return;
251
+ }
252
+ console.log(`Started Agent Run ID ${run.id}`);
253
+ // Create agent runner
254
+ let runner = null;
255
+ const modelConfig = {
256
+ providerId: agent.providerId ?? undefined,
257
+ modelId: agent.modelId ?? undefined,
258
+ };
259
+ if (agent.type === 'claude') {
260
+ runner = new ClaudeAgentRunner(modelConfig);
261
+ }
262
+ else if (agent.type === 'opencode') {
263
+ runner = new OpencodeAgentRunner(modelConfig);
264
+ }
265
+ else if (agent.type === 'adk') {
266
+ runner = new ADKAgentRunner(modelConfig);
267
+ }
268
+ else if (agent.type === 'githubcopilot') {
269
+ runner = new GitHubCopilotAgentRunner(modelConfig);
270
+ }
271
+ // This shouldn't happen because we checked first, but let's satisfy TypeScript
272
+ if (!runner) {
273
+ console.log(`- Agent @${agent.slug} of type "${agent.type}" not supported. Skipping. ❌`);
274
+ return;
275
+ }
276
+ try {
277
+ const results = await runner.run({
278
+ taskId: task.id,
279
+ prompt: this.buildPrompt(task, agent, mode, inputRequest),
280
+ cwd: workDir,
281
+ runId: run.id,
282
+ resume: sessionId ?? undefined,
283
+ }, {
284
+ onEvent: (message) => {
285
+ console.log(`[agent message] ⤵️`);
286
+ console.log(message);
287
+ console.log('[end of agent message] ⤴️');
288
+ this.transport.publishActivity({
289
+ taskId: task.id,
290
+ message,
291
+ ts: Date.now(),
292
+ });
293
+ },
294
+ onSession: (runnerSessionId) => {
295
+ if (runnerSessionId) {
296
+ setSession(agent.actorId, task.id, runnerSessionId);
297
+ }
298
+ },
299
+ onError: (error) => {
300
+ console.log('Error detected');
301
+ console.log('error message:', error.message);
302
+ console.log('raw message', error.rawMessage);
303
+ // Post error to task as a comment
304
+ this.client.addComment(task.id, `⚠️ Error Detected ⚠️\n\n${error.message}\n\n\`\`\`json\nraw message\n${JSON.stringify(error.rawMessage, null, 2)}\n\`\`\``);
305
+ }
306
+ });
307
+ console.log(results);
308
+ }
309
+ catch (error) {
310
+ console.error(`Error running task`);
311
+ console.error(error);
312
+ // Force a comment
313
+ this.client.addComment(task.id, `❌ Something went wrong ❌\n\n${error}`);
314
+ }
173
315
  }
174
- catch (error) {
175
- console.error(`Error running task`);
176
- console.error(error);
177
- // Force a comment
178
- this.client.addComment(task.id, `❌ Something went wrong ❌\n\n${error}`);
316
+ finally {
317
+ this.activeTaskIds.delete(task.id);
318
+ this.concurrencyStore.release(agent.actorId);
179
319
  }
180
320
  }
181
321
  }
package/dist/Taico.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { type AgentResponseDto, type AgentRunResponseDto, type ProjectResponseDto } from "@taico/client";
1
+ import { type AgentResponseDto, type AgentRunResponseDto, type ProjectResponseDto, type TaskResponseDto } from "@taico/client";
2
2
  export declare class Taico {
3
3
  constructor(baseUrl: string, accessToken: string);
4
4
  getAgent(agentSlug: string): Promise<AgentResponseDto | null>;
5
5
  getAgentPrompt(agentSlug: string): Promise<string>;
6
6
  getAgentStatusTriggers(agentSlug: string): Promise<string[]>;
7
7
  addComment(taskId: string, comment: string): Promise<void>;
8
+ listTasks(page?: number, limit?: number): Promise<TaskResponseDto[]>;
9
+ getTask(taskId: string): Promise<TaskResponseDto | null>;
8
10
  getProjectBySlug(slug: string): Promise<ProjectResponseDto | null>;
9
11
  startRun(taskId: string): Promise<AgentRunResponseDto | null>;
10
12
  }
package/dist/Taico.js CHANGED
@@ -44,6 +44,21 @@ export class Taico {
44
44
  console.error(`Failed to post comment to task ${taskId}:`, error);
45
45
  }
46
46
  }
47
+ async listTasks(page = 1, limit = 100) {
48
+ const response = await TaskService.tasksControllerListTasks(undefined, undefined, undefined, page, limit);
49
+ return response.items;
50
+ }
51
+ async getTask(taskId) {
52
+ try {
53
+ return await TaskService.tasksControllerGetTask(taskId);
54
+ }
55
+ catch (error) {
56
+ if (isApiError(error) && error.status === 404) {
57
+ return null;
58
+ }
59
+ throw error;
60
+ }
61
+ }
47
62
  async getProjectBySlug(slug) {
48
63
  try {
49
64
  return await MetaProjectsService.projectsControllerGetProjectBySlug(slug);
@@ -0,0 +1,6 @@
1
+ export declare class AgentConcurrencyStore {
2
+ private readonly counts;
3
+ tryAcquire(actorId: string, limit: number | null): boolean;
4
+ release(actorId: string): void;
5
+ getCount(actorId: string): number;
6
+ }
@@ -0,0 +1,40 @@
1
+ export class AgentConcurrencyStore {
2
+ counts = new Map();
3
+ tryAcquire(actorId, limit) {
4
+ try {
5
+ if (!actorId)
6
+ return false;
7
+ if (typeof limit === 'number' && limit > 0) {
8
+ const current = this.counts.get(actorId) ?? 0;
9
+ if (current >= limit) {
10
+ return false;
11
+ }
12
+ }
13
+ const current = this.counts.get(actorId) ?? 0;
14
+ this.counts.set(actorId, current + 1);
15
+ return true;
16
+ }
17
+ catch (error) {
18
+ console.error('[concurrency] failed to acquire slot', error);
19
+ return false;
20
+ }
21
+ }
22
+ release(actorId) {
23
+ try {
24
+ if (!actorId)
25
+ return;
26
+ const current = this.counts.get(actorId) ?? 0;
27
+ if (current <= 1) {
28
+ this.counts.delete(actorId);
29
+ return;
30
+ }
31
+ this.counts.set(actorId, current - 1);
32
+ }
33
+ catch (error) {
34
+ console.error('[concurrency] failed to release slot', error);
35
+ }
36
+ }
37
+ getCount(actorId) {
38
+ return this.counts.get(actorId) ?? 0;
39
+ }
40
+ }
@@ -2,4 +2,6 @@ export declare const ACCESS_TOKEN: string;
2
2
  export declare const BASE_URL: string;
3
3
  export declare const WORK_DIR: string;
4
4
  export declare const AGENT_SLUG: string;
5
+ export declare const HEARTBEAT_INTERVAL_MS: number;
6
+ export declare const HEARTBEAT_TASK_LIMIT: number;
5
7
  export declare const RUN_ID_HEADER = "x-taico-run-id";
@@ -2,6 +2,8 @@ export const ACCESS_TOKEN = process.env.ACCESS_TOKEN || '';
2
2
  export const BASE_URL = process.env.BASE_URL || '';
3
3
  export const WORK_DIR = process.env.WORK_DIR || '';
4
4
  export const AGENT_SLUG = process.env.AGENT_SLUG || '';
5
+ export const HEARTBEAT_INTERVAL_MS = Number(process.env.HEARTBEAT_INTERVAL_MS || 60_000);
6
+ export const HEARTBEAT_TASK_LIMIT = Number(process.env.HEARTBEAT_TASK_LIMIT || 100);
5
7
  export const RUN_ID_HEADER = 'x-taico-run-id';
6
8
  if (!ACCESS_TOKEN) {
7
9
  console.error('env ACCESS_TOKEN not available');
@@ -1,8 +1,45 @@
1
1
  // ADKAgentRunner.ts
2
2
  import { BaseAgentRunner } from "./BaseAgentRunner.js";
3
- import { LlmAgent, Runner, InMemorySessionService, MCPToolset } from "@google/adk";
3
+ import { LlmAgent, Runner, InMemorySessionService, MCPToolset, BaseTool, } from "@google/adk";
4
4
  import { ADKMessageFormatter } from "../formatters/ADKMessageFormatter.js";
5
5
  import { ACCESS_TOKEN, BASE_URL, RUN_ID_HEADER } from "../helpers/config.js";
6
+ class NamespacedTool extends BaseTool {
7
+ wrappedTool;
8
+ namespacedName;
9
+ constructor(wrappedTool, namespacedName) {
10
+ super({
11
+ name: namespacedName,
12
+ description: wrappedTool.description,
13
+ isLongRunning: wrappedTool.isLongRunning,
14
+ });
15
+ this.wrappedTool = wrappedTool;
16
+ this.namespacedName = namespacedName;
17
+ }
18
+ _getDeclaration() {
19
+ const declaration = this.wrappedTool._getDeclaration();
20
+ if (!declaration) {
21
+ return declaration;
22
+ }
23
+ return {
24
+ ...declaration,
25
+ name: this.namespacedName,
26
+ };
27
+ }
28
+ async runAsync(request) {
29
+ return this.wrappedTool.runAsync(request);
30
+ }
31
+ }
32
+ class NamespacedMCPToolset extends MCPToolset {
33
+ serverName;
34
+ constructor(connectionParams, serverName) {
35
+ super(connectionParams);
36
+ this.serverName = serverName;
37
+ }
38
+ async getTools(context) {
39
+ const tools = await super.getTools(context);
40
+ return tools.map((tool) => new NamespacedTool(tool, `mcp__${this.serverName}__${tool.name}`));
41
+ }
42
+ }
6
43
  export class ADKAgentRunner extends BaseAgentRunner {
7
44
  kind = 'adk';
8
45
  formatter = new ADKMessageFormatter();
@@ -26,14 +63,22 @@ export class ADKAgentRunner extends BaseAgentRunner {
26
63
  description: '',
27
64
  instruction: '',
28
65
  tools: [
29
- new MCPToolset({
66
+ new NamespacedMCPToolset({
30
67
  type: 'StreamableHTTPConnectionParams',
31
68
  url: `${BASE_URL}/api/v1/tasks/tasks/mcp`,
32
69
  header: {
33
70
  Authorization: `Bearer ${ACCESS_TOKEN}`,
34
71
  [RUN_ID_HEADER]: ctx.runId,
35
72
  },
36
- })
73
+ }, 'tasks'),
74
+ new NamespacedMCPToolset({
75
+ type: 'StreamableHTTPConnectionParams',
76
+ url: `${BASE_URL}/api/v1/context/blocks/mcp`,
77
+ header: {
78
+ Authorization: `Bearer ${ACCESS_TOKEN}`,
79
+ [RUN_ID_HEADER]: ctx.runId,
80
+ },
81
+ }, 'context')
37
82
  ]
38
83
  });
39
84
  const runner = new Runner({
@@ -27,10 +27,19 @@ export class ClaudeAgentRunner extends BaseAgentRunner {
27
27
  Authorization: `Bearer ${ACCESS_TOKEN}`,
28
28
  [RUN_ID_HEADER]: ctx.runId,
29
29
  },
30
+ },
31
+ context: {
32
+ type: "http",
33
+ url: `${BASE_URL}/api/v1/context/blocks/mcp`,
34
+ headers: {
35
+ Authorization: `Bearer ${ACCESS_TOKEN}`,
36
+ [RUN_ID_HEADER]: ctx.runId,
37
+ },
30
38
  }
31
39
  },
32
40
  allowedTools: [
33
41
  'mcp__tasks__*',
42
+ 'mcp__context__*',
34
43
  'SlashCommand',
35
44
  'Bash',
36
45
  'Read',
@@ -26,11 +26,21 @@ export class GitHubCopilotAgentRunner extends BaseAgentRunner {
26
26
  },
27
27
  tools: ["*"],
28
28
  };
29
+ const contextMcpServer = {
30
+ type: "http",
31
+ url: `${BASE_URL}/api/v1/context/blocks/mcp`,
32
+ headers: {
33
+ Authorization: `Bearer ${ACCESS_TOKEN}`,
34
+ [RUN_ID_HEADER]: ctx.runId,
35
+ },
36
+ tools: ["*"],
37
+ };
29
38
  // Create a session for this work
30
39
  const session = await this.client.createSession({
31
40
  model: this.model,
32
41
  mcpServers: {
33
42
  tasks: taskMcpServer,
43
+ context: contextMcpServer,
34
44
  },
35
45
  });
36
46
  if (session?.sessionId) {
@@ -79,6 +79,15 @@ export class OpencodeAgentRunner extends BaseAgentRunner {
79
79
  [RUN_ID_HEADER]: runId,
80
80
  },
81
81
  enabled: true,
82
+ },
83
+ context: {
84
+ type: "remote",
85
+ url: `${BASE_URL}/api/v1/context/blocks/mcp`,
86
+ headers: {
87
+ Authorization: `Bearer ${ACCESS_TOKEN}`,
88
+ [RUN_ID_HEADER]: runId,
89
+ },
90
+ enabled: true,
82
91
  }
83
92
  }
84
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taico/worker",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",