@lelouchhe/webagent 0.8.0 → 0.10.0
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 +43 -15
- package/config.toml +7 -27
- package/dist/index.html +21 -5
- package/dist/js/app.INIQQEGD.js +5 -0
- package/dist/js/chunk.3CLGCUHW.js +1 -0
- package/dist/js/{chunk.CT5WBNGZ.js → chunk.7WADDFJZ.js} +50 -49
- package/dist/js/chunk.AOTG3PL7.js +20 -0
- package/dist/js/{login.2WA6DTGM.js → login.WMURU4NI.js} +1 -1
- package/dist/js/viewer.RHZMFYWJ.js +1 -0
- package/dist/login.html +2 -2
- package/dist/share-viewer.html +6 -6
- package/dist/{styles.00etlpgs.css → styles.01aj0l37.css} +186 -4
- package/dist/sw.js +6 -6
- package/lib/agent-key.js +6 -0
- package/lib/attachment-dispatch.js +60 -31
- package/lib/attachment-interceptor.js +7 -7
- package/lib/attachment-labels.js +1 -1
- package/lib/attachments.js +69 -7
- package/lib/auth-middleware.js +11 -4
- package/lib/auth.js +2 -2
- package/lib/bridge.js +209 -90
- package/lib/client-registry.js +12 -12
- package/lib/config.js +2 -31
- package/lib/event-handler.js +166 -85
- package/lib/files/limits.js +15 -0
- package/lib/files/paths.js +155 -0
- package/lib/files/routes.js +232 -0
- package/lib/home-path.js +35 -0
- package/lib/http-status.js +1 -0
- package/lib/mcp/capability.js +74 -0
- package/lib/mcp/server.js +148 -0
- package/lib/mcp/task-history.js +245 -0
- package/lib/mcp/task-host.js +253 -0
- package/lib/mcp/tools.js +168 -0
- package/lib/mode-bucket.js +1 -1
- package/lib/push-service.js +33 -35
- package/lib/routes.js +1022 -475
- package/lib/server.js +84 -34
- package/lib/share/routes.js +97 -85
- package/lib/shared/task-reference.js +20 -0
- package/lib/sse-manager.js +8 -8
- package/lib/store.js +992 -284
- package/lib/task-collaboration.js +15 -0
- package/lib/task-manager.js +1409 -0
- package/lib/task-path.js +131 -0
- package/lib/{session-state.js → task-state.js} +90 -38
- package/lib/task-tree-lock.js +74 -0
- package/lib/{sessions-anchor.js → tasks-anchor.js} +8 -7
- package/lib/tokens.js +1 -1
- package/lib/types.js +2 -2
- package/package.json +8 -1
- package/dist/js/app.XBFXH37R.js +0 -2
- package/dist/js/chunk.UMQMOGWO.js +0 -1
- package/dist/js/viewer.CVWXSKJM.js +0 -1
- package/lib/session-manager.js +0 -613
- package/lib/title-service.js +0 -95
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
const MAX_RECORD_TEXT = 800;
|
|
2
|
+
const MAX_TOOL_DETAIL = 400;
|
|
3
|
+
function isObject(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function stringValue(value) {
|
|
7
|
+
return typeof value === "string" ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
function textValue(value) {
|
|
10
|
+
// Preserve message/code indentation; joinText only removes whitespace around
|
|
11
|
+
// the final record envelope, never from the text itself.
|
|
12
|
+
return stringValue(value) ?? "";
|
|
13
|
+
}
|
|
14
|
+
function clip(text, limit) {
|
|
15
|
+
if (text.length <= limit)
|
|
16
|
+
return { text, truncated: false };
|
|
17
|
+
// Keep the ellipsis inside the stated limit, including its separating newline.
|
|
18
|
+
return { text: `${text.slice(0, limit - 2)}\n…`, truncated: true };
|
|
19
|
+
}
|
|
20
|
+
function joinText(parts) {
|
|
21
|
+
const truncated = parts.some((part) => isObject(part) && part.truncated === true);
|
|
22
|
+
const text = parts
|
|
23
|
+
.map((part) => (typeof part === "string" ? part : (part?.text ?? "")))
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.join("\n")
|
|
26
|
+
.trim();
|
|
27
|
+
const clipped = clip(text, MAX_RECORD_TEXT);
|
|
28
|
+
return { text: clipped.text, truncated: truncated || clipped.truncated };
|
|
29
|
+
}
|
|
30
|
+
function prefixed(label, value) {
|
|
31
|
+
return { text: `${label}${value.text}`, truncated: value.truncated };
|
|
32
|
+
}
|
|
33
|
+
function toolContentText(value) {
|
|
34
|
+
if (!Array.isArray(value))
|
|
35
|
+
return "";
|
|
36
|
+
return value
|
|
37
|
+
.map((item) => {
|
|
38
|
+
if (!isObject(item))
|
|
39
|
+
return "";
|
|
40
|
+
if (isObject(item.content))
|
|
41
|
+
return textValue(item.content.text);
|
|
42
|
+
if (Array.isArray(item.content)) {
|
|
43
|
+
return item.content
|
|
44
|
+
.map((nested) => (isObject(nested) ? textValue(nested.text) : ""))
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join("");
|
|
47
|
+
}
|
|
48
|
+
return "";
|
|
49
|
+
})
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.join("\n");
|
|
52
|
+
}
|
|
53
|
+
function toolInputDetail(value) {
|
|
54
|
+
if (!isObject(value))
|
|
55
|
+
return undefined;
|
|
56
|
+
const command = textValue(value.command);
|
|
57
|
+
if (command)
|
|
58
|
+
return prefixed("$ ", clip(command, MAX_TOOL_DETAIL));
|
|
59
|
+
const path = textValue(value.path);
|
|
60
|
+
if (path)
|
|
61
|
+
return prefixed("Path: ", clip(path, MAX_TOOL_DETAIL));
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
function optionLabel(value) {
|
|
65
|
+
if (!isObject(value))
|
|
66
|
+
return "Unknown option";
|
|
67
|
+
return (textValue(value.label) ||
|
|
68
|
+
textValue(value.name) ||
|
|
69
|
+
textValue(value.optionId) ||
|
|
70
|
+
"Unknown option");
|
|
71
|
+
}
|
|
72
|
+
function planText(entries) {
|
|
73
|
+
if (!Array.isArray(entries) || entries.length === 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
return entries
|
|
76
|
+
.map((entry) => {
|
|
77
|
+
if (!isObject(entry))
|
|
78
|
+
return "- Unknown plan item";
|
|
79
|
+
const status = textValue(entry.status) || "pending";
|
|
80
|
+
const content = textValue(entry.content) || "Untitled plan item";
|
|
81
|
+
return `- [${status}] ${content}`;
|
|
82
|
+
})
|
|
83
|
+
.join("\n");
|
|
84
|
+
}
|
|
85
|
+
function malformedRecord(seq, type, createdAt, rawSize) {
|
|
86
|
+
return {
|
|
87
|
+
seq,
|
|
88
|
+
type,
|
|
89
|
+
createdAt,
|
|
90
|
+
text: `Unreadable ${type} event; raw payload omitted.`,
|
|
91
|
+
rawSize,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Produce a small deterministic, human-readable task-history record. Raw event
|
|
96
|
+
* payloads stay in SQLite and are deliberately never embedded in the MCP
|
|
97
|
+
* response: tool inputs and outputs commonly contain complete source files.
|
|
98
|
+
*/
|
|
99
|
+
// eslint-disable-next-line complexity -- maps the complete persisted event schema.
|
|
100
|
+
export function compactTaskHistoryRecord(record) {
|
|
101
|
+
const rawSize = Buffer.byteLength(record.data, "utf8");
|
|
102
|
+
let data;
|
|
103
|
+
try {
|
|
104
|
+
data = JSON.parse(record.data);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return malformedRecord(record.seq, record.type, record.createdAt, rawSize);
|
|
108
|
+
}
|
|
109
|
+
if (!isObject(data)) {
|
|
110
|
+
return malformedRecord(record.seq, record.type, record.createdAt, rawSize);
|
|
111
|
+
}
|
|
112
|
+
let result;
|
|
113
|
+
switch (record.type) {
|
|
114
|
+
case "assistant_message":
|
|
115
|
+
result = joinText([
|
|
116
|
+
"Assistant:",
|
|
117
|
+
textValue(data.text) || "(empty message)",
|
|
118
|
+
]);
|
|
119
|
+
break;
|
|
120
|
+
case "user_message": {
|
|
121
|
+
const attachments = Array.isArray(data.attachments)
|
|
122
|
+
? `Attachments: ${data.attachments.length}`
|
|
123
|
+
: undefined;
|
|
124
|
+
result = joinText([
|
|
125
|
+
"User:",
|
|
126
|
+
textValue(data.text) || "(empty message)",
|
|
127
|
+
attachments,
|
|
128
|
+
]);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "tool_call": {
|
|
132
|
+
const title = textValue(data.title) || "Unnamed tool";
|
|
133
|
+
const kind = textValue(data.kind);
|
|
134
|
+
result = joinText([
|
|
135
|
+
`Tool started: ${title}${kind ? ` (${kind})` : ""}`,
|
|
136
|
+
toolInputDetail(data.rawInput),
|
|
137
|
+
]);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "tool_call_update": {
|
|
141
|
+
const title = textValue(data.title) || textValue(data.kind) || "Unnamed tool";
|
|
142
|
+
const status = textValue(data.status) || "updated";
|
|
143
|
+
const content = toolContentText(data.content);
|
|
144
|
+
result = joinText([
|
|
145
|
+
`Tool ${status}: ${title}`,
|
|
146
|
+
content
|
|
147
|
+
? prefixed("Result:\n", clip(content, MAX_TOOL_DETAIL))
|
|
148
|
+
: undefined,
|
|
149
|
+
toolInputDetail(data.rawInput),
|
|
150
|
+
]);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "plan":
|
|
154
|
+
result = joinText(["Plan:", planText(data.entries) ?? "(empty plan)"]);
|
|
155
|
+
break;
|
|
156
|
+
case "permission_request": {
|
|
157
|
+
const options = Array.isArray(data.options)
|
|
158
|
+
? `Options: ${data.options.map(optionLabel).join(", ")}`
|
|
159
|
+
: undefined;
|
|
160
|
+
result = joinText([
|
|
161
|
+
`Permission requested: ${textValue(data.title) || "Unnamed request"}`,
|
|
162
|
+
options,
|
|
163
|
+
]);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "permission_response":
|
|
167
|
+
result = joinText([
|
|
168
|
+
`Permission ${data.denied === true ? "denied" : "allowed"}: ${textValue(data.optionName) || "Unnamed option"}`,
|
|
169
|
+
]);
|
|
170
|
+
break;
|
|
171
|
+
case "prompt_done": {
|
|
172
|
+
const stopReason = textValue(data.stopReason) || "unknown";
|
|
173
|
+
// Normal completions add no collaboration information: workflowStatus
|
|
174
|
+
// already communicates that the task is idle.
|
|
175
|
+
if (stopReason === "end_turn")
|
|
176
|
+
return null;
|
|
177
|
+
result = joinText([`Turn finished: ${stopReason}`]);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case "error":
|
|
181
|
+
result = joinText(["Error:", textValue(data.message) || "Unknown error"]);
|
|
182
|
+
break;
|
|
183
|
+
case "system_message":
|
|
184
|
+
result = joinText([
|
|
185
|
+
textValue(data.title) || "(empty system message)",
|
|
186
|
+
textValue(data.body) || undefined,
|
|
187
|
+
]);
|
|
188
|
+
break;
|
|
189
|
+
case "task_update":
|
|
190
|
+
result = joinText([
|
|
191
|
+
`Task ${textValue(data.status) || "updated"}:`,
|
|
192
|
+
textValue(data.body) || "(no details)",
|
|
193
|
+
]);
|
|
194
|
+
break;
|
|
195
|
+
case "task_cancel":
|
|
196
|
+
result = joinText([
|
|
197
|
+
"Task cancellation requested:",
|
|
198
|
+
textValue(data.reason) || "(no reason)",
|
|
199
|
+
]);
|
|
200
|
+
break;
|
|
201
|
+
case "message":
|
|
202
|
+
result = joinText([
|
|
203
|
+
`Message from ${textValue(data.from_label) || textValue(data.from_ref) || "unknown sender"}: ${textValue(data.title) || "Untitled"}`,
|
|
204
|
+
textValue(data.body) || "(empty message)",
|
|
205
|
+
]);
|
|
206
|
+
break;
|
|
207
|
+
case "bash_command":
|
|
208
|
+
result = joinText([
|
|
209
|
+
"Shell command:",
|
|
210
|
+
`$ ${textValue(data.command) || "(empty command)"}`,
|
|
211
|
+
]);
|
|
212
|
+
break;
|
|
213
|
+
case "bash_result": {
|
|
214
|
+
const code = typeof data.code === "number" || typeof data.code === "string"
|
|
215
|
+
? String(data.code)
|
|
216
|
+
: "unknown";
|
|
217
|
+
const signal = textValue(data.signal);
|
|
218
|
+
const output = textValue(data.output);
|
|
219
|
+
result = joinText([
|
|
220
|
+
`Shell finished: exit ${code}${signal ? `, signal ${signal}` : ""}`,
|
|
221
|
+
output
|
|
222
|
+
? prefixed("Output:\n", clip(output, MAX_TOOL_DETAIL))
|
|
223
|
+
: undefined,
|
|
224
|
+
]);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
default:
|
|
228
|
+
// New event types are not treated as noise: make their occurrence
|
|
229
|
+
// visible without risking an arbitrary raw payload in agent context.
|
|
230
|
+
return {
|
|
231
|
+
seq: record.seq,
|
|
232
|
+
type: record.type,
|
|
233
|
+
createdAt: record.createdAt,
|
|
234
|
+
text: `Unrecognized ${record.type} event; raw payload omitted.`,
|
|
235
|
+
rawSize,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
seq: record.seq,
|
|
240
|
+
type: record.type,
|
|
241
|
+
createdAt: record.createdAt,
|
|
242
|
+
text: result.text,
|
|
243
|
+
...(result.truncated ? { truncated: true, rawSize } : {}),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { isLocalCollaborationTarget, collaborationRelation, } from "../task-collaboration.js";
|
|
4
|
+
import { expandHomePath } from "../home-path.js";
|
|
5
|
+
import { formatTaskReference } from "../shared/task-reference.js";
|
|
6
|
+
import { compactTaskHistoryRecord } from "./task-history.js";
|
|
7
|
+
const DEFAULT_QUERY_LIMIT = 5;
|
|
8
|
+
const MAX_QUERY_LIMIT = 100;
|
|
9
|
+
function encodeCursor(cursor) {
|
|
10
|
+
return Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
11
|
+
}
|
|
12
|
+
function decodeCursor(raw) {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
15
|
+
const beforeSeq = parsed.beforeSeq;
|
|
16
|
+
if (typeof parsed.taskId !== "string" ||
|
|
17
|
+
typeof beforeSeq !== "number" ||
|
|
18
|
+
!Number.isInteger(beforeSeq) ||
|
|
19
|
+
beforeSeq < 1 ||
|
|
20
|
+
(parsed.text !== undefined && typeof parsed.text !== "string")) {
|
|
21
|
+
throw new Error("invalid cursor");
|
|
22
|
+
}
|
|
23
|
+
return { taskId: parsed.taskId, beforeSeq, text: parsed.text };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
throw new Error("invalid_cursor");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function relationOrder(relation) {
|
|
30
|
+
return { self: 0, parent: 1, child: 2, sibling: 3 }[relation];
|
|
31
|
+
}
|
|
32
|
+
export function createMcpTaskToolHost(deps) {
|
|
33
|
+
const { store, tasks, getBridge, broadcastCollaboration, broadcastTaskCreated, } = deps;
|
|
34
|
+
function requireTask(taskId) {
|
|
35
|
+
const task = store.getTask(taskId);
|
|
36
|
+
if (!task)
|
|
37
|
+
throw new Error("task_not_found");
|
|
38
|
+
return task;
|
|
39
|
+
}
|
|
40
|
+
function requireLocalTarget(sourceTaskId, targetTaskId) {
|
|
41
|
+
const source = requireTask(sourceTaskId);
|
|
42
|
+
const target = requireTask(targetTaskId);
|
|
43
|
+
if (!isLocalCollaborationTarget(source, target)) {
|
|
44
|
+
throw new Error("target_not_allowed");
|
|
45
|
+
}
|
|
46
|
+
return { source, target };
|
|
47
|
+
}
|
|
48
|
+
function requireChildTarget(sourceTaskId, targetTaskId) {
|
|
49
|
+
const source = requireTask(sourceTaskId);
|
|
50
|
+
const target = requireTask(targetTaskId);
|
|
51
|
+
if (target.parent_id !== source.id) {
|
|
52
|
+
throw new Error("target_not_allowed");
|
|
53
|
+
}
|
|
54
|
+
return { source, target };
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
list(sourceTaskId) {
|
|
58
|
+
const source = requireTask(sourceTaskId);
|
|
59
|
+
return store
|
|
60
|
+
.listTasks()
|
|
61
|
+
.map((task) => {
|
|
62
|
+
const relation = collaborationRelation(source, task);
|
|
63
|
+
if (!relation)
|
|
64
|
+
return null;
|
|
65
|
+
return {
|
|
66
|
+
id: task.id,
|
|
67
|
+
title: task.title ?? task.id,
|
|
68
|
+
brief: task.brief || null,
|
|
69
|
+
relation,
|
|
70
|
+
};
|
|
71
|
+
})
|
|
72
|
+
.filter((task) => task !== null)
|
|
73
|
+
.sort((a, b) => relationOrder(a.relation) - relationOrder(b.relation) ||
|
|
74
|
+
a.id.localeCompare(b.id));
|
|
75
|
+
},
|
|
76
|
+
query(sourceTaskId, input) {
|
|
77
|
+
const targetTaskId = input.taskId ?? sourceTaskId;
|
|
78
|
+
const { target } = targetTaskId === sourceTaskId
|
|
79
|
+
? { target: requireTask(sourceTaskId) }
|
|
80
|
+
: requireLocalTarget(sourceTaskId, targetTaskId);
|
|
81
|
+
const cursor = input.cursor ? decodeCursor(input.cursor) : undefined;
|
|
82
|
+
if (cursor && cursor.taskId !== target.id) {
|
|
83
|
+
throw new Error("invalid_cursor");
|
|
84
|
+
}
|
|
85
|
+
if (cursor && input.text !== undefined && cursor.text !== input.text) {
|
|
86
|
+
throw new Error("invalid_cursor");
|
|
87
|
+
}
|
|
88
|
+
const text = input.text ?? cursor?.text;
|
|
89
|
+
const limit = Math.min(Math.max(1, input.limit ?? DEFAULT_QUERY_LIMIT), MAX_QUERY_LIMIT);
|
|
90
|
+
const beforeSeq = cursor?.beforeSeq;
|
|
91
|
+
const events = store.getEvents(target.id, {
|
|
92
|
+
excludeThinking: true,
|
|
93
|
+
beforeSeq,
|
|
94
|
+
limit,
|
|
95
|
+
text,
|
|
96
|
+
});
|
|
97
|
+
if (events.length === 0) {
|
|
98
|
+
return {
|
|
99
|
+
workflowStatus: target.workflow_status,
|
|
100
|
+
records: [],
|
|
101
|
+
hasMore: false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const firstSeq = events[0].seq;
|
|
105
|
+
const hasMore = store.getEvents(target.id, {
|
|
106
|
+
excludeThinking: true,
|
|
107
|
+
beforeSeq: firstSeq,
|
|
108
|
+
limit: 1,
|
|
109
|
+
text,
|
|
110
|
+
}).length > 0;
|
|
111
|
+
const records = events
|
|
112
|
+
.map((event) => compactTaskHistoryRecord({
|
|
113
|
+
seq: event.seq,
|
|
114
|
+
type: event.type,
|
|
115
|
+
data: event.data,
|
|
116
|
+
createdAt: event.created_at,
|
|
117
|
+
}))
|
|
118
|
+
.filter((record) => record !== null);
|
|
119
|
+
return {
|
|
120
|
+
workflowStatus: target.workflow_status,
|
|
121
|
+
records,
|
|
122
|
+
hasMore,
|
|
123
|
+
...(hasMore
|
|
124
|
+
? {
|
|
125
|
+
nextCursor: encodeCursor({
|
|
126
|
+
taskId: target.id,
|
|
127
|
+
beforeSeq: firstSeq,
|
|
128
|
+
text,
|
|
129
|
+
}),
|
|
130
|
+
}
|
|
131
|
+
: {}),
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
getRecord(sourceTaskId, input) {
|
|
135
|
+
const targetTaskId = input.taskId ?? sourceTaskId;
|
|
136
|
+
const { target } = targetTaskId === sourceTaskId
|
|
137
|
+
? { target: requireTask(sourceTaskId) }
|
|
138
|
+
: requireLocalTarget(sourceTaskId, targetTaskId);
|
|
139
|
+
if (!Number.isInteger(input.seq) || input.seq < 1) {
|
|
140
|
+
throw new Error("invalid_seq");
|
|
141
|
+
}
|
|
142
|
+
const event = store.getEvent(target.id, input.seq);
|
|
143
|
+
// Thinking is deliberately excluded from task_query and must not be
|
|
144
|
+
// recoverable through the explicit raw-record escape hatch.
|
|
145
|
+
if (!event || event.type === "thinking") {
|
|
146
|
+
throw new Error("record_not_found");
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
taskId: target.id,
|
|
150
|
+
record: {
|
|
151
|
+
id: event.id,
|
|
152
|
+
taskId: event.task_id,
|
|
153
|
+
seq: event.seq,
|
|
154
|
+
type: event.type,
|
|
155
|
+
data: event.data,
|
|
156
|
+
fromRef: event.from_ref,
|
|
157
|
+
createdAt: event.created_at,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
async create(sourceTaskId, input) {
|
|
162
|
+
const bridge = getBridge();
|
|
163
|
+
if (!bridge)
|
|
164
|
+
throw new Error("agent_not_ready");
|
|
165
|
+
const source = requireTask(sourceTaskId);
|
|
166
|
+
const requestedCwd = input.cwd ? expandHomePath(input.cwd) : source.cwd;
|
|
167
|
+
const cwd = isAbsolute(requestedCwd)
|
|
168
|
+
? requestedCwd
|
|
169
|
+
: resolve(source.cwd, requestedCwd);
|
|
170
|
+
const created = await tasks.createTask(bridge, cwd, source.id, "agent", {
|
|
171
|
+
parentId: source.id,
|
|
172
|
+
title: input.title,
|
|
173
|
+
model: input.model,
|
|
174
|
+
thinking: input.thinking,
|
|
175
|
+
});
|
|
176
|
+
const taskCreatedMessageId = randomUUID();
|
|
177
|
+
const taskCreatedTitle = `Created task ${formatTaskReference(input.title)}`;
|
|
178
|
+
const taskCreatedBody = [
|
|
179
|
+
`Task ID: ${created.taskId}`,
|
|
180
|
+
`cwd: ${cwd}`,
|
|
181
|
+
input.model ? `model: ${input.model}` : "model: inherited",
|
|
182
|
+
input.thinking ? `thinking: ${input.thinking}` : "thinking: inherited",
|
|
183
|
+
].join("\n");
|
|
184
|
+
store.saveEvent(source.id, "system_message", {
|
|
185
|
+
kind: "task_created",
|
|
186
|
+
taskId: created.taskId,
|
|
187
|
+
taskTitle: input.title,
|
|
188
|
+
cwd,
|
|
189
|
+
model: input.model ?? null,
|
|
190
|
+
thinking: input.thinking ?? null,
|
|
191
|
+
title: taskCreatedTitle,
|
|
192
|
+
body: taskCreatedBody,
|
|
193
|
+
}, { from_ref: "agent" });
|
|
194
|
+
broadcastTaskCreated?.({
|
|
195
|
+
messageId: taskCreatedMessageId,
|
|
196
|
+
sourceTaskId: source.id,
|
|
197
|
+
targetTaskId: created.taskId,
|
|
198
|
+
title: taskCreatedTitle,
|
|
199
|
+
body: taskCreatedBody,
|
|
200
|
+
});
|
|
201
|
+
return { taskId: created.taskId };
|
|
202
|
+
},
|
|
203
|
+
async cancel(sourceTaskId, targetTaskId, reason) {
|
|
204
|
+
const bridge = getBridge();
|
|
205
|
+
const { target } = requireChildTarget(sourceTaskId, targetTaskId);
|
|
206
|
+
const result = await tasks.cancelTaskExecution(target.id, bridge, deps.cancelTimeoutMs ?? 0);
|
|
207
|
+
store.saveEvent(target.id, "task_cancel", { sourceTaskId, reason, status: result.status }, { from_ref: "agent" });
|
|
208
|
+
return { accepted: true, taskId: target.id, status: result.status };
|
|
209
|
+
},
|
|
210
|
+
async send(sourceTaskId, targetTaskId, body) {
|
|
211
|
+
const bridge = getBridge();
|
|
212
|
+
const { source, target } = requireLocalTarget(sourceTaskId, targetTaskId);
|
|
213
|
+
const created = store.createCollaborationMessage({
|
|
214
|
+
id: randomUUID(),
|
|
215
|
+
deliveryId: randomUUID(),
|
|
216
|
+
sourceTaskId,
|
|
217
|
+
directTargetTaskId: target.id,
|
|
218
|
+
sourceActor: "agent",
|
|
219
|
+
body,
|
|
220
|
+
});
|
|
221
|
+
const sourceLabel = source.title ?? source.id.slice(0, 8);
|
|
222
|
+
const targetLabel = target.title ?? target.id.slice(0, 8);
|
|
223
|
+
broadcastCollaboration?.({
|
|
224
|
+
messageId: created.message.id,
|
|
225
|
+
sourceTaskId,
|
|
226
|
+
targetTaskId: target.id,
|
|
227
|
+
title: `${formatTaskReference(sourceLabel)} sent ${formatTaskReference(targetLabel)}`,
|
|
228
|
+
body: created.message.body,
|
|
229
|
+
});
|
|
230
|
+
if (bridge) {
|
|
231
|
+
void tasks.drainCollaborationDeliveries(bridge, target.id);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
async update(sourceTaskId, status, body) {
|
|
235
|
+
const bridge = getBridge();
|
|
236
|
+
const { parentTaskId, collaborationMessageId } = store.recordAgentWorkflowUpdate(sourceTaskId, status, body);
|
|
237
|
+
if (collaborationMessageId && parentTaskId) {
|
|
238
|
+
const source = requireTask(sourceTaskId);
|
|
239
|
+
const target = requireTask(parentTaskId);
|
|
240
|
+
broadcastCollaboration?.({
|
|
241
|
+
messageId: collaborationMessageId,
|
|
242
|
+
sourceTaskId,
|
|
243
|
+
targetTaskId: parentTaskId,
|
|
244
|
+
title: `${formatTaskReference(source.title ?? source.id.slice(0, 8))} sent ${formatTaskReference(target.title ?? target.id.slice(0, 8))}`,
|
|
245
|
+
body: `Task status: ${status}\n${body}`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (bridge && parentTaskId) {
|
|
249
|
+
void tasks.drainCollaborationDeliveries(bridge, parentTaskId);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
package/lib/mcp/tools.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const TASK_ID = z.string().trim().min(1).max(256);
|
|
3
|
+
const BODY = z
|
|
4
|
+
.string()
|
|
5
|
+
.max(64 * 1024)
|
|
6
|
+
.refine((value) => value.trim().length > 0, "Body must not be empty");
|
|
7
|
+
function jsonContent(value) {
|
|
8
|
+
return {
|
|
9
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function accepted() {
|
|
13
|
+
return jsonContent({ accepted: true });
|
|
14
|
+
}
|
|
15
|
+
function unavailable() {
|
|
16
|
+
throw new Error("Task MCP tools are not configured");
|
|
17
|
+
}
|
|
18
|
+
/** Register the Agent-facing Task control-plane tools. */
|
|
19
|
+
export function registerMcpTools(server, taskId, host) {
|
|
20
|
+
server.registerTool("task_list", {
|
|
21
|
+
description: "Discover Tasks available for coordination. " +
|
|
22
|
+
"Use this before choosing a Task to contact.",
|
|
23
|
+
inputSchema: {},
|
|
24
|
+
}, async () => jsonContent({ tasks: host?.list(taskId) ?? unavailable() }));
|
|
25
|
+
server.registerTool("task_query", {
|
|
26
|
+
description: "Inspect a Task's recorded turn history — what happened and what earlier " +
|
|
27
|
+
"turns decided. Turn events, including provider errors and attachment " +
|
|
28
|
+
"metadata, are recorded here. Do not use this tool to wait for work or " +
|
|
29
|
+
"poll for completion.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
task_id: TASK_ID.nullable()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Visible Task ID; null or omission defaults to the current Task"),
|
|
34
|
+
text: z
|
|
35
|
+
.string()
|
|
36
|
+
.min(1)
|
|
37
|
+
.max(256)
|
|
38
|
+
.nullable()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Literal text to find; null is treated as omitted"),
|
|
41
|
+
cursor: z
|
|
42
|
+
.string()
|
|
43
|
+
.min(1)
|
|
44
|
+
.max(512)
|
|
45
|
+
.nullable()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Opaque cursor from a previous result; null is omitted"),
|
|
48
|
+
limit: z
|
|
49
|
+
.number()
|
|
50
|
+
.int()
|
|
51
|
+
.min(1)
|
|
52
|
+
.max(100)
|
|
53
|
+
.nullable()
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Maximum records to return; null uses the default"),
|
|
56
|
+
},
|
|
57
|
+
}, async ({ task_id, text, cursor, limit }) => jsonContent(host?.query(taskId, {
|
|
58
|
+
taskId: task_id ?? undefined,
|
|
59
|
+
text: text ?? undefined,
|
|
60
|
+
cursor: cursor ?? undefined,
|
|
61
|
+
limit: limit ?? undefined,
|
|
62
|
+
}) ?? unavailable()));
|
|
63
|
+
server.registerTool("task_get_record", {
|
|
64
|
+
description: "Inspect one full history record when the available history summary is insufficient. " +
|
|
65
|
+
"Use a sequence obtained from task_query.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
task_id: TASK_ID.nullable()
|
|
68
|
+
.optional()
|
|
69
|
+
.describe("Visible target task ID; null or omission defaults to the current task"),
|
|
70
|
+
seq: z
|
|
71
|
+
.number()
|
|
72
|
+
.int()
|
|
73
|
+
.min(1)
|
|
74
|
+
.describe("Stable event sequence within the target task"),
|
|
75
|
+
},
|
|
76
|
+
}, async ({ task_id, seq }) => jsonContent(host?.getRecord(taskId, {
|
|
77
|
+
taskId: task_id ?? undefined,
|
|
78
|
+
seq,
|
|
79
|
+
}) ?? unavailable()));
|
|
80
|
+
server.registerTool("task_create", {
|
|
81
|
+
description: "Create a direct child Task for independent work. " +
|
|
82
|
+
"Immediately use task_send to give it the first instruction, then end the dispatch turn without polling.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
title: z
|
|
85
|
+
.string()
|
|
86
|
+
.trim()
|
|
87
|
+
.min(1)
|
|
88
|
+
.max(256)
|
|
89
|
+
.refine((value) => !value.includes("/"), "Title must not contain '/'")
|
|
90
|
+
.describe("Task title"),
|
|
91
|
+
cwd: z
|
|
92
|
+
.string()
|
|
93
|
+
.trim()
|
|
94
|
+
.min(1)
|
|
95
|
+
.max(4096)
|
|
96
|
+
.nullable()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe("Working directory; null or omission inherits the current Task"),
|
|
99
|
+
model: z
|
|
100
|
+
.string()
|
|
101
|
+
.trim()
|
|
102
|
+
.min(1)
|
|
103
|
+
.max(256)
|
|
104
|
+
.nullable()
|
|
105
|
+
.optional()
|
|
106
|
+
.describe("Model override; null or omission inherits the current Task"),
|
|
107
|
+
thinking: z
|
|
108
|
+
.string()
|
|
109
|
+
.trim()
|
|
110
|
+
.min(1)
|
|
111
|
+
.max(64)
|
|
112
|
+
.nullable()
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("Thinking level override; null or omission inherits the current Task"),
|
|
115
|
+
},
|
|
116
|
+
}, async ({ title, cwd, model, thinking }) => {
|
|
117
|
+
if (!host)
|
|
118
|
+
return unavailable();
|
|
119
|
+
return jsonContent(await host.create(taskId, {
|
|
120
|
+
title,
|
|
121
|
+
cwd: cwd ?? undefined,
|
|
122
|
+
model: model ?? undefined,
|
|
123
|
+
thinking: thinking ?? undefined,
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
server.registerTool("task_cancel", {
|
|
127
|
+
description: "Stop a child Task's current work when it should no longer continue. " +
|
|
128
|
+
"The Task and its history remain available.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
target: TASK_ID.describe("Stable child Task ID"),
|
|
131
|
+
reason: BODY.describe("Why the child Task should stop"),
|
|
132
|
+
},
|
|
133
|
+
}, async ({ target, reason }) => {
|
|
134
|
+
if (!host)
|
|
135
|
+
return unavailable();
|
|
136
|
+
return jsonContent(await host.cancel(taskId, target, reason));
|
|
137
|
+
});
|
|
138
|
+
server.registerTool("task_send", {
|
|
139
|
+
description: "Send a durable coordination message to another Task. " +
|
|
140
|
+
"Use it for instructions, questions, findings, progress, decisions, and follow-up. " +
|
|
141
|
+
"This is communication, not a lifecycle handoff; use task_update(done|blocked) for completion or blocking. " +
|
|
142
|
+
"Do not wait for or poll the recipient.",
|
|
143
|
+
inputSchema: {
|
|
144
|
+
target: TASK_ID.describe("Stable target Task ID"),
|
|
145
|
+
body: BODY.describe("Verbatim coordination or handoff message"),
|
|
146
|
+
},
|
|
147
|
+
}, async ({ target, body }) => {
|
|
148
|
+
if (!host)
|
|
149
|
+
return unavailable();
|
|
150
|
+
await host.send(taskId, target, body);
|
|
151
|
+
return accepted();
|
|
152
|
+
});
|
|
153
|
+
server.registerTool("task_update", {
|
|
154
|
+
description: "Send a typed lifecycle handoff for the current Task. " +
|
|
155
|
+
"Use blocked when work needs input or a decision, and done when the assignment is complete. " +
|
|
156
|
+
"The parent receives the handoff and decides the next step; this does not delete or permanently close the Task. " +
|
|
157
|
+
"Use task_send for normal communication.",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
status: z.enum(["blocked", "done"]),
|
|
160
|
+
body: BODY.describe("Handoff body: explain the blocker or report the completed result"),
|
|
161
|
+
},
|
|
162
|
+
}, async ({ status, body }) => {
|
|
163
|
+
if (!host)
|
|
164
|
+
return unavailable();
|
|
165
|
+
await host.update(taskId, status, body);
|
|
166
|
+
return accepted();
|
|
167
|
+
});
|
|
168
|
+
}
|
package/lib/mode-bucket.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Cross-agent ACP mode classification.
|
|
2
2
|
//
|
|
3
3
|
// Different agents emit `currentModeId` in different forms:
|
|
4
|
-
// - Copilot CLI: "https://agentclientprotocol.com/protocol/
|
|
4
|
+
// - Copilot CLI: "https://agentclientprotocol.com/protocol/task-modes#autopilot"
|
|
5
5
|
// - Claude Code: "bypassPermissions" (bare camelCase string)
|
|
6
6
|
// - Codex: "read-only" / "auto" / "full-access" (bare hyphenated)
|
|
7
7
|
// - Gemini CLI: "default" / "autoEdit" / "yolo" / "plan" (bare; enum-based)
|