@taico/worker 0.0.1
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 +63 -0
- package/dist/Coordinator.d.ts +10 -0
- package/dist/Coordinator.js +183 -0
- package/dist/SocketIOTasksTransport.d.ts +68 -0
- package/dist/SocketIOTasksTransport.js +183 -0
- package/dist/Taico.d.ts +10 -0
- package/dist/Taico.js +69 -0
- package/dist/dev.d.ts +1 -0
- package/dist/dev.js +29 -0
- package/dist/formatters/ADKMessageFormatter.d.ts +4 -0
- package/dist/formatters/ADKMessageFormatter.js +32 -0
- package/dist/formatters/ClaudeMessageFormatter.d.ts +11 -0
- package/dist/formatters/ClaudeMessageFormatter.js +97 -0
- package/dist/formatters/OpencodeMessageFormatter.d.ts +8 -0
- package/dist/formatters/OpencodeMessageFormatter.js +56 -0
- package/dist/helpers/config.d.ts +5 -0
- package/dist/helpers/config.js +21 -0
- package/dist/helpers/ensureRepo.d.ts +1 -0
- package/dist/helpers/ensureRepo.js +27 -0
- package/dist/helpers/prepareWorkspace.d.ts +1 -0
- package/dist/helpers/prepareWorkspace.js +23 -0
- package/dist/helpers/sessionStore.d.ts +2 -0
- package/dist/helpers/sessionStore.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/interfaces/AgentRunResult.d.ts +5 -0
- package/dist/interfaces/AgentRunResult.js +2 -0
- package/dist/runners/ADKAgentRunner.d.ts +13 -0
- package/dist/runners/ADKAgentRunner.js +65 -0
- package/dist/runners/AgentRunner.d.ts +43 -0
- package/dist/runners/AgentRunner.js +1 -0
- package/dist/runners/BaseAgentRunner.d.ts +13 -0
- package/dist/runners/BaseAgentRunner.js +27 -0
- package/dist/runners/ClaudeAgentRunner.d.ts +11 -0
- package/dist/runners/ClaudeAgentRunner.js +68 -0
- package/dist/runners/GitHubCopilotAgentRunner.d.ts +12 -0
- package/dist/runners/GitHubCopilotAgentRunner.js +81 -0
- package/dist/runners/OpenCodeAgentRunner.d.ts +25 -0
- package/dist/runners/OpenCodeAgentRunner.js +163 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Taico Agent Worker
|
|
2
|
+
|
|
3
|
+
This is the agent worker process. It connects to the Taico backend, listens for task events, and executes AI agents in isolated workspaces.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
The worker is a Node.js process that:
|
|
8
|
+
|
|
9
|
+
1. Connects to the backend via REST API and WebSocket.
|
|
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.
|
|
14
|
+
|
|
15
|
+
## Architecture
|
|
16
|
+
|
|
17
|
+
The worker is intentionally decoupled from the backend. You can run workers anywhere — same machine, a different server, or in Kubernetes. Each worker represents one agent. To run multiple agents, start multiple worker processes.
|
|
18
|
+
|
|
19
|
+
The worker has access to whatever the host machine has access to. If you have Claude Code installed and authenticated, the Claude runner can use it. Same for OpenCode and GitHub Copilot.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
### 1. Create an Agent
|
|
24
|
+
|
|
25
|
+
In the Taico UI, go to **Agents** and create one (or use a pre-populated agent). Configure its system prompt, agent type, and status/tag triggers.
|
|
26
|
+
|
|
27
|
+
### 2. Create an Access Token
|
|
28
|
+
|
|
29
|
+
From the agent's page in the UI, create an access token. The token needs at least these scopes: `task:*`, `meta:*`, `context:*`, `agents:read`, `mcp:use`.
|
|
30
|
+
|
|
31
|
+
> **Note:** Token-based auth is a temporary solution. Eventually, workers will securely impersonate agents automatically — no manual token management needed. The UI is intentionally minimal because this flow will be replaced.
|
|
32
|
+
|
|
33
|
+
### 3. Configure the Worker
|
|
34
|
+
|
|
35
|
+
Create a `.env` file in this directory (one per agent). See `.env.example`:
|
|
36
|
+
|
|
37
|
+
```env
|
|
38
|
+
AGENT_SLUG="claude" # Which agent this worker represents
|
|
39
|
+
BASE_URL="http://localhost:2000" # URL where the backend is running
|
|
40
|
+
ACCESS_TOKEN="your-token-here" # Token created in step 2
|
|
41
|
+
WORK_DIR="/absolute/path/to/workspace" # Folder where work happens (absolute path)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 4. Start the Worker
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm -w apps/worker run start
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
To run multiple agents, create separate `.env` files (e.g., `.env.claude`, `.env.reviewer`) and start each worker with the appropriate env file loaded.
|
|
51
|
+
|
|
52
|
+
## Supported Agent Types
|
|
53
|
+
|
|
54
|
+
| Type | Harness | Requirements |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `claude` | Claude Agent SDK | Claude Code installed and authenticated |
|
|
57
|
+
| `opencode` | OpenCode SDK | OpenCode installed |
|
|
58
|
+
| `adk` | Google ADK | None (runs in-process). No tools — suited for general tasks like reading the board. |
|
|
59
|
+
| `githubcopilot` | GitHub Copilot SDK | GitHub Copilot set up on the machine |
|
|
60
|
+
|
|
61
|
+
## MCP Integration
|
|
62
|
+
|
|
63
|
+
Each agent run gets access to a Taico MCP server at the backend's `/api/v1/tasks/tasks/mcp` endpoint. This gives agents tools to interact with Taico — reading tasks, posting comments, creating subtasks, etc. Authentication is handled automatically via the access token and a per-run ID header.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Taico } from "./Taico.js";
|
|
2
|
+
import { ACCESS_TOKEN, AGENT_SLUG, BASE_URL } from "./helpers/config.js";
|
|
3
|
+
import { prepareWorkspace } from "./helpers/prepareWorkspace.js";
|
|
4
|
+
import { getSession, setSession } from "./helpers/sessionStore.js";
|
|
5
|
+
import { ClaudeAgentRunner } from "./runners/ClaudeAgentRunner.js";
|
|
6
|
+
import { SocketIOTasksTransport } from "./SocketIOTasksTransport.js";
|
|
7
|
+
import { OpencodeAgentRunner } from "./runners/OpenCodeAgentRunner.js";
|
|
8
|
+
import { ADKAgentRunner } from "./runners/ADKAgentRunner.js";
|
|
9
|
+
import { GitHubCopilotAgentRunner } from "./runners/GitHubCopilotAgentRunner.js";
|
|
10
|
+
export class Coordinator {
|
|
11
|
+
ready = false;
|
|
12
|
+
transport;
|
|
13
|
+
client;
|
|
14
|
+
// Make transport
|
|
15
|
+
constructor() {
|
|
16
|
+
this.transport = new SocketIOTasksTransport(BASE_URL, ACCESS_TOKEN, {
|
|
17
|
+
namespace: '/tasks',
|
|
18
|
+
// debug: true,
|
|
19
|
+
});
|
|
20
|
+
this.client = new Taico(BASE_URL, ACCESS_TOKEN);
|
|
21
|
+
}
|
|
22
|
+
async connect() {
|
|
23
|
+
try {
|
|
24
|
+
await this.transport.start();
|
|
25
|
+
this.ready = true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
this.ready = false;
|
|
29
|
+
}
|
|
30
|
+
return this.ready;
|
|
31
|
+
}
|
|
32
|
+
async start() {
|
|
33
|
+
// Connect
|
|
34
|
+
if (!(await this.connect())) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
// Listen
|
|
38
|
+
this.transport.onTaskEvent(this.handleEvent);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
handleEvent = async (evt) => {
|
|
42
|
+
// For now just look at create and assign and status change
|
|
43
|
+
if (evt.type === 'created' || evt.type === 'assigned' || evt.type === 'status_changed') {
|
|
44
|
+
console.log('--------------------------------------------------------');
|
|
45
|
+
console.log('Event received');
|
|
46
|
+
console.log(`- Type: ${evt.type}`);
|
|
47
|
+
console.log(`- Task: ${evt.task.name}`);
|
|
48
|
+
console.log(`- Actor: ${evt.actorId}`);
|
|
49
|
+
console.log(`- Task status: ${evt.task.status}`);
|
|
50
|
+
console.log(`- Task assignee: ${evt.task.assigneeActor?.id}`);
|
|
51
|
+
const task = evt.task;
|
|
52
|
+
if (task.assigneeActor?.id === evt.actorId) {
|
|
53
|
+
console.log(`- Update caused by assignee. Ignoring as this is a self event. ❌`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.handleTask(task);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
async handleTask(task) {
|
|
60
|
+
// Get the agent
|
|
61
|
+
const actor = task.assigneeActor;
|
|
62
|
+
if (!actor?.slug) {
|
|
63
|
+
console.log(`- Task ${task.id} not assigned or missing actor slug. Skipping. ❌`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const agent = await this.client.getAgent(actor.slug);
|
|
67
|
+
if (!agent) {
|
|
68
|
+
console.log(`- Agent @${actor.slug} not found. Skipping. ❌`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
console.log(`- Agent: @${agent.slug}`);
|
|
72
|
+
if (agent.slug != AGENT_SLUG) {
|
|
73
|
+
console.log(`- We only react to @${AGENT_SLUG}. Skipping. ❌`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Do we have runners for this agent?
|
|
77
|
+
if (agent.type !== "claude" && agent.type !== "opencode" && agent.type !== "adk" && agent.type !== "githubcopilot") {
|
|
78
|
+
console.log(`- Agent @${actor.slug} of type "${agent.type}" not supported. Skipping. ❌`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Does the agent respond to this status?
|
|
82
|
+
if (!agent.statusTriggers.includes(task.status)) {
|
|
83
|
+
console.log(`- Agent @${agent.slug} doesn't react to status '${task.status}'. Skip. ❌`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
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
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log(`- Project not found for slug: ${projectSlug}`);
|
|
105
|
+
}
|
|
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 ❌`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
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);
|
|
127
|
+
}
|
|
128
|
+
else if (agent.type === 'opencode') {
|
|
129
|
+
runner = new OpencodeAgentRunner(modelConfig);
|
|
130
|
+
}
|
|
131
|
+
else if (agent.type === 'adk') {
|
|
132
|
+
runner = new ADKAgentRunner(modelConfig);
|
|
133
|
+
}
|
|
134
|
+
else if (agent.type === 'githubcopilot') {
|
|
135
|
+
runner = new GitHubCopilotAgentRunner(modelConfig);
|
|
136
|
+
}
|
|
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. ❌`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
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);
|
|
162
|
+
}
|
|
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
|
+
}
|
|
171
|
+
});
|
|
172
|
+
console.log(results);
|
|
173
|
+
// Force a comment
|
|
174
|
+
this.client.addComment(task.id, `Finished.\n\n${results.result}`);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
console.error(`Error running task`);
|
|
178
|
+
console.error(error);
|
|
179
|
+
// Force a comment
|
|
180
|
+
this.client.addComment(task.id, `❌ Something went wrong ❌\n\n${error}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { TaskWirePayload, CommentWirePayload } from "@taico/events";
|
|
2
|
+
export type TaskEvent = {
|
|
3
|
+
type: "created";
|
|
4
|
+
actorId: string;
|
|
5
|
+
task: TaskWirePayload;
|
|
6
|
+
} | {
|
|
7
|
+
type: "updated";
|
|
8
|
+
actorId: string;
|
|
9
|
+
task: TaskWirePayload;
|
|
10
|
+
} | {
|
|
11
|
+
type: "deleted";
|
|
12
|
+
actorId: string;
|
|
13
|
+
taskId: string;
|
|
14
|
+
} | {
|
|
15
|
+
type: "assigned";
|
|
16
|
+
actorId: string;
|
|
17
|
+
task: TaskWirePayload;
|
|
18
|
+
} | {
|
|
19
|
+
type: "status_changed";
|
|
20
|
+
actorId: string;
|
|
21
|
+
task: TaskWirePayload;
|
|
22
|
+
} | {
|
|
23
|
+
type: "commented";
|
|
24
|
+
actorId: string;
|
|
25
|
+
comment: CommentWirePayload;
|
|
26
|
+
};
|
|
27
|
+
export type TaskActivity = {
|
|
28
|
+
taskId: string;
|
|
29
|
+
kind?: string;
|
|
30
|
+
message: string;
|
|
31
|
+
ts: number;
|
|
32
|
+
};
|
|
33
|
+
export interface TasksTransport {
|
|
34
|
+
start(): Promise<void>;
|
|
35
|
+
stop(): Promise<void>;
|
|
36
|
+
onTaskEvent(handler: (evt: TaskEvent) => void): void;
|
|
37
|
+
publishActivity(activity: TaskActivity): void;
|
|
38
|
+
}
|
|
39
|
+
export declare class SocketIOTasksTransport implements TasksTransport {
|
|
40
|
+
private readonly baseUrl;
|
|
41
|
+
private readonly accessToken;
|
|
42
|
+
private readonly options?;
|
|
43
|
+
private socket?;
|
|
44
|
+
private handlers;
|
|
45
|
+
private started;
|
|
46
|
+
private reconnectAttempts;
|
|
47
|
+
private subscribeAckTimer?;
|
|
48
|
+
private awaitingSubscribeAck;
|
|
49
|
+
constructor(baseUrl: string, accessToken: string, options?: {
|
|
50
|
+
namespace?: string;
|
|
51
|
+
transports?: Array<"websocket" | "polling">;
|
|
52
|
+
autoSubscribe?: boolean;
|
|
53
|
+
debug?: boolean;
|
|
54
|
+
reconnection?: boolean;
|
|
55
|
+
reconnectionAttempts?: number;
|
|
56
|
+
reconnectionDelay?: number;
|
|
57
|
+
reconnectionDelayMax?: number;
|
|
58
|
+
randomizationFactor?: number;
|
|
59
|
+
subscribeAckTimeout?: number;
|
|
60
|
+
} | undefined);
|
|
61
|
+
onTaskEvent(handler: (evt: TaskEvent) => void): void;
|
|
62
|
+
publishActivity(activity: TaskActivity): void;
|
|
63
|
+
start(): Promise<void>;
|
|
64
|
+
stop(): Promise<void>;
|
|
65
|
+
private requestSubscribeAck;
|
|
66
|
+
private clearSubscribeAckTimer;
|
|
67
|
+
private emit;
|
|
68
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Socket.IO transport for task events + activity publishing.
|
|
3
|
+
|
|
4
|
+
Goal: keep Socket.IO specifics here, expose a clean TasksTransport interface.
|
|
5
|
+
*/
|
|
6
|
+
import { io } from "socket.io-client";
|
|
7
|
+
import { TaskWireEvents, } from "@taico/events";
|
|
8
|
+
export class SocketIOTasksTransport {
|
|
9
|
+
baseUrl;
|
|
10
|
+
accessToken;
|
|
11
|
+
options;
|
|
12
|
+
socket;
|
|
13
|
+
handlers = [];
|
|
14
|
+
started = false;
|
|
15
|
+
reconnectAttempts = 0;
|
|
16
|
+
subscribeAckTimer;
|
|
17
|
+
awaitingSubscribeAck = false;
|
|
18
|
+
constructor(baseUrl, accessToken, options) {
|
|
19
|
+
this.baseUrl = baseUrl;
|
|
20
|
+
this.accessToken = accessToken;
|
|
21
|
+
this.options = options;
|
|
22
|
+
}
|
|
23
|
+
onTaskEvent(handler) {
|
|
24
|
+
this.handlers.push(handler);
|
|
25
|
+
}
|
|
26
|
+
publishActivity(activity) {
|
|
27
|
+
if (!this.socket || !this.socket.connected) {
|
|
28
|
+
if (this.options?.debug) {
|
|
29
|
+
console.warn("[tasks] publishActivity called while disconnected", activity);
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
this.socket.emit(TaskWireEvents.TASK_ACTIVITY_POST, activity, (ack) => this.options?.debug && console.log(`[${TaskWireEvents.TASK_ACTIVITY_POST}] ack:`, ack));
|
|
34
|
+
}
|
|
35
|
+
async start() {
|
|
36
|
+
if (this.started)
|
|
37
|
+
return;
|
|
38
|
+
this.started = true;
|
|
39
|
+
const namespace = this.options?.namespace ?? "/tasks";
|
|
40
|
+
const transports = this.options?.transports ?? ["websocket"];
|
|
41
|
+
const autoSubscribe = this.options?.autoSubscribe ?? true;
|
|
42
|
+
const reconnection = this.options?.reconnection ?? true;
|
|
43
|
+
const reconnectionAttempts = this.options?.reconnectionAttempts ?? Infinity;
|
|
44
|
+
const reconnectionDelay = this.options?.reconnectionDelay ?? 1000;
|
|
45
|
+
const reconnectionDelayMax = this.options?.reconnectionDelayMax ?? 5000;
|
|
46
|
+
const randomizationFactor = this.options?.randomizationFactor ?? 0.5;
|
|
47
|
+
const url = `${this.baseUrl}${namespace}`;
|
|
48
|
+
if (this.options?.debug)
|
|
49
|
+
console.log(`[tasks] connecting to ${url}`);
|
|
50
|
+
this.socket = io(url, {
|
|
51
|
+
transports,
|
|
52
|
+
auth: { token: this.accessToken },
|
|
53
|
+
reconnection,
|
|
54
|
+
reconnectionAttempts,
|
|
55
|
+
reconnectionDelay,
|
|
56
|
+
reconnectionDelayMax,
|
|
57
|
+
randomizationFactor,
|
|
58
|
+
});
|
|
59
|
+
// ----- lifecycle -----
|
|
60
|
+
this.socket.on("connect", () => {
|
|
61
|
+
if (this.reconnectAttempts > 0) {
|
|
62
|
+
console.log(`[tasks] reconnected after ${this.reconnectAttempts} attempts:`, this.socket?.id);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
if (this.options?.debug)
|
|
66
|
+
console.log("[tasks] connected:", this.socket?.id);
|
|
67
|
+
}
|
|
68
|
+
// Reset reconnect attempts on successful connection
|
|
69
|
+
this.reconnectAttempts = 0;
|
|
70
|
+
if (autoSubscribe) {
|
|
71
|
+
this.requestSubscribeAck();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
this.socket.on("disconnect", (reason) => {
|
|
75
|
+
if (this.options?.debug)
|
|
76
|
+
console.log("[tasks] disconnected:", reason);
|
|
77
|
+
this.clearSubscribeAckTimer();
|
|
78
|
+
this.awaitingSubscribeAck = false;
|
|
79
|
+
// Only log reconnection info if we're going to try reconnecting
|
|
80
|
+
if (reconnection && reason !== "io client disconnect") {
|
|
81
|
+
console.log("[tasks] will attempt to reconnect...");
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
this.socket.on("connect_error", (err) => {
|
|
85
|
+
this.reconnectAttempts++;
|
|
86
|
+
console.error(`[tasks] connect_error (attempt ${this.reconnectAttempts}):`, err?.message ?? err);
|
|
87
|
+
if (this.options?.debug)
|
|
88
|
+
console.error(err);
|
|
89
|
+
this.clearSubscribeAckTimer();
|
|
90
|
+
this.awaitingSubscribeAck = false;
|
|
91
|
+
});
|
|
92
|
+
this.socket.on("reconnect_attempt", (attemptNumber) => {
|
|
93
|
+
if (this.options?.debug)
|
|
94
|
+
console.log(`[tasks] reconnect_attempt ${attemptNumber}`);
|
|
95
|
+
});
|
|
96
|
+
this.socket.on("reconnect_failed", () => {
|
|
97
|
+
console.error("[tasks] reconnect_failed: max reconnection attempts reached");
|
|
98
|
+
});
|
|
99
|
+
// ----- events we care about -----
|
|
100
|
+
// Using shared wire event types from @taico/events
|
|
101
|
+
// These types ensure consistency between backend emission and frontend/agent reception
|
|
102
|
+
this.socket.on(TaskWireEvents.TASK_CREATED, (event) => this.emit({ type: "created", actorId: event.actor.id, task: event.payload }));
|
|
103
|
+
this.socket.on(TaskWireEvents.TASK_ASSIGNED, (event) => this.emit({ type: "assigned", actorId: event.actor.id, task: event.payload }));
|
|
104
|
+
this.socket.on(TaskWireEvents.TASK_STATUS_CHANGED, (event) => this.emit({ type: "status_changed", actorId: event.actor.id, task: event.payload }));
|
|
105
|
+
this.socket.on(TaskWireEvents.TASK_UPDATED, (event) => this.emit({ type: "updated", actorId: event.actor.id, task: event.payload }));
|
|
106
|
+
this.socket.on(TaskWireEvents.TASK_DELETED, (event) => this.emit({ type: "deleted", actorId: event.actor.id, taskId: event.payload.taskId }));
|
|
107
|
+
this.socket.on(TaskWireEvents.TASK_COMMENTED, (event) => this.emit({ type: "commented", actorId: event.actor.id, comment: event.payload }));
|
|
108
|
+
// Wait until first connect or error so callers can await start()
|
|
109
|
+
await new Promise((resolve, reject) => {
|
|
110
|
+
const onConnect = () => {
|
|
111
|
+
cleanup();
|
|
112
|
+
resolve();
|
|
113
|
+
};
|
|
114
|
+
const onErr = (e) => {
|
|
115
|
+
cleanup();
|
|
116
|
+
// don't permanently kill the transport; just reject start()
|
|
117
|
+
reject(e);
|
|
118
|
+
};
|
|
119
|
+
const cleanup = () => {
|
|
120
|
+
this.socket?.off("connect", onConnect);
|
|
121
|
+
this.socket?.off("connect_error", onErr);
|
|
122
|
+
};
|
|
123
|
+
this.socket?.once("connect", onConnect);
|
|
124
|
+
this.socket?.once("connect_error", onErr);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async stop() {
|
|
128
|
+
this.started = false;
|
|
129
|
+
if (!this.socket)
|
|
130
|
+
return;
|
|
131
|
+
const s = this.socket;
|
|
132
|
+
this.socket = undefined;
|
|
133
|
+
// remove listeners to avoid leaks if restarted
|
|
134
|
+
s.removeAllListeners();
|
|
135
|
+
s.disconnect();
|
|
136
|
+
this.clearSubscribeAckTimer();
|
|
137
|
+
this.awaitingSubscribeAck = false;
|
|
138
|
+
}
|
|
139
|
+
requestSubscribeAck() {
|
|
140
|
+
if (!this.socket)
|
|
141
|
+
return;
|
|
142
|
+
const timeoutMs = this.options?.subscribeAckTimeout ?? 5000;
|
|
143
|
+
const socket = this.socket;
|
|
144
|
+
this.clearSubscribeAckTimer();
|
|
145
|
+
this.awaitingSubscribeAck = true;
|
|
146
|
+
socket.emit("tasks.subscribe", {}, (ack) => {
|
|
147
|
+
if (this.socket !== socket)
|
|
148
|
+
return;
|
|
149
|
+
this.awaitingSubscribeAck = false;
|
|
150
|
+
this.clearSubscribeAckTimer();
|
|
151
|
+
if (this.options?.debug)
|
|
152
|
+
console.log("[tasks] subscribed:", ack);
|
|
153
|
+
});
|
|
154
|
+
this.subscribeAckTimer = setTimeout(() => {
|
|
155
|
+
if (!this.socket || this.socket !== socket)
|
|
156
|
+
return;
|
|
157
|
+
if (!this.awaitingSubscribeAck)
|
|
158
|
+
return;
|
|
159
|
+
this.awaitingSubscribeAck = false;
|
|
160
|
+
if (this.options?.debug) {
|
|
161
|
+
console.warn("[tasks] subscribe ack timeout; forcing reconnect");
|
|
162
|
+
}
|
|
163
|
+
this.socket.disconnect();
|
|
164
|
+
this.socket.connect();
|
|
165
|
+
}, timeoutMs);
|
|
166
|
+
}
|
|
167
|
+
clearSubscribeAckTimer() {
|
|
168
|
+
if (this.subscribeAckTimer) {
|
|
169
|
+
clearTimeout(this.subscribeAckTimer);
|
|
170
|
+
this.subscribeAckTimer = undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
emit(evt) {
|
|
174
|
+
for (const h of this.handlers) {
|
|
175
|
+
try {
|
|
176
|
+
h(evt);
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
console.error("[tasks] handler error:", err);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
package/dist/Taico.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type AgentResponseDto, type AgentRunResponseDto, type ProjectResponseDto } from "@taico/client";
|
|
2
|
+
export declare class Taico {
|
|
3
|
+
constructor(baseUrl: string, accessToken: string);
|
|
4
|
+
getAgent(agentSlug: string): Promise<AgentResponseDto | null>;
|
|
5
|
+
getAgentPrompt(agentSlug: string): Promise<string>;
|
|
6
|
+
getAgentStatusTriggers(agentSlug: string): Promise<string[]>;
|
|
7
|
+
addComment(taskId: string, comment: string): Promise<void>;
|
|
8
|
+
getProjectBySlug(slug: string): Promise<ProjectResponseDto | null>;
|
|
9
|
+
startRun(taskId: string): Promise<AgentRunResponseDto | null>;
|
|
10
|
+
}
|
package/dist/Taico.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Taico.ts - API client wrapper using generated services
|
|
2
|
+
import { OpenAPI, ApiError, AgentService, TaskService, MetaProjectsService, AgentRunService, } from "@taico/client";
|
|
3
|
+
function isApiError(error) {
|
|
4
|
+
return error instanceof ApiError;
|
|
5
|
+
}
|
|
6
|
+
export class Taico {
|
|
7
|
+
constructor(baseUrl, accessToken) {
|
|
8
|
+
// Configure the generated client
|
|
9
|
+
OpenAPI.BASE = baseUrl;
|
|
10
|
+
OpenAPI.TOKEN = accessToken;
|
|
11
|
+
}
|
|
12
|
+
async getAgent(agentSlug) {
|
|
13
|
+
try {
|
|
14
|
+
return await AgentService.agentsControllerGetAgentBySlug(agentSlug);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (isApiError(error) && error.status === 404) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async getAgentPrompt(agentSlug) {
|
|
24
|
+
const agent = await this.getAgent(agentSlug);
|
|
25
|
+
const prompt = agent?.systemPrompt;
|
|
26
|
+
if (typeof prompt !== "string" || prompt.trim() === "") {
|
|
27
|
+
throw new Error(`[Taico] Agent @${agentSlug} has no systemPrompt.`);
|
|
28
|
+
}
|
|
29
|
+
return prompt;
|
|
30
|
+
}
|
|
31
|
+
async getAgentStatusTriggers(agentSlug) {
|
|
32
|
+
const agent = await this.getAgent(agentSlug);
|
|
33
|
+
const triggers = agent?.statusTriggers;
|
|
34
|
+
if (!Array.isArray(triggers) || triggers.some((t) => typeof t !== "string")) {
|
|
35
|
+
throw new Error(`[Taico] Agent @${agentSlug} has invalid statusTriggers (expected string[]).`);
|
|
36
|
+
}
|
|
37
|
+
return triggers;
|
|
38
|
+
}
|
|
39
|
+
async addComment(taskId, comment) {
|
|
40
|
+
try {
|
|
41
|
+
await TaskService.tasksControllerAddComment(taskId, { content: comment });
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error(`Failed to post comment to task ${taskId}:`, error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async getProjectBySlug(slug) {
|
|
48
|
+
try {
|
|
49
|
+
return await MetaProjectsService.projectsControllerGetProjectBySlug(slug);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (isApiError(error) && error.status === 404) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async startRun(taskId) {
|
|
59
|
+
try {
|
|
60
|
+
return await AgentRunService.agentRunsControllerCreateAgentRun({
|
|
61
|
+
parentTaskId: taskId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error(`Failed to start run for task ${taskId}:`, error);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/dev.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'dotenv/config';
|
package/dist/dev.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { OpencodeAgentRunner } from "./runners/OpenCodeAgentRunner.js";
|
|
3
|
+
async function dev() {
|
|
4
|
+
const modelConfig = {
|
|
5
|
+
providerId: 'spark-qwen3-coder-next-fp8',
|
|
6
|
+
modelId: 'Qwen/Qwen3-Coder-Next-FP8',
|
|
7
|
+
};
|
|
8
|
+
const taskId = '123';
|
|
9
|
+
const runId = 'xzy';
|
|
10
|
+
const runner = new OpencodeAgentRunner(modelConfig);
|
|
11
|
+
const ctx = {
|
|
12
|
+
taskId,
|
|
13
|
+
prompt: "Run pwd and tell me what you see",
|
|
14
|
+
cwd: `/Users/franciscogalarza/github/ai-monorepo/apps/worker/temp/asds`,
|
|
15
|
+
runId,
|
|
16
|
+
};
|
|
17
|
+
const callbacks = {
|
|
18
|
+
onEvent: (message) => {
|
|
19
|
+
console.log(message);
|
|
20
|
+
},
|
|
21
|
+
onError: (err) => {
|
|
22
|
+
console.error(err.message);
|
|
23
|
+
console.error(err.rawMessage);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
await runner.run(ctx, callbacks);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
dev();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export class ADKMessageFormatter {
|
|
2
|
+
format(message) {
|
|
3
|
+
const content = message.content;
|
|
4
|
+
if (!content) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
const parts = content.parts;
|
|
8
|
+
if (!parts) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const partMessages = parts.map(part => {
|
|
12
|
+
// Think
|
|
13
|
+
if (part.thought) {
|
|
14
|
+
return `💬 Thinking...`;
|
|
15
|
+
}
|
|
16
|
+
// Tool call
|
|
17
|
+
if (part.functionCall) {
|
|
18
|
+
return `🔧 Tool call: ${part.functionCall.name}`;
|
|
19
|
+
}
|
|
20
|
+
// Tool response
|
|
21
|
+
if (part.functionResponse) {
|
|
22
|
+
return `🔧 Tool response: ${part.functionResponse.name}`;
|
|
23
|
+
}
|
|
24
|
+
// Text
|
|
25
|
+
if (part.text) {
|
|
26
|
+
return `💬 Assistant: ${part.text}`;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}).filter(p => p != null);
|
|
30
|
+
return partMessages;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
export declare class ClaudeMessageFormatter {
|
|
3
|
+
format(message: SDKMessage): string | null;
|
|
4
|
+
private formatAssistant;
|
|
5
|
+
private formatUser;
|
|
6
|
+
private formatResult;
|
|
7
|
+
private formatSystem;
|
|
8
|
+
private formatStreamEvent;
|
|
9
|
+
private formatToolProgress;
|
|
10
|
+
private formatAuthStatus;
|
|
11
|
+
}
|