@taico/worker 0.0.4 → 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 +6 -3
- package/dist/Coordinator.d.ts +12 -0
- package/dist/Coordinator.js +222 -82
- package/dist/Taico.d.ts +3 -1
- package/dist/Taico.js +15 -0
- package/dist/helpers/AgentConcurrencyStore.d.ts +6 -0
- package/dist/helpers/AgentConcurrencyStore.js +40 -0
- package/dist/helpers/config.d.ts +2 -0
- package/dist/helpers/config.js +2 -0
- package/package.json +1 -1
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.
|
|
12
|
-
4.
|
|
13
|
-
5.
|
|
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
|
package/dist/Coordinator.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/Coordinator.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
104
|
-
|
|
136
|
+
if (inputRequest.assignedToActorId !== agent.actorId) {
|
|
137
|
+
return false;
|
|
105
138
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
async dependenciesAreDone(task) {
|
|
159
|
+
if (!Array.isArray(task.dependsOnIds) || task.dependsOnIds.length === 0) {
|
|
160
|
+
return true;
|
|
133
161
|
}
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.log(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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,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
|
+
}
|
package/dist/helpers/config.d.ts
CHANGED
|
@@ -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";
|
package/dist/helpers/config.js
CHANGED
|
@@ -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');
|