@teamclaws/teamclaw 2026.3.25 → 2026.3.26-2
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 -0
- package/cli.mjs +519 -28
- package/index.ts +31 -16
- package/openclaw.plugin.json +3 -3
- package/package.json +22 -4
- package/src/config.ts +2 -2
- package/src/controller/controller-capacity.ts +23 -0
- package/src/controller/controller-service.ts +27 -1
- package/src/controller/controller-tools.ts +251 -7
- package/src/controller/http-server.ts +976 -38
- package/src/controller/orchestration-manifest.ts +105 -0
- package/src/controller/prompt-injector.ts +42 -7
- package/src/controller/worker-provisioning.ts +171 -13
- package/src/git-collaboration.ts +2 -0
- package/src/interaction-contracts.ts +459 -0
- package/src/task-executor.ts +482 -33
- package/src/types.ts +96 -0
- package/src/ui/app.js +313 -8
- package/src/ui/style.css +152 -0
- package/src/worker/http-handler.ts +15 -7
- package/src/worker/prompt-injector.ts +2 -0
- package/src/worker/skill-installer.ts +13 -0
- package/src/worker/tools.ts +172 -3
- package/src/worker/worker-service.ts +9 -6
package/src/ui/style.css
CHANGED
|
@@ -301,6 +301,19 @@ body {
|
|
|
301
301
|
margin-bottom: 6px;
|
|
302
302
|
line-height: 1.5;
|
|
303
303
|
}
|
|
304
|
+
.task-contract-summary {
|
|
305
|
+
margin-bottom: 8px;
|
|
306
|
+
padding: 8px 10px;
|
|
307
|
+
border-radius: 10px;
|
|
308
|
+
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
309
|
+
background: rgba(99, 102, 241, 0.08);
|
|
310
|
+
color: var(--text-secondary);
|
|
311
|
+
font-size: 12px;
|
|
312
|
+
line-height: 1.5;
|
|
313
|
+
}
|
|
314
|
+
.task-contract-summary strong {
|
|
315
|
+
color: var(--text-primary);
|
|
316
|
+
}
|
|
304
317
|
.skill-pills {
|
|
305
318
|
display: flex;
|
|
306
319
|
flex-wrap: wrap;
|
|
@@ -558,6 +571,135 @@ body {
|
|
|
558
571
|
font-size: 13px;
|
|
559
572
|
}
|
|
560
573
|
|
|
574
|
+
.contract-card {
|
|
575
|
+
background: var(--bg-tertiary);
|
|
576
|
+
border: 1px solid var(--border);
|
|
577
|
+
border-left: 3px solid var(--accent);
|
|
578
|
+
border-radius: 12px;
|
|
579
|
+
padding: 12px 14px;
|
|
580
|
+
display: flex;
|
|
581
|
+
flex-direction: column;
|
|
582
|
+
gap: 12px;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.contract-card-completed { border-left-color: var(--success); }
|
|
586
|
+
.contract-card-failed { border-left-color: var(--danger); }
|
|
587
|
+
.contract-card-blocked,
|
|
588
|
+
.contract-card-handoff { border-left-color: var(--warning); }
|
|
589
|
+
.contract-card-in_progress,
|
|
590
|
+
.contract-card-review,
|
|
591
|
+
.contract-card-update,
|
|
592
|
+
.contract-card-coordination,
|
|
593
|
+
.contract-card-question { border-left-color: var(--info); }
|
|
594
|
+
.contract-card-review-request,
|
|
595
|
+
.contract-card-review-response { border-left-color: #8b5cf6; }
|
|
596
|
+
.contract-card-announcement,
|
|
597
|
+
.contract-card-manifest { border-left-color: var(--accent-hover); }
|
|
598
|
+
|
|
599
|
+
.contract-card-header {
|
|
600
|
+
display: flex;
|
|
601
|
+
flex-direction: column;
|
|
602
|
+
gap: 10px;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.contract-card-kicker,
|
|
606
|
+
.contract-meta-label,
|
|
607
|
+
.contract-section-title,
|
|
608
|
+
.message-meta {
|
|
609
|
+
color: var(--text-muted);
|
|
610
|
+
font-size: 11px;
|
|
611
|
+
font-weight: 700;
|
|
612
|
+
letter-spacing: 0.06em;
|
|
613
|
+
text-transform: uppercase;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.contract-card-title {
|
|
617
|
+
font-size: 15px;
|
|
618
|
+
line-height: 1.45;
|
|
619
|
+
color: var(--text-primary);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.contract-meta-grid {
|
|
623
|
+
display: grid;
|
|
624
|
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
625
|
+
gap: 10px;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.contract-meta-item {
|
|
629
|
+
padding: 10px 12px;
|
|
630
|
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
631
|
+
border-radius: 10px;
|
|
632
|
+
background: rgba(255, 255, 255, 0.02);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.contract-meta-value {
|
|
636
|
+
margin-top: 4px;
|
|
637
|
+
font-size: 13px;
|
|
638
|
+
color: var(--text-primary);
|
|
639
|
+
line-height: 1.5;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.contract-section {
|
|
643
|
+
display: flex;
|
|
644
|
+
flex-direction: column;
|
|
645
|
+
gap: 8px;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.contract-markdown,
|
|
649
|
+
.timeline-raw-markdown,
|
|
650
|
+
.timeline-contract-note {
|
|
651
|
+
font-size: 13px;
|
|
652
|
+
line-height: 1.6;
|
|
653
|
+
color: var(--text-primary);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.contract-list {
|
|
657
|
+
padding-left: 18px;
|
|
658
|
+
color: var(--text-primary);
|
|
659
|
+
display: flex;
|
|
660
|
+
flex-direction: column;
|
|
661
|
+
gap: 6px;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.contract-list li {
|
|
665
|
+
line-height: 1.6;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.contract-chip-row {
|
|
669
|
+
display: flex;
|
|
670
|
+
flex-wrap: wrap;
|
|
671
|
+
gap: 8px;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.contract-chip {
|
|
675
|
+
display: inline-flex;
|
|
676
|
+
align-items: center;
|
|
677
|
+
min-height: 28px;
|
|
678
|
+
padding: 4px 10px;
|
|
679
|
+
border-radius: 999px;
|
|
680
|
+
border: 1px solid rgba(96, 165, 250, 0.22);
|
|
681
|
+
background: rgba(96, 165, 250, 0.1);
|
|
682
|
+
color: #c7d2fe;
|
|
683
|
+
font-size: 12px;
|
|
684
|
+
font-weight: 600;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.contract-reference-row .contract-chip {
|
|
688
|
+
border-color: rgba(52, 211, 153, 0.22);
|
|
689
|
+
background: rgba(52, 211, 153, 0.08);
|
|
690
|
+
color: #bbf7d0;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.contract-role-row .contract-chip {
|
|
694
|
+
border-color: rgba(168, 85, 247, 0.22);
|
|
695
|
+
background: rgba(168, 85, 247, 0.08);
|
|
696
|
+
color: #e9d5ff;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.contract-inline-note {
|
|
700
|
+
color: var(--text-secondary);
|
|
701
|
+
}
|
|
702
|
+
|
|
561
703
|
.messages-feed {
|
|
562
704
|
display: flex;
|
|
563
705
|
flex-direction: column;
|
|
@@ -592,6 +734,12 @@ body {
|
|
|
592
734
|
.message-type.review-request { background: #7c3aed; color: #fff; }
|
|
593
735
|
|
|
594
736
|
.message-content { font-size: 13px; color: var(--text-primary); line-height: 1.5; }
|
|
737
|
+
.message-meta {
|
|
738
|
+
margin-bottom: 8px;
|
|
739
|
+
}
|
|
740
|
+
.message-card .contract-card {
|
|
741
|
+
margin-bottom: 10px;
|
|
742
|
+
}
|
|
595
743
|
|
|
596
744
|
.create-task-form {
|
|
597
745
|
max-width: 500px;
|
|
@@ -1327,6 +1475,10 @@ body {
|
|
|
1327
1475
|
word-break: break-word;
|
|
1328
1476
|
overflow-wrap: anywhere;
|
|
1329
1477
|
}
|
|
1478
|
+
.timeline-entry-body .contract-card + .timeline-raw-markdown,
|
|
1479
|
+
.timeline-entry-body .timeline-contract-note + .timeline-contract-note {
|
|
1480
|
+
margin-top: 10px;
|
|
1481
|
+
}
|
|
1330
1482
|
|
|
1331
1483
|
.task-detail-output-stream {
|
|
1332
1484
|
display: flex;
|
|
@@ -62,6 +62,12 @@ export function createWorkerHttpHandler(
|
|
|
62
62
|
const recommendedSkills = Array.isArray(body.recommendedSkills)
|
|
63
63
|
? body.recommendedSkills.map((entry) => String(entry ?? ""))
|
|
64
64
|
: undefined;
|
|
65
|
+
const executionSessionKey = typeof body.executionSessionKey === "string"
|
|
66
|
+
? body.executionSessionKey
|
|
67
|
+
: undefined;
|
|
68
|
+
const executionIdempotencyKey = typeof body.executionIdempotencyKey === "string"
|
|
69
|
+
? body.executionIdempotencyKey
|
|
70
|
+
: undefined;
|
|
65
71
|
const repo = body.repo && typeof body.repo === "object"
|
|
66
72
|
? body.repo as TaskAssignmentPayload["repo"]
|
|
67
73
|
: undefined;
|
|
@@ -74,13 +80,15 @@ export function createWorkerHttpHandler(
|
|
|
74
80
|
logger.info(`Worker: received task assignment - ${title} (${taskId})`);
|
|
75
81
|
|
|
76
82
|
if (taskExecutor && resultReporter) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
taskExecutor({
|
|
84
|
+
taskId,
|
|
85
|
+
title,
|
|
86
|
+
description,
|
|
87
|
+
recommendedSkills,
|
|
88
|
+
executionSessionKey,
|
|
89
|
+
executionIdempotencyKey,
|
|
90
|
+
repo,
|
|
91
|
+
})
|
|
84
92
|
.then((result) => {
|
|
85
93
|
if (isTaskCancelled?.(taskId)) {
|
|
86
94
|
logger.info(`Worker: skipping result report for cancelled task ${taskId}`);
|
|
@@ -51,6 +51,8 @@ export function createWorkerPromptInjector(
|
|
|
51
51
|
parts.push("11. Treat file paths from documents, plans, and teammate messages as hints, not guarantees. Verify the real path exists in the current workspace before reading or editing it; if it does not exist, search for the closest real file and note the drift instead of repeatedly calling missing paths.");
|
|
52
52
|
parts.push("12. The workspace may be backed by a TeamClaw-managed git repository. Treat the current checkout as canonical project state; do not delete `.git` or replace the repo with ad-hoc archives.");
|
|
53
53
|
parts.push("13. If the assigned task includes recommended skills, use those exact skill slugs first. Missing skills should be searched/installed before execution when supported by the runtime.");
|
|
54
|
+
parts.push("14. Important: submit structured collaboration contracts, not only prose. Use teamclaw_submit_result_contract before your final reply, use structured fields on progress/handoff/review/message tools, and use clarification tools instead of hiding questions inside freeform output.");
|
|
55
|
+
parts.push("15. Do not use sessions_yield or end your turn while background work, coding agents, or process sessions are still running. A TeamClaw task is only done when you have the real final deliverable, not when a helper session is still working.");
|
|
54
56
|
parts.push(`Worker ID: ${identity.workerId}`);
|
|
55
57
|
parts.push(`Controller: ${identity.controllerUrl}`);
|
|
56
58
|
|
|
@@ -7,6 +7,7 @@ import { resolveDefaultOpenClawWorkspaceDir } from "../openclaw-workspace.js";
|
|
|
7
7
|
import type { TaskAssignmentPayload, TaskExecutionEventInput } from "../types.js";
|
|
8
8
|
|
|
9
9
|
type SkillCli = "openclaw" | "clawhub";
|
|
10
|
+
const ON_DEMAND_DISCOVERY_SKILLS = new Set(["find-skills"]);
|
|
10
11
|
|
|
11
12
|
type CommandResult = {
|
|
12
13
|
ok: boolean;
|
|
@@ -185,6 +186,18 @@ export async function installRecommendedSkills(
|
|
|
185
186
|
});
|
|
186
187
|
|
|
187
188
|
for (const requestedSkill of recommendedSkills) {
|
|
189
|
+
if (ON_DEMAND_DISCOVERY_SKILLS.has(normalizeKey(requestedSkill))) {
|
|
190
|
+
skipped.push(requestedSkill);
|
|
191
|
+
events.push({
|
|
192
|
+
type: "lifecycle",
|
|
193
|
+
phase: "skill_install_skipped",
|
|
194
|
+
source: "worker",
|
|
195
|
+
status: "running",
|
|
196
|
+
message: `Skipping automatic install of discovery skill ${requestedSkill}; invoke it on demand inside the task if needed.`,
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
188
201
|
let resolvedSlug = isSkillSlug(requestedSkill) ? requestedSkill : undefined;
|
|
189
202
|
const installedPath = resolvedSlug ? buildInstalledSkillPath(workspaceDir, resolvedSlug) : "";
|
|
190
203
|
|
package/src/worker/tools.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import {
|
|
3
|
+
backfillWorkerProgressContract,
|
|
4
|
+
ensureTeamMessageContract,
|
|
5
|
+
normalizeContractRole,
|
|
6
|
+
normalizeContractStringList,
|
|
7
|
+
normalizeOptionalContractText,
|
|
8
|
+
normalizeTaskHandoffContract,
|
|
9
|
+
normalizeWorkerProgressContract,
|
|
10
|
+
normalizeWorkerTaskResultContract,
|
|
11
|
+
renderWorkerProgressText,
|
|
12
|
+
} from "../interaction-contracts.js";
|
|
2
13
|
import type { PluginConfig, WorkerIdentity } from "../types.js";
|
|
3
14
|
|
|
4
15
|
const ALLOWED_PROGRESS_STATUSES = new Set(["in_progress", "review"]);
|
|
@@ -20,6 +31,10 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
20
31
|
targetRole: Type.String({ description: "Exact target role ID (pm, architect, developer, qa, release-engineer, infra-engineer, devops, security-engineer, designer, marketing)" }),
|
|
21
32
|
question: Type.String({ description: "The question to ask" }),
|
|
22
33
|
taskId: Type.Optional(Type.String({ description: "Related task ID if any" })),
|
|
34
|
+
summary: Type.Optional(Type.String({ description: "Short structured summary for this question" })),
|
|
35
|
+
details: Type.Optional(Type.String({ description: "Optional extra context for the peer" })),
|
|
36
|
+
requestedAction: Type.Optional(Type.String({ description: "Concrete response/action needed from the peer" })),
|
|
37
|
+
references: Type.Optional(Type.Array(Type.String({ description: "Relevant task IDs, file paths, or artifact references" }))),
|
|
23
38
|
}),
|
|
24
39
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
25
40
|
const identity = getIdentity();
|
|
@@ -29,12 +44,25 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
29
44
|
|
|
30
45
|
const targetRole = String(params.targetRole ?? "");
|
|
31
46
|
const question = String(params.question ?? "");
|
|
47
|
+
const normalizedTargetRole = normalizeContractRole(targetRole);
|
|
32
48
|
|
|
33
49
|
if (!targetRole || !question) {
|
|
34
50
|
return { content: [{ type: "text" as const, text: "targetRole and question are required." }] };
|
|
35
51
|
}
|
|
36
52
|
|
|
37
53
|
try {
|
|
54
|
+
const contract = ensureTeamMessageContract(null, {
|
|
55
|
+
type: "direct",
|
|
56
|
+
content: question,
|
|
57
|
+
toRole: normalizedTargetRole,
|
|
58
|
+
taskId: typeof params.taskId === "string" ? params.taskId : undefined,
|
|
59
|
+
summary: typeof params.summary === "string" ? params.summary : undefined,
|
|
60
|
+
details: typeof params.details === "string" ? params.details : undefined,
|
|
61
|
+
requestedAction: typeof params.requestedAction === "string" ? params.requestedAction : undefined,
|
|
62
|
+
references: normalizeContractStringList(params.references),
|
|
63
|
+
intent: "question",
|
|
64
|
+
needsResponse: true,
|
|
65
|
+
});
|
|
38
66
|
const res = await fetch(`${identity.controllerUrl}/api/v1/messages/direct`, {
|
|
39
67
|
method: "POST",
|
|
40
68
|
headers: { "Content-Type": "application/json" },
|
|
@@ -44,6 +72,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
44
72
|
toRole: targetRole,
|
|
45
73
|
content: question,
|
|
46
74
|
taskId: params.taskId ?? null,
|
|
75
|
+
contract,
|
|
47
76
|
}),
|
|
48
77
|
});
|
|
49
78
|
|
|
@@ -64,6 +93,11 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
64
93
|
parameters: Type.Object({
|
|
65
94
|
message: Type.String({ description: "The message to broadcast" }),
|
|
66
95
|
taskId: Type.Optional(Type.String({ description: "Related task ID if any" })),
|
|
96
|
+
summary: Type.Optional(Type.String({ description: "Short structured summary for the broadcast" })),
|
|
97
|
+
details: Type.Optional(Type.String({ description: "Optional extra context for the team" })),
|
|
98
|
+
requestedAction: Type.Optional(Type.String({ description: "Optional action the team should take after reading this message" })),
|
|
99
|
+
needsResponse: Type.Optional(Type.Boolean({ description: "Whether the broadcast expects a response from recipients" })),
|
|
100
|
+
references: Type.Optional(Type.Array(Type.String({ description: "Relevant task IDs, file paths, or artifact references" }))),
|
|
67
101
|
}),
|
|
68
102
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
69
103
|
const identity = getIdentity();
|
|
@@ -77,6 +111,17 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
77
111
|
}
|
|
78
112
|
|
|
79
113
|
try {
|
|
114
|
+
const contract = ensureTeamMessageContract(null, {
|
|
115
|
+
type: "broadcast",
|
|
116
|
+
content: message,
|
|
117
|
+
taskId: typeof params.taskId === "string" ? params.taskId : undefined,
|
|
118
|
+
summary: typeof params.summary === "string" ? params.summary : undefined,
|
|
119
|
+
details: typeof params.details === "string" ? params.details : undefined,
|
|
120
|
+
requestedAction: typeof params.requestedAction === "string" ? params.requestedAction : undefined,
|
|
121
|
+
needsResponse: typeof params.needsResponse === "boolean" ? params.needsResponse : undefined,
|
|
122
|
+
references: normalizeContractStringList(params.references),
|
|
123
|
+
intent: "announcement",
|
|
124
|
+
});
|
|
80
125
|
const res = await fetch(`${identity.controllerUrl}/api/v1/messages/broadcast`, {
|
|
81
126
|
method: "POST",
|
|
82
127
|
headers: { "Content-Type": "application/json" },
|
|
@@ -85,6 +130,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
85
130
|
fromRole: identity.role,
|
|
86
131
|
content: message,
|
|
87
132
|
taskId: params.taskId ?? null,
|
|
133
|
+
contract,
|
|
88
134
|
}),
|
|
89
135
|
});
|
|
90
136
|
|
|
@@ -106,6 +152,9 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
106
152
|
targetRole: Type.String({ description: "Exact target role ID to request review from" }),
|
|
107
153
|
reviewContent: Type.String({ description: "Content to review or description of what needs review" }),
|
|
108
154
|
taskId: Type.String({ description: "Related task ID" }),
|
|
155
|
+
summary: Type.Optional(Type.String({ description: "Short structured summary for the review request" })),
|
|
156
|
+
requestedAction: Type.Optional(Type.String({ description: "Concrete review action expected from the target role" })),
|
|
157
|
+
references: Type.Optional(Type.Array(Type.String({ description: "Relevant file paths, artifacts, or checks to review" }))),
|
|
109
158
|
}),
|
|
110
159
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
111
160
|
const identity = getIdentity();
|
|
@@ -122,6 +171,17 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
122
171
|
}
|
|
123
172
|
|
|
124
173
|
try {
|
|
174
|
+
const contract = ensureTeamMessageContract(null, {
|
|
175
|
+
type: "review-request",
|
|
176
|
+
content: reviewContent,
|
|
177
|
+
toRole: normalizeContractRole(targetRole),
|
|
178
|
+
taskId,
|
|
179
|
+
summary: typeof params.summary === "string" ? params.summary : undefined,
|
|
180
|
+
requestedAction: typeof params.requestedAction === "string" ? params.requestedAction : undefined,
|
|
181
|
+
references: normalizeContractStringList(params.references),
|
|
182
|
+
intent: "review-request",
|
|
183
|
+
needsResponse: true,
|
|
184
|
+
});
|
|
125
185
|
const res = await fetch(`${identity.controllerUrl}/api/v1/messages/review-request`, {
|
|
126
186
|
method: "POST",
|
|
127
187
|
headers: { "Content-Type": "application/json" },
|
|
@@ -131,6 +191,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
131
191
|
toRole: targetRole,
|
|
132
192
|
content: reviewContent,
|
|
133
193
|
taskId,
|
|
194
|
+
contract,
|
|
134
195
|
}),
|
|
135
196
|
});
|
|
136
197
|
|
|
@@ -152,6 +213,9 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
152
213
|
taskId: Type.String({ description: "Task ID to hand off" }),
|
|
153
214
|
targetRole: Type.String({ description: "Exact target role ID to hand off to" }),
|
|
154
215
|
reason: Type.String({ description: "Reason for the handoff" }),
|
|
216
|
+
summary: Type.Optional(Type.String({ description: "Short structured summary for the handoff" })),
|
|
217
|
+
expectedNextStep: Type.Optional(Type.String({ description: "Concrete next step the receiving role should take" })),
|
|
218
|
+
artifacts: Type.Optional(Type.Array(Type.String({ description: "Files, task IDs, or artifacts the next role should inspect first" }))),
|
|
155
219
|
}),
|
|
156
220
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
157
221
|
const identity = getIdentity();
|
|
@@ -168,6 +232,13 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
168
232
|
}
|
|
169
233
|
|
|
170
234
|
try {
|
|
235
|
+
const contract = normalizeTaskHandoffContract(null, {
|
|
236
|
+
targetRole: normalizeContractRole(targetRole),
|
|
237
|
+
reason,
|
|
238
|
+
summary: typeof params.summary === "string" ? params.summary : undefined,
|
|
239
|
+
expectedNextStep: typeof params.expectedNextStep === "string" ? params.expectedNextStep : undefined,
|
|
240
|
+
artifacts: normalizeContractStringList(params.artifacts),
|
|
241
|
+
});
|
|
171
242
|
const res = await fetch(`${identity.controllerUrl}/api/v1/tasks/${taskId}/handoff`, {
|
|
172
243
|
method: "POST",
|
|
173
244
|
headers: { "Content-Type": "application/json" },
|
|
@@ -175,6 +246,7 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
175
246
|
fromWorkerId: identity.workerId,
|
|
176
247
|
targetRole,
|
|
177
248
|
reason,
|
|
249
|
+
contract,
|
|
178
250
|
}),
|
|
179
251
|
});
|
|
180
252
|
|
|
@@ -188,6 +260,83 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
188
260
|
}
|
|
189
261
|
},
|
|
190
262
|
},
|
|
263
|
+
{
|
|
264
|
+
name: "teamclaw_submit_result_contract",
|
|
265
|
+
label: "Submit Result Contract",
|
|
266
|
+
description: "Record the structured completion/blocker contract for the current task before the final worker reply is returned",
|
|
267
|
+
parameters: Type.Object({
|
|
268
|
+
taskId: Type.String({ description: "Task ID" }),
|
|
269
|
+
outcome: Type.Optional(Type.String({ description: "Outcome: completed, blocked, or failed" })),
|
|
270
|
+
summary: Type.String({ description: "Short structured summary of the worker result" }),
|
|
271
|
+
deliverables: Type.Optional(
|
|
272
|
+
Type.Array(
|
|
273
|
+
Type.Object({
|
|
274
|
+
kind: Type.String({ description: "Deliverable kind: file, directory, command, artifact, or note" }),
|
|
275
|
+
value: Type.String({ description: "Deliverable identifier, path, or note" }),
|
|
276
|
+
summary: Type.Optional(Type.String({ description: "Optional short note about this deliverable" })),
|
|
277
|
+
}),
|
|
278
|
+
),
|
|
279
|
+
),
|
|
280
|
+
keyPoints: Type.Optional(Type.Array(Type.String({ description: "Important decisions, findings, or implementation notes" }))),
|
|
281
|
+
blockers: Type.Optional(Type.Array(Type.String({ description: "Any unresolved blockers or risks" }))),
|
|
282
|
+
followUps: Type.Optional(
|
|
283
|
+
Type.Array(
|
|
284
|
+
Type.Object({
|
|
285
|
+
type: Type.String({ description: "Follow-up type: review, handoff, clarification, downstream-task" }),
|
|
286
|
+
targetRole: Type.Optional(Type.String({ description: "Role that should handle the follow-up" })),
|
|
287
|
+
reason: Type.String({ description: "Why this follow-up is needed" }),
|
|
288
|
+
}),
|
|
289
|
+
),
|
|
290
|
+
),
|
|
291
|
+
questions: Type.Optional(Type.Array(Type.String({ description: "Any remaining open questions" }))),
|
|
292
|
+
notes: Type.Optional(Type.String({ description: "Optional extra delivery notes" })),
|
|
293
|
+
}),
|
|
294
|
+
async execute(_id: string, params: Record<string, unknown>) {
|
|
295
|
+
const identity = getIdentity();
|
|
296
|
+
if (!identity) {
|
|
297
|
+
return { content: [{ type: "text" as const, text: "Not registered with a team." }] };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const taskId = String(params.taskId ?? "");
|
|
301
|
+
if (!taskId) {
|
|
302
|
+
return { content: [{ type: "text" as const, text: "taskId is required." }] };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const contract = normalizeWorkerTaskResultContract({
|
|
306
|
+
version: "1.0",
|
|
307
|
+
outcome: typeof params.outcome === "string" ? params.outcome : "completed",
|
|
308
|
+
summary: params.summary,
|
|
309
|
+
deliverables: params.deliverables,
|
|
310
|
+
keyPoints: params.keyPoints,
|
|
311
|
+
blockers: params.blockers,
|
|
312
|
+
followUps: params.followUps,
|
|
313
|
+
questions: params.questions,
|
|
314
|
+
notes: params.notes,
|
|
315
|
+
});
|
|
316
|
+
if (!contract) {
|
|
317
|
+
return { content: [{ type: "text" as const, text: "summary is required for teamclaw_submit_result_contract." }] };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const res = await fetch(`${identity.controllerUrl}/api/v1/tasks/${taskId}/result-contract`, {
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: { "Content-Type": "application/json" },
|
|
324
|
+
body: JSON.stringify({ contract, workerId: identity.workerId }),
|
|
325
|
+
});
|
|
326
|
+
if (!res.ok) {
|
|
327
|
+
return { content: [{ type: "text" as const, text: `Failed to submit result contract: ${res.status}` }] };
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
content: [{
|
|
331
|
+
type: "text" as const,
|
|
332
|
+
text: `Result contract recorded for ${taskId}: ${contract.outcome} / ${contract.summary}`,
|
|
333
|
+
}],
|
|
334
|
+
};
|
|
335
|
+
} catch (err) {
|
|
336
|
+
return { content: [{ type: "text" as const, text: `Error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
},
|
|
191
340
|
{
|
|
192
341
|
name: "teamclaw_request_clarification",
|
|
193
342
|
label: "Request Clarification",
|
|
@@ -267,8 +416,12 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
267
416
|
description: "Report progress on an assigned task",
|
|
268
417
|
parameters: Type.Object({
|
|
269
418
|
taskId: Type.String({ description: "Task ID" }),
|
|
270
|
-
progress: Type.String({ description: "Progress update message" }),
|
|
419
|
+
progress: Type.Optional(Type.String({ description: "Progress update message" })),
|
|
271
420
|
status: Type.Optional(Type.String({ description: "Optional non-terminal status: in_progress or review. Do not use completed or failed here." })),
|
|
421
|
+
summary: Type.Optional(Type.String({ description: "Short structured progress summary" })),
|
|
422
|
+
currentStep: Type.Optional(Type.String({ description: "What the worker is doing right now" })),
|
|
423
|
+
nextStep: Type.Optional(Type.String({ description: "What the worker plans to do next" })),
|
|
424
|
+
blockers: Type.Optional(Type.Array(Type.String({ description: "Any blockers slowing progress" }))),
|
|
272
425
|
}),
|
|
273
426
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
274
427
|
const identity = getIdentity();
|
|
@@ -277,12 +430,15 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
277
430
|
}
|
|
278
431
|
|
|
279
432
|
const taskId = String(params.taskId ?? "");
|
|
280
|
-
const progress =
|
|
433
|
+
const progress = typeof params.progress === "string" ? params.progress : "";
|
|
281
434
|
const status = typeof params.status === "string" ? params.status : undefined;
|
|
282
435
|
|
|
283
436
|
if (!taskId) {
|
|
284
437
|
return { content: [{ type: "text" as const, text: "taskId is required." }] };
|
|
285
438
|
}
|
|
439
|
+
if (!progress && typeof params.summary !== "string") {
|
|
440
|
+
return { content: [{ type: "text" as const, text: "progress or summary is required." }] };
|
|
441
|
+
}
|
|
286
442
|
if (status && !ALLOWED_PROGRESS_STATUSES.has(status)) {
|
|
287
443
|
return {
|
|
288
444
|
content: [{
|
|
@@ -293,10 +449,23 @@ export function createWorkerTools(deps: WorkerToolsDeps) {
|
|
|
293
449
|
}
|
|
294
450
|
|
|
295
451
|
try {
|
|
296
|
-
const
|
|
452
|
+
const progressContract = normalizeWorkerProgressContract({
|
|
453
|
+
version: "1.0",
|
|
454
|
+
summary: typeof params.summary === "string" ? params.summary : progress,
|
|
455
|
+
status,
|
|
456
|
+
currentStep: params.currentStep,
|
|
457
|
+
nextStep: params.nextStep,
|
|
458
|
+
blockers: params.blockers,
|
|
459
|
+
}) ?? backfillWorkerProgressContract(progress, status);
|
|
460
|
+
const patch: Record<string, unknown> = {
|
|
461
|
+
progress: renderWorkerProgressText(progressContract, progress),
|
|
462
|
+
};
|
|
297
463
|
if (status) {
|
|
298
464
|
patch.status = status;
|
|
299
465
|
}
|
|
466
|
+
if (progressContract) {
|
|
467
|
+
patch.progressContract = progressContract;
|
|
468
|
+
}
|
|
300
469
|
|
|
301
470
|
const res = await fetch(`${identity.controllerUrl}/api/v1/tasks/${taskId}`, {
|
|
302
471
|
method: "PATCH",
|
|
@@ -11,10 +11,10 @@ export type WorkerServiceDeps = {
|
|
|
11
11
|
config: PluginConfig;
|
|
12
12
|
logger: PluginLogger;
|
|
13
13
|
onIdentityEstablished: (identity: WorkerIdentity) => void;
|
|
14
|
-
taskExecutor?: (taskDescription: string,
|
|
14
|
+
taskExecutor?: (taskDescription: string, assignment: TaskAssignmentPayload) => Promise<string>;
|
|
15
15
|
prepareTaskAssignment?: (assignment: TaskAssignmentPayload) => Promise<void> | void;
|
|
16
16
|
publishTaskAssignment?: (assignment: TaskAssignmentPayload, result: string) => Promise<void> | void;
|
|
17
|
-
cancelTaskExecution?: (taskId: string) => Promise<boolean> | boolean;
|
|
17
|
+
cancelTaskExecution?: (taskId: string, sessionKey?: string) => Promise<boolean> | boolean;
|
|
18
18
|
messageQueue?: MessageQueue;
|
|
19
19
|
};
|
|
20
20
|
|
|
@@ -27,6 +27,7 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
|
|
|
27
27
|
let controllerUrl: string | null = null;
|
|
28
28
|
let workerId: string | null = null;
|
|
29
29
|
let activeTaskId: string | undefined;
|
|
30
|
+
const activeTaskSessionKeys = new Map<string, string>();
|
|
30
31
|
const cancelledTaskIds = new Set<string>();
|
|
31
32
|
|
|
32
33
|
const taskExecutor = externalTaskExecutor
|
|
@@ -34,10 +35,11 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
|
|
|
34
35
|
const taskId = assignment.taskId;
|
|
35
36
|
cancelledTaskIds.delete(taskId);
|
|
36
37
|
activeTaskId = taskId;
|
|
38
|
+
activeTaskSessionKeys.set(taskId, assignment.executionSessionKey || `teamclaw-task-${taskId}`);
|
|
37
39
|
try {
|
|
38
40
|
await deps.prepareTaskAssignment?.(assignment);
|
|
39
41
|
const taskPrompt = [assignment.title.trim(), assignment.description.trim()].filter(Boolean).join("\n\n");
|
|
40
|
-
const result = await externalTaskExecutor(taskPrompt,
|
|
42
|
+
const result = await externalTaskExecutor(taskPrompt, assignment);
|
|
41
43
|
if (cancelledTaskIds.has(taskId)) {
|
|
42
44
|
throw new Error("Task execution cancelled by controller");
|
|
43
45
|
}
|
|
@@ -45,6 +47,7 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
|
|
|
45
47
|
return result;
|
|
46
48
|
} finally {
|
|
47
49
|
activeTaskId = undefined;
|
|
50
|
+
activeTaskSessionKeys.delete(taskId);
|
|
48
51
|
if (!cancelledTaskIds.has(taskId)) {
|
|
49
52
|
cancelledTaskIds.delete(taskId);
|
|
50
53
|
}
|
|
@@ -74,7 +77,7 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
|
|
|
74
77
|
|
|
75
78
|
cancelledTaskIds.add(taskId);
|
|
76
79
|
try {
|
|
77
|
-
const cancelled = await deps.cancelTaskExecution?.(taskId);
|
|
80
|
+
const cancelled = await deps.cancelTaskExecution?.(taskId, activeTaskSessionKeys.get(taskId));
|
|
78
81
|
return cancelled ?? true;
|
|
79
82
|
} catch (err) {
|
|
80
83
|
logger.warn(`Worker: failed to cancel task ${taskId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -130,9 +133,9 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
|
|
|
130
133
|
} else {
|
|
131
134
|
controllerUrl = identity.controllerUrl;
|
|
132
135
|
workerId = identity.workerId;
|
|
136
|
+
onIdentityEstablished(identity);
|
|
133
137
|
// Restart server with worker ID and task executor
|
|
134
138
|
await startServer();
|
|
135
|
-
onIdentityEstablished(identity);
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
// Start heartbeat
|
|
@@ -142,8 +145,8 @@ export function createWorkerService(deps: WorkerServiceDeps): OpenClawPluginServ
|
|
|
142
145
|
if (newIdentity && !controllerUrl) {
|
|
143
146
|
controllerUrl = newIdentity.controllerUrl;
|
|
144
147
|
workerId = newIdentity.workerId;
|
|
145
|
-
await startServer();
|
|
146
148
|
onIdentityEstablished(newIdentity);
|
|
149
|
+
await startServer();
|
|
147
150
|
}
|
|
148
151
|
return;
|
|
149
152
|
}
|