@timqi/pier 0.0.29 → 0.1.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/dist/agent/config.js +22 -0
- package/dist/agent/events.js +4 -1
- package/dist/agent/listing.js +114 -16
- package/dist/agent/pi.js +158 -16
- package/dist/channels/lark-panel.js +1 -1
- package/dist/core/inbox.js +1 -1
- package/dist/core/router.js +4 -0
- package/dist/core/types.js +13 -1
- package/dist/db.js +30 -0
- package/dist/drain.js +1 -1
- package/dist/extensions/web/content.js +2 -2
- package/dist/main.js +8 -1
- package/dist/secrets.js +1 -1
- package/dist/service.js +2 -2
- package/dist/settings.js +31 -10
- package/dist/tasks/agent.js +109 -73
- package/dist/tasks/callbacks.js +1 -3
- package/dist/tasks/command.js +27 -8
- package/dist/tasks/execution.js +6 -4
- package/dist/tasks/groups.js +35 -26
- package/dist/tasks/messages.js +33 -27
- package/dist/tasks/outbox.js +48 -21
- package/dist/tasks/routes.js +1 -4
- package/dist/tasks/runs.js +10 -4
- package/dist/tasks/service.js +24 -12
- package/dist/tasks/store.js +17 -3
- package/dist/tasks/tool.js +97 -25
- package/dist/tools.js +2 -2
- package/dist/update.js +1 -1
- package/dist/web/instance.js +10 -3
- package/dist/web/providers.js +9 -2
- package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-Bl3vZukb.js} +2 -2
- package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
- package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js +1 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js +4 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js +85 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css +2 -0
- package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js +1 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js +5 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js +3 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js +4 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
- package/dist/web/public/index.html +76 -120
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +2 -2
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
- package/dist/web/server.js +40 -8
- package/dist/web/session-state.js +5 -5
- package/package.json +2 -1
- package/skills/pier-tasks/SKILL.md +145 -160
- package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
- package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
- package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
- package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
- package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
- package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
- package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
- package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
- package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
package/dist/tasks/groups.js
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
// separate endings into one answer — when the join condition is met, which
|
|
3
3
|
// members are cancelled, and the single aggregated callback that goes back.
|
|
4
4
|
// Delivering it is the outbox's job; deciding it is this file's.
|
|
5
|
-
import { Router } from "../core/router.js";
|
|
6
5
|
import { logger } from "../log.js";
|
|
7
6
|
import { runRef, runResultText } from "./callbacks.js";
|
|
8
7
|
import { newId } from "./definitions.js";
|
|
9
8
|
import { Outbox } from "./outbox.js";
|
|
10
|
-
import { TaskStore } from "./store.js";
|
|
11
9
|
import { isTerminal } from "./types.js";
|
|
12
10
|
const log = logger("tasks");
|
|
13
11
|
/** Core-owned fan-out join: members run detached, the group delivers one
|
|
@@ -16,11 +14,13 @@ export class TaskGroups {
|
|
|
16
14
|
store;
|
|
17
15
|
host;
|
|
18
16
|
changed;
|
|
17
|
+
unreachable;
|
|
19
18
|
outbox;
|
|
20
19
|
constructor(store, router, host, changed, unreachable) {
|
|
21
20
|
this.store = store;
|
|
22
21
|
this.host = host;
|
|
23
22
|
this.changed = changed;
|
|
23
|
+
this.unreachable = unreachable;
|
|
24
24
|
this.outbox = new Outbox(router, {
|
|
25
25
|
id: (group) => group.id,
|
|
26
26
|
reload: (id) => this.store.getGroup(id),
|
|
@@ -38,21 +38,21 @@ export class TaskGroups {
|
|
|
38
38
|
}
|
|
39
39
|
/** Enqueues every member or none: a partially started group is worse than
|
|
40
40
|
* a rejected one. */
|
|
41
|
-
runAll(definitions, join, callerSessionId, parentRunId, callbackSessionId) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.
|
|
41
|
+
runAll(definitions, join, callerSessionId, parentRunId, callbackSessionId, callbackMode) {
|
|
42
|
+
if (definitions.length < 2)
|
|
43
|
+
throw new Error("a task group needs at least 2 members");
|
|
44
|
+
const { group, runs } = this.store.transact(() => {
|
|
45
|
+
const group = this.create(join, callerSessionId, callbackSessionId, callbackMode);
|
|
46
|
+
const runs = definitions.map((definition) => this.host.prepareMember(definition.id, group.id, callerSessionId, parentRunId));
|
|
47
|
+
group.memberRunIds = runs.map((run) => run.id);
|
|
48
|
+
this.store.saveGroup(group);
|
|
49
|
+
return { group, runs };
|
|
50
|
+
});
|
|
51
|
+
this.changed(group);
|
|
52
|
+
for (const run of runs)
|
|
53
|
+
this.host.startMember(run);
|
|
54
|
+
// Skipped members do not pass through execution's settled hook.
|
|
55
|
+
this.evaluate(this.get(group.id));
|
|
56
56
|
return { group: this.get(group.id), runs };
|
|
57
57
|
}
|
|
58
58
|
members(id) {
|
|
@@ -64,12 +64,13 @@ export class TaskGroups {
|
|
|
64
64
|
this.host.cancel(runId);
|
|
65
65
|
return this.get(id);
|
|
66
66
|
}
|
|
67
|
-
create(join, invokedBySessionId, callbackSessionId) {
|
|
68
|
-
|
|
67
|
+
create(join, invokedBySessionId, callbackSessionId, callbackMode) {
|
|
68
|
+
return {
|
|
69
69
|
id: newId(),
|
|
70
70
|
join,
|
|
71
71
|
invokedBySessionId,
|
|
72
72
|
callbackSessionId,
|
|
73
|
+
...(callbackMode === "steer" ? { callbackMode } : {}),
|
|
73
74
|
memberRunIds: [],
|
|
74
75
|
winnerRunId: null,
|
|
75
76
|
callbackState: null,
|
|
@@ -79,13 +80,6 @@ export class TaskGroups {
|
|
|
79
80
|
createdAt: Date.now(),
|
|
80
81
|
finishedAt: null,
|
|
81
82
|
};
|
|
82
|
-
this.store.saveGroup(group);
|
|
83
|
-
return group;
|
|
84
|
-
}
|
|
85
|
-
setMembers(group, runIds) {
|
|
86
|
-
group.memberRunIds = runIds;
|
|
87
|
-
this.store.saveGroup(group);
|
|
88
|
-
this.changed(group);
|
|
89
83
|
}
|
|
90
84
|
get(id) {
|
|
91
85
|
const group = this.store.getGroup(id);
|
|
@@ -109,6 +103,21 @@ export class TaskGroups {
|
|
|
109
103
|
}
|
|
110
104
|
}
|
|
111
105
|
evaluate(group) {
|
|
106
|
+
if (group.finishedAt !== null)
|
|
107
|
+
return;
|
|
108
|
+
// Old versions could leave an empty group when admission failed. Retire
|
|
109
|
+
// that broken record without inventing a successful zero-member join.
|
|
110
|
+
if (group.memberRunIds.length === 0) {
|
|
111
|
+
group.finishedAt = Date.now();
|
|
112
|
+
group.callbackState = group.callbackSessionId ? "abandoned" : null;
|
|
113
|
+
group.callbackError = "group admission failed before its members were recorded";
|
|
114
|
+
this.store.saveGroup(group);
|
|
115
|
+
this.changed(group);
|
|
116
|
+
log.error(`group ${group.id}: ${group.callbackError}`);
|
|
117
|
+
if (group.callbackSessionId)
|
|
118
|
+
this.unreachable(group.callbackSessionId, `task group ${group.id}`, group.callbackError);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
112
121
|
const members = group.memberRunIds.map((id) => this.host.getRun(id));
|
|
113
122
|
if (group.join === "first") {
|
|
114
123
|
const winner = members.find((run) => isTerminal(run.state));
|
package/dist/tasks/messages.js
CHANGED
|
@@ -4,12 +4,9 @@
|
|
|
4
4
|
// the two ends are different sessions and either may be mid-turn, gone, or
|
|
5
5
|
// finished — an undelivered message is retried, expired and *said*, never
|
|
6
6
|
// dropped (§5b).
|
|
7
|
-
import { EventHub } from "../core/hub.js";
|
|
8
|
-
import { Router } from "../core/router.js";
|
|
9
7
|
import { logger } from "../log.js";
|
|
10
8
|
import { runSource } from "./callbacks.js";
|
|
11
9
|
import { newId } from "./definitions.js";
|
|
12
|
-
import { TaskStore } from "./store.js";
|
|
13
10
|
import { isTerminal, MAX_DELIVERY_ATTEMPTS, retryDelay, undeliverable } from "./types.js";
|
|
14
11
|
const log = logger("tasks");
|
|
15
12
|
const MAX_MESSAGE_LENGTH = 16 * 1024;
|
|
@@ -25,18 +22,20 @@ export class TaskMessenger {
|
|
|
25
22
|
store;
|
|
26
23
|
router;
|
|
27
24
|
hub;
|
|
28
|
-
|
|
25
|
+
prepareResume;
|
|
29
26
|
unreachable;
|
|
27
|
+
startRun;
|
|
30
28
|
constructor(store, router, hub,
|
|
31
|
-
/**
|
|
32
|
-
|
|
29
|
+
/** Prepares a continuation; it starts only after the reply commits. */
|
|
30
|
+
prepareResume,
|
|
33
31
|
/** Reports a delivery nobody can complete (service.ts owns the surfaces). */
|
|
34
|
-
unreachable) {
|
|
32
|
+
unreachable, startRun) {
|
|
35
33
|
this.store = store;
|
|
36
34
|
this.router = router;
|
|
37
35
|
this.hub = hub;
|
|
38
|
-
this.
|
|
36
|
+
this.prepareResume = prepareResume;
|
|
39
37
|
this.unreachable = unreachable;
|
|
38
|
+
this.startRun = startRun;
|
|
40
39
|
}
|
|
41
40
|
expirePending() {
|
|
42
41
|
for (const message of this.store.expirePendingMessages())
|
|
@@ -49,6 +48,7 @@ export class TaskMessenger {
|
|
|
49
48
|
/** A manual continuation supersedes an unanswered decision (design 04):
|
|
50
49
|
* one continuation per run, never two racing ones. */
|
|
51
50
|
expireDecisions(runId, reason) {
|
|
51
|
+
const expired = [];
|
|
52
52
|
for (const message of this.store.listMessages(runId)) {
|
|
53
53
|
if (message.kind !== "decision" || (message.state !== "pending" && message.state !== "delivered"))
|
|
54
54
|
continue;
|
|
@@ -56,8 +56,9 @@ export class TaskMessenger {
|
|
|
56
56
|
message.error = reason;
|
|
57
57
|
message.answeredAt = Date.now();
|
|
58
58
|
this.store.saveMessage(message);
|
|
59
|
-
|
|
59
|
+
expired.push(message);
|
|
60
60
|
}
|
|
61
|
+
return expired;
|
|
61
62
|
}
|
|
62
63
|
list(runId) {
|
|
63
64
|
return this.store.listMessages(runId);
|
|
@@ -67,6 +68,7 @@ export class TaskMessenger {
|
|
|
67
68
|
}
|
|
68
69
|
async control(run, fromSessionId, kind, content) {
|
|
69
70
|
const message = this.create(run, kind, fromSessionId, run.targetSessionId ?? "", content, null);
|
|
71
|
+
this.changed(message);
|
|
70
72
|
if (run.targetSessionId)
|
|
71
73
|
this.deliver(message, run, run.targetSessionId);
|
|
72
74
|
return this.require(message.id);
|
|
@@ -139,6 +141,7 @@ export class TaskMessenger {
|
|
|
139
141
|
throw new Error("run already has a pending supervisor decision");
|
|
140
142
|
}
|
|
141
143
|
const message = this.create(run, reason, fromSessionId, run.invokedBySessionId, content, null);
|
|
144
|
+
this.changed(message);
|
|
142
145
|
this.deliver(message, run, run.invokedBySessionId);
|
|
143
146
|
return this.require(message.id);
|
|
144
147
|
}
|
|
@@ -161,25 +164,28 @@ export class TaskMessenger {
|
|
|
161
164
|
const run = this.store.getRun(question.runId);
|
|
162
165
|
if (!run)
|
|
163
166
|
throw new Error(`unknown task run: ${question.runId}`);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
// A rejected continuation must leave the question answerable. Persist the
|
|
168
|
+
// answer and its queued run together, before either publishes or executes.
|
|
169
|
+
const { reply, continuation } = this.store.transact(() => {
|
|
170
|
+
const reply = this.create(run, "reply", fromSessionId, question.fromSessionId, text, question.id);
|
|
171
|
+
question.state = "answered";
|
|
172
|
+
question.answeredAt = Date.now();
|
|
173
|
+
this.store.saveMessage(question);
|
|
174
|
+
const continuation = isTerminal(run.state)
|
|
175
|
+
? this.prepareResume(run.id, this.format(reply, run), fromSessionId)
|
|
176
|
+
: undefined;
|
|
177
|
+
if (continuation) {
|
|
178
|
+
reply.resumeRunId = continuation.id;
|
|
179
|
+
this.store.saveMessage(reply);
|
|
180
|
+
}
|
|
181
|
+
return { reply, continuation };
|
|
182
|
+
});
|
|
168
183
|
this.changed(question);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
this.changed(reply);
|
|
185
|
+
if (continuation)
|
|
186
|
+
this.startRun(continuation);
|
|
187
|
+
else if (run.targetSessionId)
|
|
172
188
|
this.deliver(reply, run, run.targetSessionId);
|
|
173
|
-
}
|
|
174
|
-
else if (isTerminal(run.state)) {
|
|
175
|
-
// Creating the continuation is not delivering the reply: the run is
|
|
176
|
-
// queued, and a restart or a cancel before it starts loses the prompt
|
|
177
|
-
// that carries the text. So the id is recorded and the sweep below
|
|
178
|
-
// settles it against the continuation's own `startedAt`.
|
|
179
|
-
reply.resumeRunId = this.resumeRun(run.id, this.format(reply, run), fromSessionId).id;
|
|
180
|
-
this.store.saveMessage(reply);
|
|
181
|
-
this.changed(reply);
|
|
182
|
-
}
|
|
183
189
|
return this.require(reply.id);
|
|
184
190
|
}
|
|
185
191
|
create(run, kind, fromSessionId, toSessionId, content, replyTo) {
|
|
@@ -200,7 +206,6 @@ export class TaskMessenger {
|
|
|
200
206
|
nextAttemptAt: null,
|
|
201
207
|
};
|
|
202
208
|
this.store.saveMessage(message);
|
|
203
|
-
this.changed(message);
|
|
204
209
|
return message;
|
|
205
210
|
}
|
|
206
211
|
/** Never awaits the recipient: the seam's `systemInput` settles with the turn
|
|
@@ -377,6 +382,7 @@ export class TaskMessenger {
|
|
|
377
382
|
throw new Error(`unknown task message: ${id}`);
|
|
378
383
|
return message;
|
|
379
384
|
}
|
|
385
|
+
/** Publish only after any transaction changing this message has committed. */
|
|
380
386
|
changed(message) {
|
|
381
387
|
this.hub.emitWorkspace({ type: "task-message-changed", runId: message.runId, messageId: message.id });
|
|
382
388
|
}
|
package/dist/tasks/outbox.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
// this logic was written twice and the two copies drifted — the ceiling was
|
|
4
4
|
// checked in one order here and another there, and only one of them counted a
|
|
5
5
|
// failed attempt.
|
|
6
|
-
import { Router } from "../core/router.js";
|
|
7
6
|
import { logger } from "../log.js";
|
|
8
7
|
import { MAX_DELIVERY_ATTEMPTS, retryDelay, undeliverable } from "./types.js";
|
|
9
8
|
const log = logger("tasks");
|
|
9
|
+
/** The records a delivery names, from either side of the seam: a batch names
|
|
10
|
+
* every one of them, a group names itself. */
|
|
11
|
+
const callbackIds = (origin) => origin?.kind === "task-callback" ? origin.runIds ?? [origin.runId] : [];
|
|
10
12
|
export class Outbox {
|
|
11
13
|
router;
|
|
12
14
|
kind;
|
|
@@ -35,6 +37,7 @@ export class Outbox {
|
|
|
35
37
|
return;
|
|
36
38
|
for (const record of mine)
|
|
37
39
|
this.delivering.add(this.kind.id(record));
|
|
40
|
+
const counted = new Set();
|
|
38
41
|
try {
|
|
39
42
|
const session = await this.router.ensure({ channelId: "task", conversationId: sessionId });
|
|
40
43
|
// The transcript read is both the crash-window dedupe and the proof.
|
|
@@ -46,40 +49,64 @@ export class Outbox {
|
|
|
46
49
|
return;
|
|
47
50
|
// Waiting for a busy target is not a delivery attempt: counting it would
|
|
48
51
|
// inflate the attempts once per second and skip the failure backoff
|
|
49
|
-
// straight to its ceiling.
|
|
50
|
-
|
|
52
|
+
// straight to its ceiling. A record delegated with `steer` is the
|
|
53
|
+
// exception it asked for — it joins the running turn instead, and the
|
|
54
|
+
// rest of the batch keeps waiting for the turn to end. But only once:
|
|
55
|
+
// handed over, a steer sits in Pi's in-memory queue, invisible in the
|
|
56
|
+
// transcript until the turn drains it, so the queue is the second place
|
|
57
|
+
// this has to look before deciding nothing arrived (messages.ts:340).
|
|
58
|
+
const streaming = session.state === "streaming";
|
|
59
|
+
let sending = live;
|
|
60
|
+
if (streaming) {
|
|
61
|
+
const handedOver = await this.queued(session);
|
|
62
|
+
const steerNow = (record) => record.callbackMode === "steer" && !handedOver.has(this.kind.id(record));
|
|
63
|
+
sending = live.filter(steerNow);
|
|
51
64
|
for (const record of live)
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
if (!steerNow(record))
|
|
66
|
+
this.defer(record);
|
|
67
|
+
if (sending.length === 0)
|
|
68
|
+
return;
|
|
54
69
|
}
|
|
55
|
-
for (const record of
|
|
70
|
+
for (const record of sending) {
|
|
56
71
|
this.sent(record);
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
counted.add(this.kind.id(record));
|
|
73
|
+
}
|
|
74
|
+
const { text, origin } = this.kind.input(sending);
|
|
75
|
+
log.debug(`callback for ${sending.map((r) => this.kind.id(r)).join(", ")} → session ${sessionId}`);
|
|
59
76
|
// Not awaited: `systemInput` settles with the recipient's whole turn, and
|
|
60
77
|
// holding the delivery lock that long would keep the proof from ever
|
|
61
78
|
// being read — which is the only thing that marks this delivered.
|
|
62
|
-
session.systemInput(text, origin, "followUp")
|
|
63
|
-
.catch((error) => this.retry(sessionId,
|
|
79
|
+
session.systemInput(text, origin, streaming ? "steer" : "followUp")
|
|
80
|
+
.catch((error) => this.retry(sessionId, sending, error, counted));
|
|
64
81
|
// Pi records the input as it starts the turn, so the proof is usually
|
|
65
82
|
// here already; the tick sweep is the backstop when it is not.
|
|
66
|
-
await this.settle(
|
|
83
|
+
await this.settle(sending, session);
|
|
67
84
|
}
|
|
68
85
|
catch (error) {
|
|
69
|
-
this.retry(sessionId, mine, error);
|
|
86
|
+
this.retry(sessionId, mine, error, counted);
|
|
70
87
|
}
|
|
71
88
|
finally {
|
|
72
89
|
for (const record of mine)
|
|
73
90
|
this.delivering.delete(this.kind.id(record));
|
|
74
91
|
}
|
|
75
92
|
}
|
|
93
|
+
/** Handed over and waiting in the recipient's queue for the running turn to
|
|
94
|
+
* drain it — what the transcript cannot answer yet. Empty unless the
|
|
95
|
+
* session is streaming: Pi drops the list when the turn ends. */
|
|
96
|
+
async queued(session) {
|
|
97
|
+
const ids = new Set();
|
|
98
|
+
for (const origin of await session.pendingSystemInputs())
|
|
99
|
+
for (const id of callbackIds(origin))
|
|
100
|
+
ids.add(id);
|
|
101
|
+
return ids;
|
|
102
|
+
}
|
|
76
103
|
/** Marks every record the transcript proves; returns the ones it does not. */
|
|
77
104
|
async settle(records, session) {
|
|
78
105
|
const seen = new Set();
|
|
79
106
|
for (const turn of await session.history()) {
|
|
80
|
-
if (turn.role !== "system"
|
|
107
|
+
if (turn.role !== "system")
|
|
81
108
|
continue;
|
|
82
|
-
for (const id of turn.origin
|
|
109
|
+
for (const id of callbackIds(turn.origin))
|
|
83
110
|
seen.add(id);
|
|
84
111
|
}
|
|
85
112
|
const unproven = [];
|
|
@@ -95,14 +122,15 @@ export class Outbox {
|
|
|
95
122
|
/** The recipient is waiting for an answer that is now late: the retry itself
|
|
96
123
|
* is silent, so this line is the only sign it is being retried — and the
|
|
97
124
|
* ceiling is what ends the retrying out loud. */
|
|
98
|
-
retry(sessionId, batch, error) {
|
|
125
|
+
retry(sessionId, batch, error, counted) {
|
|
99
126
|
log.warn(`callback to session ${sessionId} failed, will retry`, error);
|
|
100
127
|
for (const stale of batch) {
|
|
101
128
|
const record = this.kind.reload(this.kind.id(stale));
|
|
102
129
|
// Already proven delivered, or already given up on: not a failure.
|
|
103
130
|
if (!record || (record.callbackState !== "pending" && record.callbackState !== "failed"))
|
|
104
131
|
continue;
|
|
105
|
-
this.failed(record, error);
|
|
132
|
+
this.failed(record, error, counted.has(this.kind.id(record)));
|
|
133
|
+
counted.add(this.kind.id(record));
|
|
106
134
|
this.spent(record, sessionId);
|
|
107
135
|
}
|
|
108
136
|
}
|
|
@@ -140,11 +168,10 @@ export class Outbox {
|
|
|
140
168
|
record.callbackNextAttemptAt = null;
|
|
141
169
|
this.write(record);
|
|
142
170
|
}
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
record.callbackAttempts += 1;
|
|
171
|
+
/** A failed pass costs one attempt, whether it died before or after send. */
|
|
172
|
+
failed(record, error, counted) {
|
|
173
|
+
if (!counted)
|
|
174
|
+
record.callbackAttempts += 1;
|
|
148
175
|
record.callbackState = "failed";
|
|
149
176
|
record.callbackError = String(error);
|
|
150
177
|
record.callbackNextAttemptAt = Date.now() + retryDelay(record.callbackAttempts);
|
package/dist/tasks/routes.js
CHANGED
|
@@ -51,10 +51,7 @@ export function registerTaskRoutes(app, tasks, activity) {
|
|
|
51
51
|
const now = Date.now();
|
|
52
52
|
const recent = c.req.query("scope") === "recent";
|
|
53
53
|
const windowStart = now - 24 * 60 * 60 * 1000;
|
|
54
|
-
|
|
55
|
-
// last 24h no matter when it was queued, so it never drops a live run.
|
|
56
|
-
const runs = tasks.recentRuns(200).filter((run) => run.state === "queued" || run.state === "running" ||
|
|
57
|
-
(recent && run.queuedAt >= windowStart));
|
|
54
|
+
const runs = tasks.activityRuns(recent ? windowStart : undefined);
|
|
58
55
|
const listed = await activity.factory.list();
|
|
59
56
|
const byId = new Map(listed.map((session) => [session.id, session]));
|
|
60
57
|
const linkedIds = new Set();
|
package/dist/tasks/runs.js
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
// exploding (depth, children per root) are decided here, once, because every
|
|
5
5
|
// caller — scheduler, tool, HTTP — enqueues through this one door.
|
|
6
6
|
import { logger } from "../log.js";
|
|
7
|
-
import { TaskCallbacks } from "./callbacks.js";
|
|
8
7
|
import { newId } from "./definitions.js";
|
|
9
|
-
import { TaskStore } from "./store.js";
|
|
10
8
|
const log = logger("tasks");
|
|
11
9
|
const MAX_DEPTH = 2;
|
|
12
10
|
const MAX_CHILDREN_PER_ROOT = 16;
|
|
@@ -23,7 +21,8 @@ export class TaskRunQueue {
|
|
|
23
21
|
this.execute = execute;
|
|
24
22
|
this.changed = changed;
|
|
25
23
|
}
|
|
26
|
-
|
|
24
|
+
/** Validate and persist only; callers may stage several runs in a transaction. */
|
|
25
|
+
prepare(definition, input, source, parentRunId, provenance) {
|
|
27
26
|
const id = newId();
|
|
28
27
|
const parent = parentRunId ? this.getRun(parentRunId) : null;
|
|
29
28
|
const depth = provenance.depth ?? (parent ? parent.depth + 1 : 0);
|
|
@@ -63,6 +62,7 @@ export class TaskRunQueue {
|
|
|
63
62
|
targetSessionId,
|
|
64
63
|
sessionMode,
|
|
65
64
|
callbackSessionId,
|
|
65
|
+
...(provenance.callbackMode === "steer" ? { callbackMode: "steer" } : {}),
|
|
66
66
|
background: provenance.background ?? false,
|
|
67
67
|
callbackState: overlapped && callbackSessionId ? "pending" : null,
|
|
68
68
|
callbackAttempts: 0,
|
|
@@ -84,7 +84,14 @@ export class TaskRunQueue {
|
|
|
84
84
|
finishedAt: overlapped ? now : null,
|
|
85
85
|
};
|
|
86
86
|
this.store.saveRun(run);
|
|
87
|
+
return run;
|
|
88
|
+
}
|
|
89
|
+
/** Publish and execute only after all records this run relies on committed. */
|
|
90
|
+
start(run) {
|
|
87
91
|
this.changed(run);
|
|
92
|
+
const { id, depth, triggerSource: source } = run;
|
|
93
|
+
const overlapped = run.skipReason === "overlap";
|
|
94
|
+
const definition = run.context.definition;
|
|
88
95
|
// Why a run exists is the first question asked of a surprising one, and it
|
|
89
96
|
// is answerable only here: the row keeps the ids, not the reason. A watch
|
|
90
97
|
// probe queues on every interval and mostly matches nothing, so it says so
|
|
@@ -99,6 +106,5 @@ export class TaskRunQueue {
|
|
|
99
106
|
this.execute(run);
|
|
100
107
|
else if (run.callbackState === "pending")
|
|
101
108
|
void this.callbacks.deliver(run);
|
|
102
|
-
return run;
|
|
103
109
|
}
|
|
104
110
|
}
|
package/dist/tasks/service.js
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
// like it makes belongs to a file beside it (definitions, runs, execution,
|
|
5
5
|
// groups, messages, callbacks) — what is genuinely here is scheduling and the
|
|
6
6
|
// facade, so the HTTP routes and the task tool cannot drift apart.
|
|
7
|
-
import { EventHub } from "../core/hub.js";
|
|
8
|
-
import { Router } from "../core/router.js";
|
|
9
7
|
import { logger } from "../log.js";
|
|
10
8
|
import { AgentTaskRunner } from "./agent.js";
|
|
11
9
|
import { TaskCallbacks } from "./callbacks.js";
|
|
@@ -14,7 +12,6 @@ import { TaskExecution } from "./execution.js";
|
|
|
14
12
|
import { TaskGroups } from "./groups.js";
|
|
15
13
|
import { TaskMessenger } from "./messages.js";
|
|
16
14
|
import { TaskRunQueue } from "./runs.js";
|
|
17
|
-
import { TaskStore } from "./store.js";
|
|
18
15
|
import { handleTaskTool } from "./tool.js";
|
|
19
16
|
import { isTerminal } from "./types.js";
|
|
20
17
|
const log = logger("tasks");
|
|
@@ -52,20 +49,21 @@ export class TaskService {
|
|
|
52
49
|
this.hub = hub;
|
|
53
50
|
this.instance = instance;
|
|
54
51
|
const unreachable = (sessionId, what, why) => this.unreachable(sessionId, what, why);
|
|
55
|
-
this.messages = new TaskMessenger(store, router, hub, (runId, prompt, fromSessionId) => this.
|
|
52
|
+
this.messages = new TaskMessenger(store, router, hub, (runId, prompt, fromSessionId) => this.prepareResume(runId, prompt, { invokedBySessionId: fromSessionId, callbackSessionId: fromSessionId, background: true }), unreachable, (run) => this.runs.start(run));
|
|
56
53
|
this.definitions = new TaskDefinitions(store, factory, router, hub, instance?.systemActions);
|
|
57
54
|
this.callbacks = new TaskCallbacks(store, router, (run) => this.changed(run), unreachable);
|
|
58
55
|
this.groups = new TaskGroups(store, router, {
|
|
59
56
|
getRun: (id) => this.getRun(id),
|
|
60
57
|
cancel: (id) => { this.cancel(id); },
|
|
61
58
|
openDecisionId: (runId) => this.messages.openDecisionId(runId),
|
|
62
|
-
|
|
59
|
+
prepareMember: (taskId, groupId, callerSessionId, parentRunId) => this.prepareRun(taskId, null, "agent", parentRunId, {
|
|
63
60
|
invokedBySessionId: callerSessionId,
|
|
64
61
|
sourceSessionId: callerSessionId,
|
|
65
62
|
callbackSessionId: null,
|
|
66
63
|
background: true,
|
|
67
64
|
groupId,
|
|
68
65
|
}),
|
|
66
|
+
startMember: (run) => this.runs.start(run),
|
|
69
67
|
}, (group) => this.hub.emitWorkspace({ type: "task-group-changed", groupId: group.id }), unreachable);
|
|
70
68
|
const agent = new AgentTaskRunner(factory, router, store, this.messages, (run) => this.changed(run));
|
|
71
69
|
this.execution = new TaskExecution(store, this.definitions, this.callbacks, agent, {
|
|
@@ -198,8 +196,8 @@ export class TaskService {
|
|
|
198
196
|
queryRuns(query = {}) {
|
|
199
197
|
return this.store.queryRuns(query);
|
|
200
198
|
}
|
|
201
|
-
|
|
202
|
-
return this.store.
|
|
199
|
+
activityRuns(since) {
|
|
200
|
+
return this.store.activityRuns(since);
|
|
203
201
|
}
|
|
204
202
|
recentMessages(since) {
|
|
205
203
|
return this.messages.recent(since);
|
|
@@ -222,13 +220,18 @@ export class TaskService {
|
|
|
222
220
|
return this.store.taskOwnedSessionIds();
|
|
223
221
|
}
|
|
224
222
|
run(taskId, input = null, source = "manual", parentRunId = null, provenance = {}) {
|
|
223
|
+
const run = this.prepareRun(taskId, input, source, parentRunId, provenance);
|
|
224
|
+
this.runs.start(run);
|
|
225
|
+
return run;
|
|
226
|
+
}
|
|
227
|
+
prepareRun(taskId, input, source, parentRunId, provenance) {
|
|
225
228
|
this.refusePaused(parentRunId);
|
|
226
229
|
const task = this.get(taskId);
|
|
227
230
|
// `enabled:false` pauses scheduling only; manual and agent triggers still
|
|
228
231
|
// run a paused task on demand. Archiving is the terminal state.
|
|
229
232
|
if (task.archived)
|
|
230
233
|
throw new Error("archived tasks cannot run");
|
|
231
|
-
return this.runs.
|
|
234
|
+
return this.runs.prepare(task, input, source, parentRunId, provenance);
|
|
232
235
|
}
|
|
233
236
|
async waitForRun(id) {
|
|
234
237
|
const current = this.getRun(id);
|
|
@@ -256,9 +259,9 @@ export class TaskService {
|
|
|
256
259
|
getGroup(id) {
|
|
257
260
|
return this.groups.members(id);
|
|
258
261
|
}
|
|
259
|
-
runGroup(definitions, join, callerSessionId, parentRunId, callbackSessionId) {
|
|
262
|
+
runGroup(definitions, join, callerSessionId, parentRunId, callbackSessionId, callbackMode) {
|
|
260
263
|
this.refusePaused(parentRunId);
|
|
261
|
-
return this.groups.runAll(definitions, join, callerSessionId, parentRunId, callbackSessionId);
|
|
264
|
+
return this.groups.runAll(definitions, join, callerSessionId, parentRunId, callbackSessionId, callbackMode);
|
|
262
265
|
}
|
|
263
266
|
descendants(run) {
|
|
264
267
|
const byParent = new Map();
|
|
@@ -291,6 +294,16 @@ export class TaskService {
|
|
|
291
294
|
return this.messages.reply(messageId, fromSessionId, message);
|
|
292
295
|
}
|
|
293
296
|
resume(id, message, provenance = {}) {
|
|
297
|
+
const { run, expired } = this.store.transact(() => ({
|
|
298
|
+
run: this.prepareResume(id, message, provenance),
|
|
299
|
+
expired: this.messages.expireDecisions(id, "superseded by a manual resume"),
|
|
300
|
+
}));
|
|
301
|
+
for (const message of expired)
|
|
302
|
+
this.messages.changed(message);
|
|
303
|
+
this.runs.start(run);
|
|
304
|
+
return run;
|
|
305
|
+
}
|
|
306
|
+
prepareResume(id, message, provenance) {
|
|
294
307
|
this.refusePaused();
|
|
295
308
|
const prior = this.getRun(id);
|
|
296
309
|
if (!isTerminal(prior.state))
|
|
@@ -299,8 +312,7 @@ export class TaskService {
|
|
|
299
312
|
throw new Error("only persisted Agent runs can be resumed");
|
|
300
313
|
}
|
|
301
314
|
const prompt = requiredString(message, "message");
|
|
302
|
-
this.
|
|
303
|
-
return this.runs.enqueue(prior.context.definition, null, "agent", null, {
|
|
315
|
+
return this.runs.prepare(prior.context.definition, null, "agent", null, {
|
|
304
316
|
...provenance,
|
|
305
317
|
sourceSessionId: provenance.invokedBySessionId ?? prior.invokedBySessionId,
|
|
306
318
|
targetSessionId: prior.targetSessionId,
|
package/dist/tasks/store.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// runs, groups and messages are rows here, read and written through one
|
|
3
3
|
// connection db.ts opened. A store owns its queries, never its own tables or
|
|
4
4
|
// its own handle — the schema is db.ts's migration list.
|
|
5
|
-
import { pierDb, statements } from "../db.js";
|
|
5
|
+
import { pierDb, statements, transact } from "../db.js";
|
|
6
6
|
const clamp = (limit, cap) => Math.min(Math.max(limit, 1), cap);
|
|
7
7
|
export class TaskStore {
|
|
8
8
|
db;
|
|
@@ -12,6 +12,10 @@ export class TaskStore {
|
|
|
12
12
|
this.db = db;
|
|
13
13
|
this.sql = statements(db);
|
|
14
14
|
}
|
|
15
|
+
/** Commit related task records before publishing events or starting work. */
|
|
16
|
+
transact(work) {
|
|
17
|
+
return transact(this.db, work);
|
|
18
|
+
}
|
|
15
19
|
// Every table is one JSON column plus query columns; these two are the only
|
|
16
20
|
// readers, and the one seam where a future schema change normalizes old rows
|
|
17
21
|
// (pre-v1 databases are refused outright in db.ts).
|
|
@@ -101,8 +105,18 @@ export class TaskStore {
|
|
|
101
105
|
nextCursor: rows.length > limit && last ? { queuedAt: last.queuedAt, id: last.id } : null,
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
/** Activity never limits live work; only its optional history is bounded. */
|
|
109
|
+
activityRuns(since) {
|
|
110
|
+
const active = this.#many("SELECT json FROM task_runs WHERE state IN ('queued', 'running') ORDER BY queued_at DESC, id DESC");
|
|
111
|
+
if (since === undefined)
|
|
112
|
+
return active;
|
|
113
|
+
const recent = this.#many(`
|
|
114
|
+
SELECT json FROM task_runs
|
|
115
|
+
WHERE state NOT IN ('queued', 'running') AND queued_at >= ?
|
|
116
|
+
AND NOT (state = 'succeeded' AND json_extract(json, '$.matched') IS 0)
|
|
117
|
+
ORDER BY queued_at DESC, id DESC LIMIT 200
|
|
118
|
+
`, since);
|
|
119
|
+
return [...active, ...recent].sort((a, b) => b.queuedAt - a.queuedAt || b.id.localeCompare(a.id));
|
|
106
120
|
}
|
|
107
121
|
listRunsByRoot(rootRunId, limit = 100) {
|
|
108
122
|
return this.#many(`
|