@ycodium-ai/plugin-teams 0.1.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 +66 -0
- package/dist/index.js +454 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @ycodium-ai/plugin-teams
|
|
2
|
+
|
|
3
|
+
Run a team of agents on one Ycodium machine: a task board, a roster of roles, a mailbox
|
|
4
|
+
between them, and the rule for who gets sent work next.
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
A **run** is a board of tasks and a roster of roles. Each role is an agent session — it can
|
|
9
|
+
be a different model or provider from the others. The plugin asks the host for ready tasks
|
|
10
|
+
and requests dispatch when tasks or member availability change, and when the plugin loads.
|
|
11
|
+
|
|
12
|
+
The host checks the current task revision, role, member availability, and turn budget. It
|
|
13
|
+
prepares the role brief, task details, acceptance findings, and unread messages for that run.
|
|
14
|
+
Claiming the task, queuing its turn, and marking those messages delivered commit together.
|
|
15
|
+
The board follows persisted events; a queued request is not proof that the provider started.
|
|
16
|
+
|
|
17
|
+
What it enforces:
|
|
18
|
+
|
|
19
|
+
- **One dispatcher at a time.** A task is offered to exactly one role, and not again until
|
|
20
|
+
that attempt settles.
|
|
21
|
+
- **Admission.** A role is only sent work if the run is active, the role can be woken, it is
|
|
22
|
+
not already busy, and the run still has budget.
|
|
23
|
+
- **Acceptance.** A delivered task waits for whoever was named to sign it off — another role,
|
|
24
|
+
or a person on the board.
|
|
25
|
+
- **Budget.** A run has a ceiling on how many turns it may spend, checked before every
|
|
26
|
+
dispatch.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
A Ycodium server with the `teams.dispatch` capability. Older servers cannot activate this
|
|
31
|
+
version of the plugin.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
ycodium plugin add @ycodium-ai/plugin-teams
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The plugin requests the **Assign ready team tasks to their members** permission. It can request dispatch of
|
|
40
|
+
existing ready tasks; the host chooses the assigned member and prepares the turn. This
|
|
41
|
+
permission does not allow arbitrary turns on unrelated threads.
|
|
42
|
+
|
|
43
|
+
## Read this before installing
|
|
44
|
+
|
|
45
|
+
**This plugin takes over dispatch.** It ships enabled with Ycodium. Disabling it stops new
|
|
46
|
+
task dispatch; work already in progress can still finish. Enabling it scans existing ready
|
|
47
|
+
tasks. The host executes its requests and does not run a second task scheduler.
|
|
48
|
+
|
|
49
|
+
## Upgrading
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
ycodium plugin add @ycodium-ai/plugin-teams@<version>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Installing a newer version replaces the running one without restarting the server.
|
|
56
|
+
|
|
57
|
+
## Developing
|
|
58
|
+
|
|
59
|
+
Built from the Ycodium repository. The entry file is bundled into one ESM file with no
|
|
60
|
+
runtime imports — the host loads a plugin by absolute path, from a directory that is not
|
|
61
|
+
inside the plugin's own dependency tree, so a bare `import` at runtime would not resolve.
|
|
62
|
+
Types come from `ycodium-plugin-api` and are erased at build time.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
UNLICENSED.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// src/facts.ts
|
|
2
|
+
var TEAM_FACTS = {
|
|
3
|
+
/** 批次本身:在跑还是停了、预算多少、用了几轮。 */
|
|
4
|
+
run: "run",
|
|
5
|
+
/** 名册变了:谁在这个 run 里、什么角色、挂在哪条线程上。 */
|
|
6
|
+
member: "member",
|
|
7
|
+
/** 看板上一张卡的整张快照。卡的每一次变化都发一条,不发增量。 */
|
|
8
|
+
task: "task",
|
|
9
|
+
/**
|
|
10
|
+
* 受理了一次起轮尝试,以及它的结局。
|
|
11
|
+
*
|
|
12
|
+
* 这一条是 C4 争了六轮的那格。过去它只活在内存里,于是「排队等启动」和「启动失败」
|
|
13
|
+
* 只能从会话状态猜——猜错了就把还在排队的成员判成启动失败。当场记下来就没得猜。
|
|
14
|
+
*/
|
|
15
|
+
startAttempt: "start-attempt",
|
|
16
|
+
/** 谁收过说明书。按线程记,不按角色:接班线程是新上下文,前任那份没跟过去。 */
|
|
17
|
+
briefSent: "brief-sent",
|
|
18
|
+
/** 一张卡正在从谁交到谁手上。 */
|
|
19
|
+
handoff: "handoff",
|
|
20
|
+
/** 在等谁回话,等到什么时候。 */
|
|
21
|
+
wait: "wait",
|
|
22
|
+
/** 信箱:一条消息进了队。 */
|
|
23
|
+
messageQueued: "message-queued",
|
|
24
|
+
/**
|
|
25
|
+
* 信箱:给收件人排了一次叫醒。**这不等于他取走了。**
|
|
26
|
+
*
|
|
27
|
+
* 宿主那套把这两件事塞进同一个事件 `team.message-delivered`,靠 `deliveredAt` 还是
|
|
28
|
+
* `wakeQueuedAt` 有值来区分——真库里 343 条里有 71 条只是排了叫醒。一个事件装两件事,
|
|
29
|
+
* 读的人迟早分不清;这里拆开。
|
|
30
|
+
*/
|
|
31
|
+
messageWakeQueued: "message-wake-queued",
|
|
32
|
+
/** 信箱:收件人真的取走了。 */
|
|
33
|
+
messageDelivered: "message-delivered"
|
|
34
|
+
};
|
|
35
|
+
var TEAMS_PLUGIN_ID = "teams";
|
|
36
|
+
|
|
37
|
+
// src/dispatchAction.ts
|
|
38
|
+
var enqueueReadyTasks = async (ctx, query) => {
|
|
39
|
+
const tasks = await ctx.teams.listReadyTasks(query);
|
|
40
|
+
for (const task of tasks) {
|
|
41
|
+
await ctx.teams.dispatchTask(task);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/state.ts
|
|
46
|
+
var TEAM_STATE_VERSION = 2;
|
|
47
|
+
var TEAM_FOLDED_EVENTS = [
|
|
48
|
+
"plugin.event-appended",
|
|
49
|
+
"team.run-started",
|
|
50
|
+
"team.run-state-changed",
|
|
51
|
+
"team.member-thread-attached",
|
|
52
|
+
"team.task-created",
|
|
53
|
+
"team.task-changed",
|
|
54
|
+
"team.message-sent",
|
|
55
|
+
"team.message-delivered",
|
|
56
|
+
"thread.message-sent"
|
|
57
|
+
];
|
|
58
|
+
var emptyTeamState = () => ({
|
|
59
|
+
runs: {},
|
|
60
|
+
members: {},
|
|
61
|
+
tasks: {},
|
|
62
|
+
startAttempts: {},
|
|
63
|
+
briefed: [],
|
|
64
|
+
messages: {}
|
|
65
|
+
});
|
|
66
|
+
var ourFact = (event) => {
|
|
67
|
+
if (event.type !== "plugin.event-appended") return null;
|
|
68
|
+
const payload = event.payload;
|
|
69
|
+
if (payload === void 0 || payload.pluginId !== TEAMS_PLUGIN_ID) return null;
|
|
70
|
+
return payload;
|
|
71
|
+
};
|
|
72
|
+
var factName = (full) => full.startsWith(`${TEAMS_PLUGIN_ID}/`) ? full.slice(TEAMS_PLUGIN_ID.length + 1) : full;
|
|
73
|
+
var applyHostTeamEvent = (state, event) => {
|
|
74
|
+
switch (event.type) {
|
|
75
|
+
case "team.run-started": {
|
|
76
|
+
const p = event.payload;
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
runs: {
|
|
80
|
+
...state.runs,
|
|
81
|
+
// 没定过预算就记 null,和宿主投影表一样;默认值在用的时候才套(见 dispatch.ts)。
|
|
82
|
+
[p.runId]: {
|
|
83
|
+
runId: p.runId,
|
|
84
|
+
teamId: p.teamId ?? "",
|
|
85
|
+
state: "running",
|
|
86
|
+
turnBudget: p.turnBudget ?? null,
|
|
87
|
+
turnsUsed: 0
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
case "team.run-state-changed": {
|
|
93
|
+
const p = event.payload;
|
|
94
|
+
const current = state.runs[p.runId];
|
|
95
|
+
if (current === void 0) return state;
|
|
96
|
+
return {
|
|
97
|
+
...state,
|
|
98
|
+
runs: {
|
|
99
|
+
...state.runs,
|
|
100
|
+
[p.runId]: {
|
|
101
|
+
...current,
|
|
102
|
+
state: p.state,
|
|
103
|
+
...p.turnBudget === void 0 ? {} : { turnBudget: p.turnBudget },
|
|
104
|
+
// 事件带的是**增量**(每次那一笔),不是全量清单——所以要累加,不能替换。
|
|
105
|
+
// 第一版写成替换,拿真库一对全都变成 1,当场露馅。
|
|
106
|
+
turnsUsed: current.turnsUsed + (p.turnConsumption?.length ?? 0)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
case "team.member-thread-attached": {
|
|
112
|
+
const p = event.payload;
|
|
113
|
+
return {
|
|
114
|
+
...state,
|
|
115
|
+
members: {
|
|
116
|
+
...state.members,
|
|
117
|
+
[p.threadId]: { runId: p.runId, role: p.role, threadId: p.threadId, present: true }
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
case "team.task-created": {
|
|
122
|
+
const p = event.payload;
|
|
123
|
+
return {
|
|
124
|
+
...state,
|
|
125
|
+
tasks: {
|
|
126
|
+
...state.tasks,
|
|
127
|
+
[p.taskId]: {
|
|
128
|
+
runId: p.runId,
|
|
129
|
+
taskId: p.taskId,
|
|
130
|
+
title: p.title,
|
|
131
|
+
state: "pending",
|
|
132
|
+
assigneeRole: p.assigneeRole,
|
|
133
|
+
acceptorRole: p.acceptor ?? null,
|
|
134
|
+
acceptanceRounds: 0,
|
|
135
|
+
revision: 0,
|
|
136
|
+
threadId: null,
|
|
137
|
+
turnId: null,
|
|
138
|
+
startAttempt: null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
case "team.task-changed": {
|
|
144
|
+
const p = event.payload;
|
|
145
|
+
const current = state.tasks[p.taskId];
|
|
146
|
+
if (current === void 0) return state;
|
|
147
|
+
return {
|
|
148
|
+
...state,
|
|
149
|
+
tasks: {
|
|
150
|
+
...state.tasks,
|
|
151
|
+
[p.taskId]: {
|
|
152
|
+
...current,
|
|
153
|
+
...p.title === void 0 ? {} : { title: p.title },
|
|
154
|
+
...p.state === void 0 ? {} : { state: p.state },
|
|
155
|
+
...p.assigneeRole === void 0 ? {} : { assigneeRole: p.assigneeRole },
|
|
156
|
+
...p.acceptor === void 0 ? {} : { acceptorRole: p.acceptor },
|
|
157
|
+
...p.acceptanceRounds === void 0 ? {} : { acceptanceRounds: p.acceptanceRounds },
|
|
158
|
+
...p.revision === void 0 ? {} : { revision: p.revision },
|
|
159
|
+
...p.threadId === void 0 ? {} : { threadId: p.threadId },
|
|
160
|
+
...p.turnId === void 0 ? {} : { turnId: p.turnId },
|
|
161
|
+
...p.startAttempt === void 0 ? {} : { startAttempt: p.startAttempt }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
case "thread.message-sent": {
|
|
167
|
+
const p = event.payload;
|
|
168
|
+
if (p.carriesRoleBrief !== true || state.briefed.includes(p.threadId)) return state;
|
|
169
|
+
return { ...state, briefed: [...state.briefed, p.threadId] };
|
|
170
|
+
}
|
|
171
|
+
case "team.message-sent": {
|
|
172
|
+
const p = event.payload;
|
|
173
|
+
return {
|
|
174
|
+
...state,
|
|
175
|
+
messages: {
|
|
176
|
+
...state.messages,
|
|
177
|
+
[p.messageId]: {
|
|
178
|
+
runId: p.runId,
|
|
179
|
+
messageId: p.messageId,
|
|
180
|
+
fromThreadId: p.fromThreadId,
|
|
181
|
+
fromRole: p.fromRole,
|
|
182
|
+
toRole: p.toRole,
|
|
183
|
+
delivery: p.delivery,
|
|
184
|
+
wakeQueued: false,
|
|
185
|
+
delivered: false,
|
|
186
|
+
readTurnId: null
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
case "team.message-delivered": {
|
|
192
|
+
const p = event.payload;
|
|
193
|
+
const current = state.messages[p.messageId];
|
|
194
|
+
if (current === void 0) return state;
|
|
195
|
+
const reallyDelivered = p.deliveredAt !== void 0;
|
|
196
|
+
return {
|
|
197
|
+
...state,
|
|
198
|
+
messages: {
|
|
199
|
+
...state.messages,
|
|
200
|
+
[p.messageId]: {
|
|
201
|
+
...current,
|
|
202
|
+
wakeQueued: true,
|
|
203
|
+
delivered: reallyDelivered,
|
|
204
|
+
// 旧事件没有 readTurnId 这一格。读不出来就是 null,不猜一个出来。
|
|
205
|
+
readTurnId: reallyDelivered ? p.readTurnId ?? null : current.readTurnId
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
default:
|
|
211
|
+
return state;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var applyTeamFact = (state, event) => {
|
|
215
|
+
const payload = ourFact(event);
|
|
216
|
+
if (payload === null) return applyHostTeamEvent(state, event);
|
|
217
|
+
switch (factName(payload.name)) {
|
|
218
|
+
case TEAM_FACTS.run: {
|
|
219
|
+
const fact = payload.data;
|
|
220
|
+
return { ...state, runs: { ...state.runs, [fact.runId]: fact } };
|
|
221
|
+
}
|
|
222
|
+
case TEAM_FACTS.member: {
|
|
223
|
+
const fact = payload.data;
|
|
224
|
+
return { ...state, members: { ...state.members, [fact.threadId]: fact } };
|
|
225
|
+
}
|
|
226
|
+
case TEAM_FACTS.task: {
|
|
227
|
+
const fact = payload.data;
|
|
228
|
+
return { ...state, tasks: { ...state.tasks, [fact.taskId]: fact } };
|
|
229
|
+
}
|
|
230
|
+
case TEAM_FACTS.startAttempt: {
|
|
231
|
+
const fact = payload.data;
|
|
232
|
+
return { ...state, startAttempts: { ...state.startAttempts, [fact.threadId]: fact } };
|
|
233
|
+
}
|
|
234
|
+
case TEAM_FACTS.briefSent: {
|
|
235
|
+
const fact = payload.data;
|
|
236
|
+
if (state.briefed.includes(fact.threadId)) return state;
|
|
237
|
+
return { ...state, briefed: [...state.briefed, fact.threadId] };
|
|
238
|
+
}
|
|
239
|
+
case TEAM_FACTS.messageQueued: {
|
|
240
|
+
const fact = payload.data;
|
|
241
|
+
return {
|
|
242
|
+
...state,
|
|
243
|
+
messages: {
|
|
244
|
+
...state.messages,
|
|
245
|
+
[fact.messageId]: { ...fact, wakeQueued: false, delivered: false, readTurnId: null }
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
case TEAM_FACTS.messageWakeQueued: {
|
|
250
|
+
const fact = payload.data;
|
|
251
|
+
const current = state.messages[fact.messageId];
|
|
252
|
+
if (current === void 0) return state;
|
|
253
|
+
return {
|
|
254
|
+
...state,
|
|
255
|
+
messages: { ...state.messages, [fact.messageId]: { ...current, wakeQueued: true } }
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
case TEAM_FACTS.messageDelivered: {
|
|
259
|
+
const fact = payload.data;
|
|
260
|
+
const current = state.messages[fact.messageId];
|
|
261
|
+
if (current === void 0) return state;
|
|
262
|
+
return {
|
|
263
|
+
...state,
|
|
264
|
+
messages: {
|
|
265
|
+
...state.messages,
|
|
266
|
+
[fact.messageId]: { ...current, delivered: true, readTurnId: fact.readTurnId }
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// 还没折的事实(交接、等待)先原样跳过:折不了不等于该丢,它们还在日志里,
|
|
271
|
+
// 补上分支并把 TEAM_STATE_VERSION +1 就能全部重折出来。
|
|
272
|
+
default:
|
|
273
|
+
return state;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
var memberStartPhase = (state, threadId) => {
|
|
277
|
+
const tasks = Object.values(state.tasks).filter((task) => task.threadId === threadId);
|
|
278
|
+
const latest = tasks.flatMap((task) => task.startAttempt == null ? [] : [{ task, attempt: task.startAttempt }]).sort((left, right) => right.attempt.acceptedAt.localeCompare(left.attempt.acceptedAt))[0];
|
|
279
|
+
if (latest !== void 0) {
|
|
280
|
+
switch (latest.attempt.outcome) {
|
|
281
|
+
case "pending":
|
|
282
|
+
return { kind: "pending" };
|
|
283
|
+
case "in-round":
|
|
284
|
+
return { kind: "started", turnId: latest.task.turnId ?? null };
|
|
285
|
+
case "start-failed":
|
|
286
|
+
return { kind: "failed", reason: latest.attempt.failureReason ?? "\u672A\u8BB0\u5F55\u5931\u8D25\u539F\u56E0" };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (tasks.some((task) => task.startAttempt !== void 0)) return { kind: "never-attempted" };
|
|
290
|
+
const attempt = state.startAttempts[threadId];
|
|
291
|
+
if (attempt === void 0) return { kind: "never-attempted" };
|
|
292
|
+
return attempt.outcome;
|
|
293
|
+
};
|
|
294
|
+
var inboxFor = (state, role, runId) => Object.values(state.messages).filter(
|
|
295
|
+
(m) => m.toRole === role && !m.delivered && (runId === void 0 || m.runId === runId)
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// src/view.ts
|
|
299
|
+
var OPEN_STATES = /* @__PURE__ */ new Set(["pending", "ready", "in-progress", "awaiting-acceptance", "blocked"]);
|
|
300
|
+
var memberLine = (state, threadId) => {
|
|
301
|
+
const member = state.members[threadId];
|
|
302
|
+
if (member === void 0) return threadId;
|
|
303
|
+
const phase = memberStartPhase(state, threadId);
|
|
304
|
+
const phaseText = phase.kind === "never-attempted" ? "\u8D77\u8F6E\u65E0\u8BB0\u5F55" : phase.kind === "pending" ? "\u53D7\u7406\u4E86,\u8FD8\u6CA1\u8D77\u6765" : phase.kind === "started" ? phase.turnId === null ? "\u5DF2\u8D77\u8F6E" : `\u5DF2\u8D77\u8F6E (${phase.turnId})` : `\u8D77\u4E0D\u6765: ${phase.reason}`;
|
|
305
|
+
const inbox = inboxFor(state, member.role, member.runId).length;
|
|
306
|
+
const inboxText = inbox === 0 ? "" : ` \u4FE1\u7BB1 ${inbox} \u5C01\u672A\u53D6`;
|
|
307
|
+
return `${member.present ? "" : "[\u5DF2\u79BB\u573A] "}${member.role} ${phaseText}${inboxText}`;
|
|
308
|
+
};
|
|
309
|
+
var taskLines = (state, runId) => {
|
|
310
|
+
const open = Object.values(state.tasks).filter(
|
|
311
|
+
(task) => OPEN_STATES.has(task.state) && (runId === null || task.runId === runId)
|
|
312
|
+
);
|
|
313
|
+
if (open.length === 0) return ["\uFF08\u6CA1\u6709\u5F00\u7740\u7684\u5361\uFF09"];
|
|
314
|
+
return open.slice().sort((a, b) => a.state.localeCompare(b.state) || a.title.localeCompare(b.title)).map((task) => {
|
|
315
|
+
const who = task.assigneeRole ?? "\u6CA1\u6D3E\u4EBA";
|
|
316
|
+
const rounds = task.acceptanceRounds === 0 ? "" : ` \u5DF2\u9A73\u56DE ${task.acceptanceRounds} \u8F6E`;
|
|
317
|
+
const acceptor = task.state === "awaiting-acceptance" && task.acceptorRole !== null ? ` \u7B49 ${task.acceptorRole} \u9A8C\u6536` : "";
|
|
318
|
+
return `[${task.state}] ${task.title} \u2014\u2014 ${who}${acceptor}${rounds}`;
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
var renderBoard = (state, runId) => {
|
|
322
|
+
const members = Object.keys(state.members).filter((threadId) => runId === null || state.members[threadId]?.runId === runId).sort().map((threadId) => ` ${memberLine(state, threadId)}`);
|
|
323
|
+
const lines = [
|
|
324
|
+
"\u6210\u5458",
|
|
325
|
+
...members.length === 0 ? [" \uFF08\u6CA1\u6709\u6210\u5458\uFF09"] : members,
|
|
326
|
+
"",
|
|
327
|
+
"\u5F00\u7740\u7684\u5361",
|
|
328
|
+
...taskLines(state, runId).map((line) => ` ${line}`)
|
|
329
|
+
];
|
|
330
|
+
return lines.join("\n");
|
|
331
|
+
};
|
|
332
|
+
var renderInbox = (state, role, runId) => {
|
|
333
|
+
const entries = inboxFor(state, role, runId);
|
|
334
|
+
if (entries.length === 0) return `${role} \u7684\u4FE1\u7BB1\u662F\u7A7A\u7684\u3002`;
|
|
335
|
+
return [
|
|
336
|
+
`${role} \u7684\u4FE1\u7BB1,${entries.length} \u5C01\u672A\u53D6:`,
|
|
337
|
+
...entries.map((entry) => {
|
|
338
|
+
const from = entry.fromRole ?? `${entry.fromThreadId.slice(0, 8)}\u2026`;
|
|
339
|
+
const wake = entry.wakeQueued ? "\uFF08\u5DF2\u6392\u53EB\u9192\uFF09" : "\uFF08\u53EA\u8FDB\u4FE1\u7BB1\uFF09";
|
|
340
|
+
return ` \u6765\u81EA ${from} ${entry.delivery}${wake}`;
|
|
341
|
+
})
|
|
342
|
+
].join("\n");
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/index.ts
|
|
346
|
+
var index_default = {
|
|
347
|
+
id: "teams",
|
|
348
|
+
apiVersion: 1,
|
|
349
|
+
activate: async (ctx) => {
|
|
350
|
+
const board = ctx.derivedState({
|
|
351
|
+
key: "board",
|
|
352
|
+
version: TEAM_STATE_VERSION,
|
|
353
|
+
events: [...TEAM_FOLDED_EVENTS],
|
|
354
|
+
init: emptyTeamState,
|
|
355
|
+
apply: applyTeamFact
|
|
356
|
+
});
|
|
357
|
+
ctx.reactor({
|
|
358
|
+
name: "teams:dispatch",
|
|
359
|
+
events: [
|
|
360
|
+
"team.task-changed",
|
|
361
|
+
"team.run-state-changed",
|
|
362
|
+
"team.member-thread-attached",
|
|
363
|
+
"team.updated",
|
|
364
|
+
"thread.session-set",
|
|
365
|
+
"thread.turn-intent-transitioned",
|
|
366
|
+
"thread.turn-queue-paused"
|
|
367
|
+
],
|
|
368
|
+
handle: async (event) => {
|
|
369
|
+
const payload = event.payload;
|
|
370
|
+
switch (event.type) {
|
|
371
|
+
case "team.task-changed":
|
|
372
|
+
if ((payload.state === void 0 || payload.state === "ready" || payload.state === "done" || payload.state === "failed" || payload.state === "cancelled" || payload.state === "awaiting-acceptance") && payload.runId !== void 0) {
|
|
373
|
+
await enqueueReadyTasks(ctx, { runId: payload.runId });
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
case "team.run-state-changed":
|
|
377
|
+
if (payload.state !== "running" || payload.runId === void 0) return;
|
|
378
|
+
if (payload.turnConsumption !== void 0 || payload.attention !== void 0 || payload.wakeCursors !== void 0 || payload.briefedRoles !== void 0 || payload.briefedThreads !== void 0)
|
|
379
|
+
return;
|
|
380
|
+
await enqueueReadyTasks(ctx, { runId: payload.runId });
|
|
381
|
+
return;
|
|
382
|
+
case "team.member-thread-attached":
|
|
383
|
+
if (payload.runId !== void 0) await enqueueReadyTasks(ctx, { runId: payload.runId });
|
|
384
|
+
return;
|
|
385
|
+
case "team.updated":
|
|
386
|
+
if (payload.teamId !== void 0)
|
|
387
|
+
await enqueueReadyTasks(ctx, { teamId: payload.teamId });
|
|
388
|
+
return;
|
|
389
|
+
case "thread.session-set": {
|
|
390
|
+
const status = payload.session?.status;
|
|
391
|
+
if (payload.threadId !== void 0 && status !== void 0 && status !== "starting" && status !== "running") {
|
|
392
|
+
await enqueueReadyTasks(ctx, { threadId: payload.threadId });
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
case "thread.turn-intent-transitioned":
|
|
397
|
+
if (payload.threadId !== void 0 && (payload.status === "completed" || payload.status === "interrupted" || payload.status === "failed" || payload.status === "cancelled" || payload.status === "abandoned")) {
|
|
398
|
+
await enqueueReadyTasks(ctx, { threadId: payload.threadId });
|
|
399
|
+
}
|
|
400
|
+
return;
|
|
401
|
+
case "thread.turn-queue-paused":
|
|
402
|
+
if (payload.paused === false && payload.threadId !== void 0) {
|
|
403
|
+
await enqueueReadyTasks(ctx, { threadId: payload.threadId });
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
ctx.toolkit({
|
|
410
|
+
name: "teams",
|
|
411
|
+
tools: [
|
|
412
|
+
{
|
|
413
|
+
name: "board",
|
|
414
|
+
description: "Read the team board: who is on the roster, what each member is doing, and every open card.",
|
|
415
|
+
inputSchema: {
|
|
416
|
+
type: "object",
|
|
417
|
+
properties: {
|
|
418
|
+
runId: { type: "string", description: "Only this run. Omit for every run." }
|
|
419
|
+
},
|
|
420
|
+
additionalProperties: false
|
|
421
|
+
},
|
|
422
|
+
handle: async (input) => ({
|
|
423
|
+
text: renderBoard(await board.get(), input.runId ?? null)
|
|
424
|
+
})
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: "inbox",
|
|
428
|
+
description: "Read one role's unread team messages.",
|
|
429
|
+
inputSchema: {
|
|
430
|
+
type: "object",
|
|
431
|
+
properties: {
|
|
432
|
+
role: { type: "string" },
|
|
433
|
+
runId: { type: "string", description: "Only this run. Omit for every run." }
|
|
434
|
+
},
|
|
435
|
+
required: ["role"],
|
|
436
|
+
additionalProperties: false
|
|
437
|
+
},
|
|
438
|
+
handle: async (input) => ({
|
|
439
|
+
text: renderInbox(
|
|
440
|
+
await board.get(),
|
|
441
|
+
input.role,
|
|
442
|
+
input.runId
|
|
443
|
+
)
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
});
|
|
448
|
+
await enqueueReadyTasks(ctx);
|
|
449
|
+
ctx.log.info("teams plugin active", { facts: Object.keys(TEAM_FACTS).length });
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
export {
|
|
453
|
+
index_default as default
|
|
454
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ycodium-ai/plugin-teams",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Teams for Ycodium: the board, the roster, the mailbox, and who gets sent work.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"teams",
|
|
7
|
+
"ycodium",
|
|
8
|
+
"ycodium-plugin"
|
|
9
|
+
],
|
|
10
|
+
"license": "UNLICENSED",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@effect/vitest": "4.0.0-beta.103",
|
|
20
|
+
"effect": "4.0.0-beta.103",
|
|
21
|
+
"esbuild": "0.28.1",
|
|
22
|
+
"vite-plus": "0.2.2",
|
|
23
|
+
"ycodium-plugin-api": "0.0.35"
|
|
24
|
+
},
|
|
25
|
+
"ycodium": {
|
|
26
|
+
"plugin": "./dist/index.js",
|
|
27
|
+
"displayName": "Teams",
|
|
28
|
+
"icon": "users",
|
|
29
|
+
"permissions": [
|
|
30
|
+
{
|
|
31
|
+
"kind": "teams.dispatch"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "node build.mjs",
|
|
37
|
+
"typecheck": "tsgo --noEmit",
|
|
38
|
+
"test": "vp test run"
|
|
39
|
+
}
|
|
40
|
+
}
|