@timqi/pier 0.1.14 → 0.3.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 +44 -22
- package/dist/agent/config.js +2 -2
- package/dist/agent/events.js +32 -3
- package/dist/agent/listing.js +14 -10
- package/dist/agent/packages.js +6 -10
- package/dist/agent/pi.js +113 -70
- package/dist/agent/roles.js +104 -0
- package/dist/agent/types.js +78 -0
- package/dist/boards/boards.js +42 -32
- package/dist/channels/chains.js +0 -4
- package/dist/channels/commands.js +7 -12
- package/dist/channels/config.js +27 -1
- package/dist/channels/control.js +5 -16
- package/dist/channels/conversations.js +4 -9
- package/dist/channels/lark-api.js +20 -26
- package/dist/channels/lark-outbound.js +21 -29
- package/dist/channels/lark-panel.js +1 -1
- package/dist/channels/lark.js +81 -39
- package/dist/channels/lines.js +33 -1
- package/dist/channels/panel.js +17 -107
- package/dist/channels/routes.js +11 -21
- package/dist/channels/runtime.js +79 -25
- package/dist/channels/slack-outbound.js +12 -14
- package/dist/channels/slack-panel.js +1 -1
- package/dist/channels/slack-render.js +1 -1
- package/dist/channels/slack-thread.js +65 -0
- package/dist/channels/slack.js +85 -124
- package/dist/channels/types.js +1 -1
- package/dist/cli.js +7 -2
- package/dist/config-sync.js +42 -37
- package/dist/core/chain.js +221 -0
- package/dist/core/identity.js +36 -13
- package/dist/core/inbound-file.js +11 -4
- package/dist/core/reply.js +111 -81
- package/dist/core/router.js +111 -118
- package/dist/core/search.js +65 -0
- package/dist/core/types.js +38 -77
- package/dist/db.js +41 -2
- package/dist/drain.js +1 -1
- package/dist/main.js +65 -40
- package/dist/settings.js +26 -19
- package/dist/socket.js +1 -0
- package/dist/tasks/agent.js +11 -5
- package/dist/tasks/callbacks.js +55 -5
- package/dist/tasks/cli.js +22 -11
- package/dist/tasks/definitions.js +30 -17
- package/dist/tasks/execution.js +11 -5
- package/dist/tasks/groups.js +5 -2
- package/dist/tasks/messages.js +7 -7
- package/dist/tasks/open-items.js +88 -0
- package/dist/tasks/operations.js +76 -39
- package/dist/tasks/outbox.js +19 -6
- package/dist/tasks/routes.js +19 -249
- package/dist/tasks/service.js +166 -34
- package/dist/tasks/store.js +123 -57
- package/dist/tasks/types.js +3 -0
- package/dist/tools.js +12 -5
- package/dist/web/auth.js +2 -2
- package/dist/web/config-sync.js +4 -5
- package/dist/web/config.js +9 -2
- package/dist/web/explorer.js +2 -2
- package/dist/web/fs.js +16 -1
- package/dist/web/instance.js +8 -5
- package/dist/web/packages.js +1 -1
- package/dist/web/passkeys.js +3 -3
- package/dist/web/providers.js +3 -3
- package/dist/web/public/assets/code-CWT27KH1.js +2 -0
- package/dist/web/public/assets/code-CWT27KH1.js.br +0 -0
- package/dist/web/public/assets/code-CWT27KH1.js.gz +0 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js +5 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js.br +0 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js.gz +0 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js +72 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js.br +0 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js.gz +0 -0
- package/dist/web/public/assets/{hljs-tWqyD59G.js → hljs-BRz6a8Dq.js} +2 -2
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.br +0 -0
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.gz +0 -0
- package/dist/web/public/assets/index-D0ZYdweB.js +14 -0
- package/dist/web/public/assets/index-D0ZYdweB.js.br +0 -0
- package/dist/web/public/assets/index-D0ZYdweB.js.gz +0 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css +2 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css.br +0 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css.gz +0 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js +9 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js.br +0 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js.gz +0 -0
- package/dist/web/public/index.html +39 -109
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +0 -2
- package/dist/web/push.js +3 -3
- package/dist/web/server.js +89 -116
- package/dist/web/session-state.js +4 -40
- package/docs/deploy.md +5 -3
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +14 -4
- package/skills/pier-help/SKILL.md +62 -23
- package/skills/pier-search/SKILL.md +27 -0
- package/skills/pier-tasks/SKILL.md +68 -18
- package/dist/channels/handoff.js +0 -94
- package/dist/web/public/assets/activity-Ds6fCHnb.js +0 -5
- package/dist/web/public/assets/activity-Ds6fCHnb.js.br +0 -0
- package/dist/web/public/assets/activity-Ds6fCHnb.js.gz +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js +0 -1
- package/dist/web/public/assets/boards-BKCj6EwK.js.br +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js +0 -4
- package/dist/web/public/assets/explorer-DnTm975c.js.br +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js.gz +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js +0 -85
- package/dist/web/public/assets/index-BQo-haPN.js.br +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js.gz +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css +0 -2
- package/dist/web/public/assets/index-DiHj0w1i.css.br +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css.gz +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js +0 -1
- package/dist/web/public/assets/runs-C_AthWcW.js.br +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js.gz +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js +0 -5
- package/dist/web/public/assets/settings-DDaAtFlc.js.br +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js.gz +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js +0 -3
- package/dist/web/public/assets/task-runs-0pvdITiV.js.br +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js.gz +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js +0 -4
- package/dist/web/public/assets/tasks-Bz29caHJ.js.br +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js.gz +0 -0
package/dist/tasks/routes.js
CHANGED
|
@@ -1,258 +1,28 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!states.includes(params.state))
|
|
11
|
-
throw new Error("invalid state");
|
|
12
|
-
query.state = params.state;
|
|
13
|
-
}
|
|
14
|
-
if (params.source) {
|
|
15
|
-
if (!sources.includes(params.source))
|
|
16
|
-
throw new Error("invalid source");
|
|
17
|
-
query.source = params.source;
|
|
18
|
-
}
|
|
19
|
-
if (params.taskId)
|
|
20
|
-
query.taskId = params.taskId;
|
|
21
|
-
for (const key of ["since", "until", "limit"]) {
|
|
22
|
-
if (params[key] === undefined)
|
|
23
|
-
continue;
|
|
24
|
-
const value = Number(params[key]);
|
|
25
|
-
if (!params[key] || !Number.isSafeInteger(value) || value < 0 || (key === "limit" && (value < 1 || value > 200)))
|
|
26
|
-
throw new Error(`invalid ${key}`);
|
|
27
|
-
query[key] = value;
|
|
28
|
-
}
|
|
29
|
-
if (query.since !== undefined && query.until !== undefined && query.since > query.until)
|
|
30
|
-
throw new Error("since exceeds until");
|
|
31
|
-
if (params.showUnmatched !== undefined) {
|
|
32
|
-
if (!["true", "false"].includes(params.showUnmatched))
|
|
33
|
-
throw new Error("invalid showUnmatched");
|
|
34
|
-
query.showUnmatched = params.showUnmatched === "true";
|
|
35
|
-
}
|
|
36
|
-
if (params.cursor) {
|
|
37
|
-
const cursor = JSON.parse(params.cursor);
|
|
38
|
-
const value = record(cursor);
|
|
39
|
-
if (!value || !Number.isSafeInteger(value.queuedAt) || Number(value.queuedAt) < 0 || typeof value.id !== "string" || !value.id)
|
|
40
|
-
throw new Error("invalid cursor");
|
|
41
|
-
query.cursor = { queuedAt: Number(value.queuedAt), id: value.id };
|
|
42
|
-
}
|
|
43
|
-
return query;
|
|
44
|
-
}
|
|
45
|
-
const jsonBody = async (req) => req.json().catch(() => null);
|
|
46
|
-
export function registerTaskRoutes(app, tasks, activity) {
|
|
47
|
-
if (activity)
|
|
48
|
-
app.get("/api/activity", async (c) => {
|
|
49
|
-
const now = Date.now();
|
|
50
|
-
const recent = c.req.query("scope") === "recent";
|
|
51
|
-
const windowStart = now - 24 * 60 * 60 * 1000;
|
|
52
|
-
const runs = tasks.activityRuns(recent ? windowStart : undefined);
|
|
53
|
-
const listed = await activity.factory.list();
|
|
54
|
-
const byId = new Map(listed.map((session) => [session.id, session]));
|
|
55
|
-
const linkedIds = new Set();
|
|
56
|
-
for (const run of runs) {
|
|
57
|
-
for (const id of [run.invokedBySessionId, run.targetSessionId, run.callbackSessionId]) {
|
|
58
|
-
if (id)
|
|
59
|
-
linkedIds.add(id);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
for (const session of listed) {
|
|
63
|
-
if (activity.router.stateOf(session.id) === "streaming")
|
|
64
|
-
linkedIds.add(session.id);
|
|
65
|
-
}
|
|
66
|
-
const messages = tasks.recentMessages(windowStart)
|
|
67
|
-
.filter((message) => runs.some((run) => run.id === message.runId));
|
|
68
|
-
for (const message of messages) {
|
|
69
|
-
if (message.fromSessionId !== "console")
|
|
70
|
-
linkedIds.add(message.fromSessionId);
|
|
71
|
-
if (message.toSessionId !== "console")
|
|
72
|
-
linkedIds.add(message.toSessionId);
|
|
73
|
-
}
|
|
74
|
-
return c.json({
|
|
75
|
-
sessions: [...linkedIds].map((id) => {
|
|
76
|
-
const session = byId.get(id);
|
|
77
|
-
return {
|
|
78
|
-
id,
|
|
79
|
-
cwd: session?.cwd ?? "",
|
|
80
|
-
createdAt: session?.createdAt ?? null,
|
|
81
|
-
title: session?.title,
|
|
82
|
-
state: activity.router.stateOf(id) ?? "idle",
|
|
83
|
-
stateSince: activity.router.stateSinceOf(id) ?? null,
|
|
84
|
-
};
|
|
85
|
-
}),
|
|
86
|
-
runs,
|
|
87
|
-
messages,
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
app.get("/api/task-runs", (c) => {
|
|
91
|
-
try {
|
|
92
|
-
return c.json(tasks.queryRuns(runQuery(c.req.query())));
|
|
93
|
-
}
|
|
94
|
-
catch (err) {
|
|
95
|
-
return c.json({ error: String(err) }, 400);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
app.get("/api/tasks", (c) => {
|
|
99
|
-
const trigger = c.req.query("trigger");
|
|
100
|
-
const state = c.req.query("state");
|
|
101
|
-
const kind = c.req.query("kind");
|
|
102
|
-
let rows = tasks.list();
|
|
103
|
-
// Subagent one-shots are hidden unless explicitly requested via ?kind=subagent.
|
|
104
|
-
rows = rows.filter((task) => task.kind === (kind ?? "task"));
|
|
105
|
-
if (trigger)
|
|
106
|
-
rows = rows.filter((task) => task.trigger.type === trigger);
|
|
107
|
-
if (state === "archived")
|
|
108
|
-
rows = rows.filter((task) => task.archived);
|
|
109
|
-
else if (state === "active")
|
|
110
|
-
rows = rows.filter((task) => !task.archived);
|
|
111
|
-
return c.json(rows.map((task) => ({
|
|
112
|
-
...task,
|
|
113
|
-
lastRun: tasks.listRuns(task.id, 1)[0] ?? null,
|
|
114
|
-
})));
|
|
115
|
-
});
|
|
116
|
-
app.post("/api/tasks", async (c) => {
|
|
117
|
-
const body = await jsonBody(c.req);
|
|
118
|
-
const runNow = typeof body === "object" && body !== null && "runNow" in body && body.runNow === true;
|
|
119
|
-
const definition = typeof body === "object" && body !== null && "task" in body ? body.task : body;
|
|
120
|
-
try {
|
|
121
|
-
const task = await tasks.create(definition);
|
|
122
|
-
const run = runNow ? tasks.run(task.id) : null;
|
|
123
|
-
return c.json({ task, runId: run?.id ?? null }, 201);
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
return c.json({ error: String(err) }, 400);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
1
|
+
// Settings → Tasks' HTTP surface: the list, pause/resume, a task's runs. A
|
|
2
|
+
// route names the caller and hands the decision to TaskService: policy here
|
|
3
|
+
// would be policy `pier task` does not get.
|
|
4
|
+
const RUNS_LIMIT = 20;
|
|
5
|
+
export function registerTaskRoutes(app, tasks) {
|
|
6
|
+
// Subagent one-shots and archived tasks are history, not something to manage.
|
|
7
|
+
app.get("/api/tasks", (c) => c.json(tasks.list()
|
|
8
|
+
.filter((task) => task.kind === "task" && !task.archived)
|
|
9
|
+
.map((task) => ({ ...task, lastRun: tasks.listRuns(task.id, 1)[0] ?? null }))));
|
|
129
10
|
app.get("/api/tasks/:id/runs", (c) => {
|
|
130
11
|
try {
|
|
131
|
-
|
|
132
|
-
const offset = Number(c.req.query("offset") ?? 0);
|
|
133
|
-
return c.json(tasks.listRuns(c.req.param("id"), limit, offset));
|
|
12
|
+
return c.json(tasks.listRuns(tasks.get(c.req.param("id")).id, RUNS_LIMIT));
|
|
134
13
|
}
|
|
135
14
|
catch (err) {
|
|
136
15
|
return c.json({ error: String(err) }, 404);
|
|
137
16
|
}
|
|
138
17
|
});
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
catch (err) {
|
|
144
|
-
return c.json({ error: String(err) }, 404);
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
app.get("/api/task-groups/:id", (c) => {
|
|
148
|
-
try {
|
|
149
|
-
return c.json(tasks.getGroup(c.req.param("id")));
|
|
150
|
-
}
|
|
151
|
-
catch (err) {
|
|
152
|
-
return c.json({ error: String(err) }, 404);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
app.get("/api/task-runs/:id/messages", (c) => {
|
|
156
|
-
try {
|
|
157
|
-
return c.json(tasks.listMessages(c.req.param("id")));
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
return c.json({ error: String(err) }, 404);
|
|
161
|
-
}
|
|
162
|
-
});
|
|
163
|
-
app.post("/api/task-runs/:id/steer", async (c) => {
|
|
164
|
-
const body = record(await jsonBody(c.req));
|
|
165
|
-
try {
|
|
166
|
-
return c.json(await tasks.control(c.req.param("id"), typeof body?.sourceSessionId === "string" ? body.sourceSessionId : "console", body?.mode === "followUp" ? "follow_up" : "steer", requiredString(body?.message, "message")), 202);
|
|
167
|
-
}
|
|
168
|
-
catch (err) {
|
|
169
|
-
return c.json({ error: String(err) }, 400);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
app.post("/api/task-runs/:id/resume", async (c) => {
|
|
173
|
-
const body = record(await jsonBody(c.req));
|
|
174
|
-
try {
|
|
175
|
-
const wait = body?.wait === true;
|
|
176
|
-
const source = typeof body?.sourceSessionId === "string" ? body.sourceSessionId : null;
|
|
177
|
-
const run = tasks.resume(c.req.param("id"), requiredString(body?.message, "message"), {
|
|
178
|
-
invokedBySessionId: source,
|
|
179
|
-
callbackSessionId: wait ? null : source,
|
|
180
|
-
background: !wait,
|
|
181
|
-
});
|
|
182
|
-
return c.json(wait ? await tasks.waitForRun(run.id) : run, wait ? 200 : 202);
|
|
183
|
-
}
|
|
184
|
-
catch (err) {
|
|
185
|
-
return c.json({ error: String(err) }, 400);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
app.post("/api/task-runs/:id/cancel", (c) => {
|
|
189
|
-
try {
|
|
190
|
-
return c.json(tasks.cancel(c.req.param("id")), 202);
|
|
191
|
-
}
|
|
192
|
-
catch (err) {
|
|
193
|
-
return c.json({ error: String(err) }, 404);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
app.get("/api/tasks/:id", (c) => {
|
|
197
|
-
try {
|
|
198
|
-
return c.json(tasks.get(c.req.param("id")));
|
|
199
|
-
}
|
|
200
|
-
catch (err) {
|
|
201
|
-
return c.json({ error: String(err) }, 404);
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
app.patch("/api/tasks/:id", async (c) => {
|
|
205
|
-
try {
|
|
206
|
-
return c.json(await tasks.update(c.req.param("id"), await jsonBody(c.req)));
|
|
207
|
-
}
|
|
208
|
-
catch (err) {
|
|
209
|
-
return c.json({ error: String(err) }, 400);
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
app.post("/api/tasks/:id/run", async (c) => {
|
|
213
|
-
const body = record(await jsonBody(c.req));
|
|
214
|
-
const input = body && "input" in body ? body.input : null;
|
|
215
|
-
try {
|
|
216
|
-
// Dropping an unsupported override would run the definition's own policy
|
|
217
|
-
// instead — on a reuse definition, work injected into a live session.
|
|
218
|
-
if (body?.sessionMode !== undefined && body.sessionMode !== "fresh") {
|
|
219
|
-
throw new Error(`unsupported sessionMode: ${String(body.sessionMode)}`);
|
|
18
|
+
for (const [verb, enabled] of [["pause", false], ["resume", true]]) {
|
|
19
|
+
app.post(`/api/tasks/:id/${verb}`, (c) => {
|
|
20
|
+
try {
|
|
21
|
+
return c.json(tasks.setEnabled(c.req.param("id"), enabled));
|
|
220
22
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
sourceSessionId,
|
|
227
|
-
sessionMode,
|
|
228
|
-
}).id }, 202);
|
|
229
|
-
}
|
|
230
|
-
catch (err) {
|
|
231
|
-
return c.json({ error: String(err) }, 400);
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
app.post("/api/tasks/:id/pause", (c) => {
|
|
235
|
-
try {
|
|
236
|
-
return c.json(tasks.setEnabled(c.req.param("id"), false));
|
|
237
|
-
}
|
|
238
|
-
catch (err) {
|
|
239
|
-
return c.json({ error: String(err) }, 400);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
app.post("/api/tasks/:id/resume", (c) => {
|
|
243
|
-
try {
|
|
244
|
-
return c.json(tasks.setEnabled(c.req.param("id"), true));
|
|
245
|
-
}
|
|
246
|
-
catch (err) {
|
|
247
|
-
return c.json({ error: String(err) }, 400);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
app.post("/api/tasks/:id/archive", (c) => {
|
|
251
|
-
try {
|
|
252
|
-
return c.json(tasks.archive(c.req.param("id")));
|
|
253
|
-
}
|
|
254
|
-
catch (err) {
|
|
255
|
-
return c.json({ error: String(err) }, 400);
|
|
256
|
-
}
|
|
257
|
-
});
|
|
23
|
+
catch (err) {
|
|
24
|
+
return c.json({ error: String(err) }, 400);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
258
28
|
}
|
package/dist/tasks/service.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// drain needs. Decisions belong to the files beside it.
|
|
4
4
|
import { logger } from "../log.js";
|
|
5
5
|
import { AgentTaskRunner } from "./agent.js";
|
|
6
|
-
import { TaskCallbacks } from "./callbacks.js";
|
|
6
|
+
import { DESIGN_FINAL, LEAD_TURN, MILESTONE, runCwd, settleCallback, TaskCallbacks } from "./callbacks.js";
|
|
7
7
|
import { TaskDefinitions, requiredString } from "./definitions.js";
|
|
8
8
|
import { TaskExecution } from "./execution.js";
|
|
9
9
|
import { TaskGroups } from "./groups.js";
|
|
10
10
|
import { TaskMessenger } from "./messages.js";
|
|
11
|
+
import { openItems, recordOpenItems } from "./open-items.js";
|
|
11
12
|
import { TaskRunQueue } from "./runs.js";
|
|
12
13
|
import { handleTask } from "./operations.js";
|
|
13
14
|
import { isTerminal } from "./types.js";
|
|
@@ -36,17 +37,23 @@ export class TaskService {
|
|
|
36
37
|
runs;
|
|
37
38
|
execution;
|
|
38
39
|
constructor(store, factory, router, hub,
|
|
39
|
-
/** Structural: tasks/ must not import settings.ts.
|
|
40
|
+
/** Structural: tasks/ must not import settings.ts. */
|
|
40
41
|
instance) {
|
|
41
42
|
this.store = store;
|
|
42
43
|
this.factory = factory;
|
|
43
44
|
this.router = router;
|
|
44
45
|
this.hub = hub;
|
|
45
46
|
this.instance = instance;
|
|
47
|
+
const headOf = (id) => instance.continuous.chainOf(id)?.[0] ?? id;
|
|
48
|
+
const conversation = () => instance.continuous.members()[0]?.sessionId ?? null;
|
|
46
49
|
const unreachable = (sessionId, what, why) => this.unreachable(sessionId, what, why);
|
|
47
|
-
this.messages = new TaskMessenger(store, router, hub, unreachable)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
this.messages = new TaskMessenger(store, router, hub, unreachable, (runId) => {
|
|
51
|
+
const run = store.getRun(runId);
|
|
52
|
+
if (run)
|
|
53
|
+
this.status(run);
|
|
54
|
+
});
|
|
55
|
+
this.definitions = new TaskDefinitions(store, factory, router, hub, instance.systemActions);
|
|
56
|
+
this.callbacks = new TaskCallbacks(store, router, (run) => this.changed(run), unreachable, headOf, this.milestone, conversation);
|
|
50
57
|
this.groups = new TaskGroups(store, router, {
|
|
51
58
|
getRun: (id) => this.getRun(id),
|
|
52
59
|
cancel: (id) => { this.cancel(id); },
|
|
@@ -58,7 +65,7 @@ export class TaskService {
|
|
|
58
65
|
groupId,
|
|
59
66
|
}),
|
|
60
67
|
startMember: (run) => this.runs.start(run),
|
|
61
|
-
}, (group) => this.hub.emitWorkspace({ type: "task-group-changed", groupId: group.id }), unreachable);
|
|
68
|
+
}, (group) => this.hub.emitWorkspace({ type: "task-group-changed", groupId: group.id }), unreachable, headOf, this.milestone);
|
|
62
69
|
const agent = new AgentTaskRunner(factory, router, store, this.messages, (run) => this.changed(run));
|
|
63
70
|
this.execution = new TaskExecution(store, this.definitions, this.callbacks, agent, {
|
|
64
71
|
runChild: (taskId, parent) => this.run(taskId, parent.input, "task", parent.id, {
|
|
@@ -73,6 +80,84 @@ export class TaskService {
|
|
|
73
80
|
changed: (run) => this.changed(run),
|
|
74
81
|
});
|
|
75
82
|
this.runs = new TaskRunQueue(store, this.callbacks, (run) => this.execution.start(run), (run) => this.changed(run));
|
|
83
|
+
router.onTurnEnd((sessionId, text) => {
|
|
84
|
+
// Only the head's turns write the list.
|
|
85
|
+
if (instance.continuous.members()[0]?.sessionId === sessionId) {
|
|
86
|
+
try {
|
|
87
|
+
if (recordOpenItems(store, text, Date.now()))
|
|
88
|
+
hub.emitWorkspace({ type: "open-items-changed" });
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
log.warn(`open items: the markers in ${sessionId}'s reply could not be written`, err);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!DESIGN_FINAL.test(text))
|
|
95
|
+
return;
|
|
96
|
+
// On Pi's dispatch stack, which must not unwind.
|
|
97
|
+
try {
|
|
98
|
+
this.designFinal(sessionId, text);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
log.error(`lead ${sessionId}: a Design final: outside any run could not be recorded; main was not told`, err);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** A design lead finalized in its own session, a turn outside any run (the
|
|
106
|
+
* user confirmed there): recorded as a finished run of the lead whose result
|
|
107
|
+
* is that reply, so it reports to the lead's supervisor, and the session list and
|
|
108
|
+
* `/status` see it, exactly as a run's `Design final:` does. */
|
|
109
|
+
designFinal(sessionId, text) {
|
|
110
|
+
// A run's own turn reports through the run.
|
|
111
|
+
if (this.store.leadPhaseOf(sessionId) !== "design" || this.store.findActiveRunForTarget(sessionId)?.state === "running")
|
|
112
|
+
return;
|
|
113
|
+
const last = this.store.latestRunForTarget(sessionId);
|
|
114
|
+
if (!last)
|
|
115
|
+
return;
|
|
116
|
+
const run = this.store.transact(() => {
|
|
117
|
+
const run = this.runs.prepare(last.context.definition, null, "agent", null, {
|
|
118
|
+
invokedBySessionId: last.invokedBySessionId,
|
|
119
|
+
sourceSessionId: last.invokedBySessionId,
|
|
120
|
+
targetSessionId: sessionId,
|
|
121
|
+
sessionMode: "reuse",
|
|
122
|
+
resumedFromRunId: last.id,
|
|
123
|
+
callbackSessionId: last.callbackSessionId,
|
|
124
|
+
callbackMode: last.callbackMode,
|
|
125
|
+
background: last.background,
|
|
126
|
+
});
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
run.state = "succeeded";
|
|
129
|
+
run.startedAt = run.finishedAt = now;
|
|
130
|
+
run.result = { type: "agent", text, sessionId };
|
|
131
|
+
run.context.sessionId = sessionId;
|
|
132
|
+
settleCallback(run, this.store);
|
|
133
|
+
this.store.saveRun(run);
|
|
134
|
+
return run;
|
|
135
|
+
});
|
|
136
|
+
if (run.callbackSessionId === null)
|
|
137
|
+
log.warn(`lead ${sessionId}: Design final: recorded as run ${run.id}; the lead reports to no one`);
|
|
138
|
+
else
|
|
139
|
+
log.info(`lead ${sessionId}: Design final: outside any run, recorded as run ${run.id} for ${run.callbackSessionId}`);
|
|
140
|
+
this.runs.start(run);
|
|
141
|
+
this.designLead(run);
|
|
142
|
+
}
|
|
143
|
+
/** What a settled run of a design lead means to whoever shows the lead to
|
|
144
|
+
* the user (channels/runtime.ts); a reporter that throws must not unwind the settle. */
|
|
145
|
+
designLead(run) {
|
|
146
|
+
const lead = run.targetSessionId;
|
|
147
|
+
if (!this.instance.designLead || lead === null || this.store.leadPhaseOf(lead) !== "design")
|
|
148
|
+
return;
|
|
149
|
+
const state = run.callbackError === LEAD_TURN ? "waiting"
|
|
150
|
+
: run.state !== "succeeded" ? "failed"
|
|
151
|
+
: run.result?.type === "agent" && DESIGN_FINAL.test(run.result.text) ? "final"
|
|
152
|
+
: undefined;
|
|
153
|
+
if (!state)
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
this.instance.designLead(run, state);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
log.error(`lead ${lead}: its ${state} state could not be reported`, err);
|
|
160
|
+
}
|
|
76
161
|
}
|
|
77
162
|
start(tickMs = 1000) {
|
|
78
163
|
if (this.timer)
|
|
@@ -145,7 +230,7 @@ export class TaskService {
|
|
|
145
230
|
create(raw, creator = "http") {
|
|
146
231
|
return this.definitions.create(raw, creator);
|
|
147
232
|
}
|
|
148
|
-
/** `by` is how owning code says so;
|
|
233
|
+
/** `by` is how owning code says so; `pier task` has none (definitions.ts). */
|
|
149
234
|
update(id, raw, by) {
|
|
150
235
|
return this.definitions.update(id, raw, by);
|
|
151
236
|
}
|
|
@@ -155,9 +240,9 @@ export class TaskService {
|
|
|
155
240
|
archive(id, by) {
|
|
156
241
|
return this.definitions.archive(id, by);
|
|
157
242
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return this.store.listRuns(taskId,
|
|
243
|
+
/** `pier task list`'s last run: the newest `limit` of a definition's runs. */
|
|
244
|
+
listRuns(taskId, limit = 1) {
|
|
245
|
+
return this.store.listRuns(taskId, limit);
|
|
161
246
|
}
|
|
162
247
|
getRun(id) {
|
|
163
248
|
const run = this.store.getRun(id);
|
|
@@ -165,24 +250,6 @@ export class TaskService {
|
|
|
165
250
|
throw new Error(`unknown task run: ${id}`);
|
|
166
251
|
return run;
|
|
167
252
|
}
|
|
168
|
-
getRunView(id) {
|
|
169
|
-
const run = this.getRun(id);
|
|
170
|
-
return { ...run,
|
|
171
|
-
groupCallbackState: run.groupId ? this.store.getGroup(run.groupId)?.callbackState ?? null : null };
|
|
172
|
-
}
|
|
173
|
-
listMessages(runId) {
|
|
174
|
-
this.getRun(runId);
|
|
175
|
-
return this.messages.list(runId);
|
|
176
|
-
}
|
|
177
|
-
queryRuns(query = {}) {
|
|
178
|
-
return this.store.queryRuns(query);
|
|
179
|
-
}
|
|
180
|
-
activityRuns(since) {
|
|
181
|
-
return this.store.activityRuns(since);
|
|
182
|
-
}
|
|
183
|
-
recentMessages(since) {
|
|
184
|
-
return this.messages.recent(since);
|
|
185
|
-
}
|
|
186
253
|
/** Every run, not the last hour's: the card is a message in the transcript. */
|
|
187
254
|
backgroundRuns(sessionId) {
|
|
188
255
|
return this.store.listRunsForSession(sessionId, 200)
|
|
@@ -190,6 +257,28 @@ export class TaskService {
|
|
|
190
257
|
.reverse()
|
|
191
258
|
.map((run) => this.backgroundRun(run));
|
|
192
259
|
}
|
|
260
|
+
/** What the session's queue shows beside Pi's own: follow-ups waiting for it to idle. */
|
|
261
|
+
parkedMessages(sessionId) {
|
|
262
|
+
return this.store.pendingFollowUpsTo(sessionId).map((message) => ({
|
|
263
|
+
messageId: message.id,
|
|
264
|
+
runName: this.store.getRun(message.runId)?.context.definition.name ?? message.runId,
|
|
265
|
+
text: message.content,
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
/** The run ledger: runs any of `sessionIds` launched, in flight or finished since `since`, at most 200. */
|
|
269
|
+
ledger(sessionIds, since) {
|
|
270
|
+
return this.store.ledgerRuns(sessionIds, since).map(ledgerRun);
|
|
271
|
+
}
|
|
272
|
+
/** Design leads no run of which has reported `Design final:`, by their creating run: the user's to finalize. */
|
|
273
|
+
openDesigns() {
|
|
274
|
+
return [...this.store.leads().values()].flatMap((lead) => {
|
|
275
|
+
const run = lead.designOpen ? this.store.getRun(lead.runId) : undefined;
|
|
276
|
+
return run ? [ledgerRun(run)] : [];
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
openItems(now = Date.now()) {
|
|
280
|
+
return openItems(this, this.router, this.instance.continuous.members().map((m) => m.sessionId), this.openDesigns(), now);
|
|
281
|
+
}
|
|
193
282
|
activeBackgroundRunCounts() {
|
|
194
283
|
return this.store.countActiveBackgroundRunsBySession();
|
|
195
284
|
}
|
|
@@ -262,15 +351,19 @@ export class TaskService {
|
|
|
262
351
|
return this.messages.control(run, fromSessionId, mode, message);
|
|
263
352
|
}
|
|
264
353
|
resume(id, message, provenance = {}) {
|
|
354
|
+
const run = this.prepareResume(this.getRun(id), message, provenance);
|
|
355
|
+
this.runs.start(run);
|
|
356
|
+
return run;
|
|
357
|
+
}
|
|
358
|
+
prepareResume(prior, message, provenance) {
|
|
265
359
|
this.refusePaused();
|
|
266
|
-
const prior = this.getRun(id);
|
|
267
360
|
if (!isTerminal(prior.state))
|
|
268
361
|
throw new Error("run must be terminal before resume");
|
|
269
362
|
if (prior.context.definition.action.type !== "agent" || !prior.targetSessionId) {
|
|
270
363
|
throw new Error("only persisted Agent runs can be resumed");
|
|
271
364
|
}
|
|
272
365
|
const prompt = requiredString(message, "message");
|
|
273
|
-
|
|
366
|
+
return this.runs.prepare(prior.context.definition, null, "agent", null, {
|
|
274
367
|
...provenance,
|
|
275
368
|
sourceSessionId: provenance.invokedBySessionId ?? prior.invokedBySessionId,
|
|
276
369
|
targetSessionId: prior.targetSessionId,
|
|
@@ -278,16 +371,46 @@ export class TaskService {
|
|
|
278
371
|
resumedFromRunId: prior.id,
|
|
279
372
|
resumePrompt: prompt,
|
|
280
373
|
});
|
|
281
|
-
this.runs.start(run);
|
|
282
|
-
return run;
|
|
283
374
|
}
|
|
375
|
+
/** A lead reports once per wave (docs/design/10-continuous-session.md
|
|
376
|
+
* §Feature lead): the result that leaves nothing owed to it in flight
|
|
377
|
+
* resumes its last run, whose own callback reaches its supervisor. */
|
|
378
|
+
milestone = (sessionId, text, settle) => {
|
|
379
|
+
if (this.store.roleOf(sessionId) !== "lead" || this.store.countOwedTo(sessionId) > 0)
|
|
380
|
+
return "plain";
|
|
381
|
+
const last = this.store.latestRunForTarget(sessionId);
|
|
382
|
+
if (!last || last.callbackSessionId === null)
|
|
383
|
+
return "plain";
|
|
384
|
+
// A running turn reports upstream when it ends; a drain leaves the result to the next boot.
|
|
385
|
+
if (!isTerminal(last.state) || this.paused)
|
|
386
|
+
return "wait";
|
|
387
|
+
try {
|
|
388
|
+
// One transaction: a crash between the resume and the marks would resume twice.
|
|
389
|
+
const run = this.store.transact(() => {
|
|
390
|
+
const run = this.prepareResume(last, `${MILESTONE}\n\n${text()}`, {
|
|
391
|
+
invokedBySessionId: last.invokedBySessionId,
|
|
392
|
+
callbackSessionId: last.callbackSessionId,
|
|
393
|
+
callbackMode: last.callbackMode,
|
|
394
|
+
background: last.background,
|
|
395
|
+
});
|
|
396
|
+
settle();
|
|
397
|
+
return run;
|
|
398
|
+
});
|
|
399
|
+
this.runs.start(run);
|
|
400
|
+
return "resumed";
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
log.error(`lead ${sessionId}: the wave's last result could not resume run ${last.id}; delivering it as a plain callback`, err);
|
|
404
|
+
return "plain";
|
|
405
|
+
}
|
|
406
|
+
};
|
|
284
407
|
/** What `pier task` asks, under the calling session's identity. */
|
|
285
408
|
handle(raw, callerSessionId) {
|
|
286
|
-
return handleTask(this, this.definitions, this.store, raw, callerSessionId);
|
|
409
|
+
return handleTask(this, this.definitions, this.store, raw, callerSessionId, this.instance.continuous);
|
|
287
410
|
}
|
|
288
411
|
/** An agent picks from names that exist right now, never from memory. */
|
|
289
412
|
async models() {
|
|
290
|
-
const menu = this.instance
|
|
413
|
+
const menu = this.instance.modelMenu();
|
|
291
414
|
if (menu.length)
|
|
292
415
|
return { source: "menu", models: menu };
|
|
293
416
|
return { source: "catalog", models: await this.factory.availableModels() };
|
|
@@ -342,6 +465,7 @@ export class TaskService {
|
|
|
342
465
|
resolve(run);
|
|
343
466
|
this.waiters.delete(run.id);
|
|
344
467
|
this.groups.onSettled(run);
|
|
468
|
+
this.designLead(run);
|
|
345
469
|
}
|
|
346
470
|
backgroundRun(run) {
|
|
347
471
|
return {
|
|
@@ -355,12 +479,20 @@ export class TaskService {
|
|
|
355
479
|
queuedAt: run.queuedAt,
|
|
356
480
|
startedAt: run.startedAt,
|
|
357
481
|
finishedAt: run.finishedAt,
|
|
482
|
+
// A finished run's pending messages only wait for the sweep to expire them.
|
|
483
|
+
queuedMessages: isTerminal(run.state) ? 0 : this.store.countPendingFollowUps(run.id),
|
|
358
484
|
};
|
|
359
485
|
}
|
|
360
486
|
changed(run) {
|
|
361
487
|
this.hub.emitWorkspace({ type: "task-run-changed", taskId: run.taskId, runId: run.id });
|
|
488
|
+
this.status(run);
|
|
489
|
+
}
|
|
490
|
+
status(run) {
|
|
362
491
|
if (run.background && run.invokedBySessionId) {
|
|
363
492
|
this.hub.emit(run.invokedBySessionId, { type: "task-status", run: this.backgroundRun(run) });
|
|
364
493
|
}
|
|
365
494
|
}
|
|
366
495
|
}
|
|
496
|
+
function ledgerRun(run) {
|
|
497
|
+
return { runId: run.id, name: run.context.definition.name, state: run.state, targetSessionId: run.targetSessionId, cwd: runCwd(run), queuedAt: run.queuedAt, finishedAt: run.finishedAt };
|
|
498
|
+
}
|