@timqi/pier 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -125
- package/dist/agent/config.js +6 -15
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +42 -64
- package/dist/agent/listing.js +39 -92
- package/dist/agent/pi.js +103 -252
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +7 -23
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +12 -34
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -264
- package/dist/core/types.js +4 -0
- package/dist/db.js +88 -272
- package/dist/drain.js +57 -50
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +4 -12
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +84 -183
- package/dist/paths.js +10 -26
- package/dist/secrets.js +18 -45
- package/dist/service.js +31 -73
- package/dist/settings.js +19 -63
- package/dist/tasks/agent.js +24 -45
- package/dist/tasks/callbacks.js +8 -16
- package/dist/tasks/command.js +2 -6
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +41 -39
- package/dist/tasks/groups.js +8 -11
- package/dist/tasks/messages.js +88 -155
- package/dist/tasks/outbox.js +33 -54
- package/dist/tasks/routes.js +4 -7
- package/dist/tasks/runs.js +4 -9
- package/dist/tasks/service.js +29 -42
- package/dist/tasks/store.js +36 -27
- package/dist/tasks/tool.js +59 -60
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +98 -325
- package/dist/update.js +20 -43
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +35 -82
- package/dist/web/providers.js +5 -11
- package/dist/web/public/assets/{activity-Bl3vZukb.js → activity-B89_hH7q.js} +1 -1
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/{boards-DYuf4Mlj.js → boards-BeKW0ZXK.js} +1 -1
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/{runs-BLJu7EXN.js → runs-Cwy0mN8i.js} +1 -1
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/{settings-BrdVh-Zi.js → settings-DzZLmujq.js} +1 -1
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-CeQS1rxa.js → task-runs-BCakxFk8.js} +1 -1
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +30 -16
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +109 -190
- package/dist/web/session-state.js +13 -53
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +1 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +23 -15
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- 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.br +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js +0 -4
- 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 +0 -85
- 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 +0 -2
- 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.br +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -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.br +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js +0 -4
- package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
package/dist/tasks/messages.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
// What a parent and a child say to each other while a run is going
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// finished — an undelivered message is retried, expired and *said*, never
|
|
6
|
-
// dropped (§5b).
|
|
7
|
-
import { logger } from "../log.js";
|
|
1
|
+
// What a parent and a child say to each other while a run is going. Every
|
|
2
|
+
// message is a durable row before it is a delivery: either end may be mid-turn
|
|
3
|
+
// or gone, and an undelivered message is retried, expired and said, never
|
|
4
|
+
// dropped (§5). Delivery itself belongs to outbox.ts.
|
|
8
5
|
import { runSource } from "./callbacks.js";
|
|
9
6
|
import { newId } from "./definitions.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
7
|
+
import { Outbox } from "./outbox.js";
|
|
8
|
+
import { isTerminal } from "./types.js";
|
|
12
9
|
const MAX_MESSAGE_LENGTH = 16 * 1024;
|
|
10
|
+
/** `answered` is a delivery that was read; `expired` is one given up on. */
|
|
11
|
+
const ENGINE_STATE = {
|
|
12
|
+
pending: "pending", failed: "failed", delivered: "delivered", answered: "delivered", expired: "abandoned",
|
|
13
|
+
};
|
|
13
14
|
function bounded(content) {
|
|
14
15
|
const text = content.trim();
|
|
15
16
|
if (!text)
|
|
@@ -20,22 +21,56 @@ function bounded(content) {
|
|
|
20
21
|
}
|
|
21
22
|
export class TaskMessenger {
|
|
22
23
|
store;
|
|
23
|
-
router;
|
|
24
24
|
hub;
|
|
25
25
|
prepareResume;
|
|
26
26
|
unreachable;
|
|
27
27
|
startRun;
|
|
28
|
+
outbox;
|
|
28
29
|
constructor(store, router, hub,
|
|
29
30
|
/** Prepares a continuation; it starts only after the reply commits. */
|
|
30
31
|
prepareResume,
|
|
31
32
|
/** Reports a delivery nobody can complete (service.ts owns the surfaces). */
|
|
32
33
|
unreachable, startRun) {
|
|
33
34
|
this.store = store;
|
|
34
|
-
this.router = router;
|
|
35
35
|
this.hub = hub;
|
|
36
36
|
this.prepareResume = prepareResume;
|
|
37
37
|
this.unreachable = unreachable;
|
|
38
38
|
this.startRun = startRun;
|
|
39
|
+
this.outbox = new Outbox(router, {
|
|
40
|
+
id: ({ message }) => message.id,
|
|
41
|
+
reload: (id) => {
|
|
42
|
+
const message = this.store.getMessage(id);
|
|
43
|
+
return message && this.carry(message);
|
|
44
|
+
},
|
|
45
|
+
save: ({ message, callbackState, callbackAttempts, callbackError, callbackNextAttemptAt }) => {
|
|
46
|
+
message.attempts = callbackAttempts;
|
|
47
|
+
message.error = callbackError;
|
|
48
|
+
message.nextAttemptAt = callbackNextAttemptAt;
|
|
49
|
+
if (callbackState === "delivered") {
|
|
50
|
+
message.state = "delivered";
|
|
51
|
+
message.deliveredAt = Date.now();
|
|
52
|
+
}
|
|
53
|
+
else if (callbackState === "abandoned") {
|
|
54
|
+
message.state = "expired";
|
|
55
|
+
message.answeredAt = Date.now();
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
message.state = callbackState ?? "pending";
|
|
59
|
+
this.store.saveMessage(message);
|
|
60
|
+
},
|
|
61
|
+
changed: ({ message }) => this.changed(message),
|
|
62
|
+
input: (records) => {
|
|
63
|
+
const message = records[0].message;
|
|
64
|
+
const run = this.store.getRun(message.runId);
|
|
65
|
+
if (!run)
|
|
66
|
+
throw new Error(`unknown task run: ${message.runId}`);
|
|
67
|
+
return { text: this.format(message, run), origin: this.origin(message, run) };
|
|
68
|
+
},
|
|
69
|
+
abandoned: ({ message }, _sessionId, why) => this.told(message, why),
|
|
70
|
+
// A follow-up joins the recipient's queue now: the run it guides may end
|
|
71
|
+
// with the turn it would otherwise wait out.
|
|
72
|
+
queues: true,
|
|
73
|
+
});
|
|
39
74
|
}
|
|
40
75
|
expirePending() {
|
|
41
76
|
for (const message of this.store.expirePendingMessages())
|
|
@@ -70,7 +105,7 @@ export class TaskMessenger {
|
|
|
70
105
|
const message = this.create(run, kind, fromSessionId, run.targetSessionId ?? "", content, null);
|
|
71
106
|
this.changed(message);
|
|
72
107
|
if (run.targetSessionId)
|
|
73
|
-
this.deliver(message, run
|
|
108
|
+
this.deliver(message, run.targetSessionId);
|
|
74
109
|
return this.require(message.id);
|
|
75
110
|
}
|
|
76
111
|
deliverPendingControls(run) {
|
|
@@ -79,12 +114,11 @@ export class TaskMessenger {
|
|
|
79
114
|
for (const message of this.store.listMessages(run.id)) {
|
|
80
115
|
if (message.state !== "pending" || (message.kind !== "steer" && message.kind !== "follow_up"))
|
|
81
116
|
continue;
|
|
82
|
-
this.deliver(message, run
|
|
117
|
+
this.deliver(message, run.targetSessionId);
|
|
83
118
|
}
|
|
84
119
|
}
|
|
85
|
-
/**
|
|
86
|
-
*
|
|
87
|
-
* deliver. Controls aimed at a finished run are dead and expire here. */
|
|
120
|
+
/** Delivery is fire-and-forget, so this sweep is what closes a failed one.
|
|
121
|
+
* Controls aimed at a finished run are dead and expire here. */
|
|
88
122
|
retryUndelivered(now = Date.now()) {
|
|
89
123
|
for (const { message, run } of this.store.listUndeliveredMessages()) {
|
|
90
124
|
if (!run)
|
|
@@ -97,26 +131,19 @@ export class TaskMessenger {
|
|
|
97
131
|
this.changed(message);
|
|
98
132
|
continue;
|
|
99
133
|
}
|
|
100
|
-
// A reply that resumed a terminal run
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
// continuation is the proof instead. `startedAt` is written one statement
|
|
104
|
-
// before the prompt reaches the session (agent.ts), so `delivered` here
|
|
105
|
-
// means "handed to the run that reports for it" — a `systemInput` that
|
|
106
|
-
// then throws fails *that* run, and the failure reaches the replier
|
|
107
|
-
// through its callback.
|
|
134
|
+
// A reply that resumed a terminal run is the continuation's prompt, not a
|
|
135
|
+
// system input: the transcript read would re-inject it and start a turn no
|
|
136
|
+
// run owns. The continuation's `startedAt` is the proof instead.
|
|
108
137
|
if (message.kind === "reply" && isTerminal(run.state)) {
|
|
109
138
|
const resumed = message.resumeRunId ? this.store.getRun(message.resumeRunId) : undefined;
|
|
110
139
|
if (resumed?.startedAt)
|
|
111
|
-
this.
|
|
140
|
+
this.delivered(message);
|
|
112
141
|
else if (!resumed)
|
|
113
142
|
this.abandon(message, `continuation ${message.resumeRunId ?? "(none)"} is gone`);
|
|
114
143
|
else if (isTerminal(resumed.state)) {
|
|
115
|
-
// Never started
|
|
116
|
-
// ceiling
|
|
117
|
-
//
|
|
118
|
-
// continuation's own cancelled callback, and a duplicate beats a
|
|
119
|
-
// special case that could suppress the only report either gets.
|
|
144
|
+
// Never started and never will: said now, not four minutes later at
|
|
145
|
+
// the ceiling. A duplicate report beats a special case that could
|
|
146
|
+
// suppress the only one.
|
|
120
147
|
this.abandon(message, `continuation ${resumed.id} ${resumed.state} before it started`);
|
|
121
148
|
}
|
|
122
149
|
continue;
|
|
@@ -125,15 +152,12 @@ export class TaskMessenger {
|
|
|
125
152
|
continue;
|
|
126
153
|
const target = message.toSessionId || run.targetSessionId;
|
|
127
154
|
if (target)
|
|
128
|
-
this.deliver(message,
|
|
155
|
+
this.deliver(message, target);
|
|
129
156
|
}
|
|
130
157
|
}
|
|
131
|
-
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* A decision steers the supervisor: a follow-up only lands once the
|
|
135
|
-
* supervisor has no tool calls left, so a blocked child would wait out the
|
|
136
|
-
* whole turn. Progress stays a follow-up — nobody waits on it. */
|
|
158
|
+
/** Returns the receipt immediately; the reply arrives as a follow-up (active
|
|
159
|
+
* run) or resumes the session (terminal run). A decision steers the
|
|
160
|
+
* supervisor, or a blocked child would wait out its whole turn. */
|
|
137
161
|
async contact(run, fromSessionId, reason, content) {
|
|
138
162
|
if (!run.invokedBySessionId)
|
|
139
163
|
throw new Error("run has no supervisor session");
|
|
@@ -142,7 +166,7 @@ export class TaskMessenger {
|
|
|
142
166
|
}
|
|
143
167
|
const message = this.create(run, reason, fromSessionId, run.invokedBySessionId, content, null);
|
|
144
168
|
this.changed(message);
|
|
145
|
-
this.deliver(message, run
|
|
169
|
+
this.deliver(message, run.invokedBySessionId);
|
|
146
170
|
return this.require(message.id);
|
|
147
171
|
}
|
|
148
172
|
async reply(questionId, fromSessionId, content) {
|
|
@@ -185,7 +209,7 @@ export class TaskMessenger {
|
|
|
185
209
|
if (continuation)
|
|
186
210
|
this.startRun(continuation);
|
|
187
211
|
else if (run.targetSessionId)
|
|
188
|
-
this.deliver(reply, run
|
|
212
|
+
this.deliver(reply, run.targetSessionId);
|
|
189
213
|
return this.require(reply.id);
|
|
190
214
|
}
|
|
191
215
|
create(run, kind, fromSessionId, toSessionId, content, replyTo) {
|
|
@@ -208,30 +232,28 @@ export class TaskMessenger {
|
|
|
208
232
|
this.store.saveMessage(message);
|
|
209
233
|
return message;
|
|
210
234
|
}
|
|
211
|
-
/**
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
* So `delivered` is written by `confirmed`, against the one proof that
|
|
215
|
-
* survives an abort or a restart: the message visible in the recipient's own
|
|
216
|
-
* transcript. Until then it stays pending and the sweep tries again. */
|
|
217
|
-
deliver(candidate, run, targetSessionId) {
|
|
218
|
-
const message = this.require(candidate.id);
|
|
219
|
-
if (message.state !== "pending" && message.state !== "failed")
|
|
220
|
-
return;
|
|
221
|
-
if (message.toSessionId !== targetSessionId)
|
|
235
|
+
/** A control created before its run had a session is aimed once it has one. */
|
|
236
|
+
deliver(message, targetSessionId) {
|
|
237
|
+
if (message.toSessionId !== targetSessionId) {
|
|
222
238
|
message.toSessionId = targetSessionId;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
this.
|
|
226
|
-
this.changed(message);
|
|
227
|
-
void this.inject(message, run, targetSessionId, this.mode(message))
|
|
228
|
-
.catch((error) => log.error(`message ${message.id} delivery collapsed`, error));
|
|
239
|
+
this.store.saveMessage(message);
|
|
240
|
+
}
|
|
241
|
+
void this.outbox.deliver(targetSessionId, [this.carry(message)]);
|
|
229
242
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
243
|
+
carry(message) {
|
|
244
|
+
return {
|
|
245
|
+
message,
|
|
246
|
+
callbackState: ENGINE_STATE[message.state],
|
|
247
|
+
callbackAttempts: message.attempts,
|
|
248
|
+
callbackError: message.error,
|
|
249
|
+
callbackNextAttemptAt: message.nextAttemptAt,
|
|
250
|
+
// Steer whatever someone is blocked on: a follow-up lands only once the
|
|
251
|
+
// recipient runs out of tool calls. Progress is a follow-up because nobody waits.
|
|
252
|
+
...(message.kind === "follow_up" || message.kind === "progress" ? {} : { callbackMode: "steer" }),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Proven by something other than the recipient's transcript. */
|
|
256
|
+
delivered(message) {
|
|
235
257
|
message.state = "delivered";
|
|
236
258
|
message.deliveredAt = Date.now();
|
|
237
259
|
message.error = null;
|
|
@@ -239,37 +261,6 @@ export class TaskMessenger {
|
|
|
239
261
|
this.store.saveMessage(message);
|
|
240
262
|
this.changed(message);
|
|
241
263
|
}
|
|
242
|
-
/** Waiting is not a failed attempt: the message is in flight or the turn it
|
|
243
|
-
* has to wait for is still running, so it is tried again shortly and the
|
|
244
|
-
* ceiling is left for the deliveries that actually failed (the rule
|
|
245
|
-
* outbox.ts keeps for callbacks). */
|
|
246
|
-
defer(id) {
|
|
247
|
-
const message = this.store.getMessage(id);
|
|
248
|
-
if (!message || message.state !== "pending")
|
|
249
|
-
return;
|
|
250
|
-
message.nextAttemptAt = Date.now() + 1000;
|
|
251
|
-
this.store.saveMessage(message);
|
|
252
|
-
}
|
|
253
|
-
/** Counts one hand-off and says whether it may happen: a recipient that
|
|
254
|
-
* never records the message must not be re-sent once a second forever.
|
|
255
|
-
* False means the ceiling was reached and the message is now expired. */
|
|
256
|
-
spend(id) {
|
|
257
|
-
const message = this.store.getMessage(id);
|
|
258
|
-
if (!message || message.state !== "pending")
|
|
259
|
-
return false;
|
|
260
|
-
message.attempts += 1;
|
|
261
|
-
message.nextAttemptAt = Date.now() + retryDelay(message.attempts);
|
|
262
|
-
if (message.attempts > MAX_DELIVERY_ATTEMPTS) {
|
|
263
|
-
this.abandon(message, undeliverable(message.attempts - 1, message.error));
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
this.store.saveMessage(message);
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
/** Out of attempts. Both ends are told — the recipient that was owed it and
|
|
270
|
-
* the sender waiting on the answer — and a decision that expires here stops
|
|
271
|
-
* suppressing its run's completion callback, which `execution.ts` decided
|
|
272
|
-
* once, at the end of the run, and never revisits. */
|
|
273
264
|
abandon(message, why) {
|
|
274
265
|
message.state = "expired";
|
|
275
266
|
message.error = why;
|
|
@@ -277,6 +268,11 @@ export class TaskMessenger {
|
|
|
277
268
|
message.nextAttemptAt = null;
|
|
278
269
|
this.store.saveMessage(message);
|
|
279
270
|
this.changed(message);
|
|
271
|
+
this.told(message, why);
|
|
272
|
+
}
|
|
273
|
+
/** Both ends are told, and an expired decision stops suppressing its run's
|
|
274
|
+
* completion callback, which `execution.ts` decided once and never revisits. */
|
|
275
|
+
told(message, why) {
|
|
280
276
|
this.unreachable(message.toSessionId, `a ${message.kind} from run ${message.runId}`, why);
|
|
281
277
|
if (message.fromSessionId && message.fromSessionId !== message.toSessionId) {
|
|
282
278
|
this.unreachable(message.fromSessionId, `your ${message.kind} on run ${message.runId}`, why);
|
|
@@ -289,69 +285,6 @@ export class TaskMessenger {
|
|
|
289
285
|
this.store.saveRun(run);
|
|
290
286
|
}
|
|
291
287
|
}
|
|
292
|
-
/** Steer whatever someone is blocked on: a decision, and the reply that
|
|
293
|
-
* answers it — a follow-up lands only once the recipient runs out of tool
|
|
294
|
-
* calls, so each would wait out a whole turn (the reply waited 3 minutes
|
|
295
|
-
* behind one on 2026-09-07). Progress is a follow-up because nobody waits. */
|
|
296
|
-
mode(message) {
|
|
297
|
-
return message.kind === "follow_up" || message.kind === "progress" ? "follow_up" : "steer";
|
|
298
|
-
}
|
|
299
|
-
failed(id, error, spent) {
|
|
300
|
-
const message = this.store.getMessage(id);
|
|
301
|
-
// Only a still-pending message can fail: a late rejection must not undo a
|
|
302
|
-
// delivery a newer attempt already proved, nor revive an expired one.
|
|
303
|
-
if (message?.state !== "pending")
|
|
304
|
-
return;
|
|
305
|
-
// Both ends are waiting on this one: the sender for an answer, the
|
|
306
|
-
// recipient for a message it never got told about.
|
|
307
|
-
log.warn(`${message.kind} ${id} to session ${message.toSessionId} failed`, error);
|
|
308
|
-
message.state = "failed";
|
|
309
|
-
message.error = String(error);
|
|
310
|
-
// A pass that died before the send never spent its attempt — an
|
|
311
|
-
// unresolvable target dies there every time, and would retry forever.
|
|
312
|
-
if (!spent)
|
|
313
|
-
message.attempts += 1;
|
|
314
|
-
message.nextAttemptAt = Date.now() + retryDelay(message.attempts);
|
|
315
|
-
this.store.saveMessage(message);
|
|
316
|
-
this.changed(message);
|
|
317
|
-
if (message.attempts > MAX_DELIVERY_ATTEMPTS) {
|
|
318
|
-
this.abandon(message, undeliverable(message.attempts - 1, message.error));
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
/** One pass at getting the message into the recipient: the dedupe on a retry
|
|
322
|
-
* and the proof of delivery are the same transcript read. Owns its own
|
|
323
|
-
* failure, so an attempt is counted exactly once whether the pass died
|
|
324
|
-
* before the send or the send itself was refused. */
|
|
325
|
-
async inject(message, run, targetSessionId, mode) {
|
|
326
|
-
let spent = false;
|
|
327
|
-
try {
|
|
328
|
-
const session = await this.router.ensure({ channelId: "task", conversationId: targetSessionId });
|
|
329
|
-
const recorded = async () => (await session.history()).some((turn) => turn.role === "system" && turn.origin?.kind === "task-message" && turn.origin.messageId === message.id);
|
|
330
|
-
if (await recorded())
|
|
331
|
-
return this.confirmed(message.id);
|
|
332
|
-
// Accepted and waiting in the recipient's queue for the running turn to
|
|
333
|
-
// drain it: not recorded yet, and sending again would deliver the same
|
|
334
|
-
// guidance twice. Unlike a callback this is not deferred on a busy
|
|
335
|
-
// target — a steer's whole point is to reach the turn already running —
|
|
336
|
-
// so the queue is where it sits, and waiting for it costs no attempt.
|
|
337
|
-
if (await this.queued(session, message.id))
|
|
338
|
-
return this.defer(message.id);
|
|
339
|
-
spent = this.spend(message.id);
|
|
340
|
-
if (!spent)
|
|
341
|
-
return;
|
|
342
|
-
await session.systemInput(this.format(message, run), this.origin(message, run), mode === "follow_up" ? "followUp" : mode);
|
|
343
|
-
if (await recorded())
|
|
344
|
-
this.confirmed(message.id);
|
|
345
|
-
}
|
|
346
|
-
catch (error) {
|
|
347
|
-
this.failed(message.id, error, spent);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
/** In the recipient's queue, by the id its origin carries — the transcript
|
|
351
|
-
* read above answers "landed", this answers "handed over and waiting". */
|
|
352
|
-
async queued(session, id) {
|
|
353
|
-
return (await session.pendingSystemInputs()).some((origin) => origin.kind === "task-message" && origin.messageId === id);
|
|
354
|
-
}
|
|
355
288
|
format(message, run) {
|
|
356
289
|
const title = run.context.definition.name;
|
|
357
290
|
if (message.kind === "progress") {
|
package/dist/tasks/outbox.js
CHANGED
|
@@ -1,36 +1,28 @@
|
|
|
1
|
-
// One delivery engine for everything that
|
|
2
|
-
//
|
|
3
|
-
// this logic was written twice and the two copies drifted — the ceiling was
|
|
4
|
-
// checked in one order here and another there, and only one of them counted a
|
|
5
|
-
// failed attempt.
|
|
1
|
+
// One delivery engine for everything that reaches a session as a system input:
|
|
2
|
+
// run callbacks, group callbacks and run messages.
|
|
6
3
|
import { logger } from "../log.js";
|
|
7
4
|
import { MAX_DELIVERY_ATTEMPTS, retryDelay, undeliverable } from "./types.js";
|
|
8
5
|
const log = logger("tasks");
|
|
9
6
|
/** 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
|
|
7
|
+
* every one of them, a group names itself, a message names its id. */
|
|
8
|
+
const recordIds = (origin) => {
|
|
9
|
+
if (origin?.kind === "task-callback")
|
|
10
|
+
return origin.runIds ?? [origin.runId];
|
|
11
|
+
if (origin?.kind === "task-message")
|
|
12
|
+
return [origin.messageId];
|
|
13
|
+
return [];
|
|
14
|
+
};
|
|
12
15
|
export class Outbox {
|
|
13
16
|
router;
|
|
14
17
|
kind;
|
|
15
|
-
unreachable;
|
|
16
18
|
delivering = new Set();
|
|
17
|
-
constructor(router, kind
|
|
18
|
-
/** Reports a delivery nobody can complete (service.ts owns the surfaces). */
|
|
19
|
-
unreachable) {
|
|
19
|
+
constructor(router, kind) {
|
|
20
20
|
this.router = router;
|
|
21
21
|
this.kind = kind;
|
|
22
|
-
this.unreachable = unreachable;
|
|
23
22
|
}
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `delivered` is written only against proof — the input visible in the
|
|
29
|
-
* recipient's own transcript. Pi's queues are memory, so an abort or a
|
|
30
|
-
* restart drops an accepted input and `systemInput` resolving proves
|
|
31
|
-
* nothing; a delivery reported on a resolved send is how a delegating agent
|
|
32
|
-
* ends up waiting forever on a result that was never read.
|
|
33
|
-
*/
|
|
23
|
+
/** One model turn drains the batch instead of one per record. `delivered` is
|
|
24
|
+
* written only against the input visible in the recipient's transcript: Pi's
|
|
25
|
+
* queues are memory, so a resolved `systemInput` proves nothing. */
|
|
34
26
|
async deliver(sessionId, batch) {
|
|
35
27
|
const mine = batch.filter((record) => !this.delivering.has(this.kind.id(record)));
|
|
36
28
|
if (mine.length === 0)
|
|
@@ -47,22 +39,16 @@ export class Outbox {
|
|
|
47
39
|
const live = unproven.filter((record) => !this.spent(record, sessionId));
|
|
48
40
|
if (live.length === 0)
|
|
49
41
|
return;
|
|
50
|
-
// Waiting for a busy target is not a delivery attempt: counting it would
|
|
51
|
-
// inflate the attempts once per second and skip the failure backoff
|
|
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
42
|
let sending = live;
|
|
60
|
-
|
|
43
|
+
// Waiting for a busy target is not an attempt, or the ceiling arrives in
|
|
44
|
+
// seconds. A `steer` record joins the running turn instead, but only once:
|
|
45
|
+
// handed over, it sits in Pi's in-memory queue, invisible in the transcript.
|
|
46
|
+
if (session.state === "streaming") {
|
|
61
47
|
const handedOver = await this.queued(session);
|
|
62
|
-
const
|
|
63
|
-
sending = live.filter(
|
|
48
|
+
const sendNow = (record) => (record.callbackMode === "steer" || this.kind.queues === true) && !handedOver.has(this.kind.id(record));
|
|
49
|
+
sending = live.filter(sendNow);
|
|
64
50
|
for (const record of live)
|
|
65
|
-
if (!
|
|
51
|
+
if (!sendNow(record))
|
|
66
52
|
this.defer(record);
|
|
67
53
|
if (sending.length === 0)
|
|
68
54
|
return;
|
|
@@ -72,14 +58,12 @@ export class Outbox {
|
|
|
72
58
|
counted.add(this.kind.id(record));
|
|
73
59
|
}
|
|
74
60
|
const { text, origin } = this.kind.input(sending);
|
|
61
|
+
const mode = sending.every((record) => record.callbackMode === "steer") ? "steer" : "followUp";
|
|
75
62
|
log.debug(`callback for ${sending.map((r) => this.kind.id(r)).join(", ")} → session ${sessionId}`);
|
|
76
|
-
// Not awaited: `systemInput` settles with the recipient's whole turn
|
|
77
|
-
|
|
78
|
-
// being read — which is the only thing that marks this delivered.
|
|
79
|
-
session.systemInput(text, origin, streaming ? "steer" : "followUp")
|
|
63
|
+
// Not awaited: `systemInput` settles with the recipient's whole turn.
|
|
64
|
+
session.systemInput(text, origin, mode)
|
|
80
65
|
.catch((error) => this.retry(sessionId, sending, error, counted));
|
|
81
|
-
// Pi records the input as it starts the turn
|
|
82
|
-
// here already; the tick sweep is the backstop when it is not.
|
|
66
|
+
// Pi records the input as it starts the turn; the tick sweep is the backstop.
|
|
83
67
|
await this.settle(sending, session);
|
|
84
68
|
}
|
|
85
69
|
catch (error) {
|
|
@@ -90,13 +74,11 @@ export class Outbox {
|
|
|
90
74
|
this.delivering.delete(this.kind.id(record));
|
|
91
75
|
}
|
|
92
76
|
}
|
|
93
|
-
/**
|
|
94
|
-
* drain it — what the transcript cannot answer yet. Empty unless the
|
|
95
|
-
* session is streaming: Pi drops the list when the turn ends. */
|
|
77
|
+
/** What the transcript cannot answer yet; empty unless the session is streaming. */
|
|
96
78
|
async queued(session) {
|
|
97
79
|
const ids = new Set();
|
|
98
80
|
for (const origin of await session.pendingSystemInputs())
|
|
99
|
-
for (const id of
|
|
81
|
+
for (const id of recordIds(origin))
|
|
100
82
|
ids.add(id);
|
|
101
83
|
return ids;
|
|
102
84
|
}
|
|
@@ -106,7 +88,7 @@ export class Outbox {
|
|
|
106
88
|
for (const turn of await session.history()) {
|
|
107
89
|
if (turn.role !== "system")
|
|
108
90
|
continue;
|
|
109
|
-
for (const id of
|
|
91
|
+
for (const id of recordIds(turn.origin))
|
|
110
92
|
seen.add(id);
|
|
111
93
|
}
|
|
112
94
|
const unproven = [];
|
|
@@ -119,9 +101,7 @@ export class Outbox {
|
|
|
119
101
|
}
|
|
120
102
|
return unproven;
|
|
121
103
|
}
|
|
122
|
-
/** The
|
|
123
|
-
* is silent, so this line is the only sign it is being retried — and the
|
|
124
|
-
* ceiling is what ends the retrying out loud. */
|
|
104
|
+
/** The retry itself is silent, so this line is the only sign of it. */
|
|
125
105
|
retry(sessionId, batch, error, counted) {
|
|
126
106
|
log.warn(`callback to session ${sessionId} failed, will retry`, error);
|
|
127
107
|
for (const stale of batch) {
|
|
@@ -134,8 +114,7 @@ export class Outbox {
|
|
|
134
114
|
this.spent(record, sessionId);
|
|
135
115
|
}
|
|
136
116
|
}
|
|
137
|
-
/**
|
|
138
|
-
* result it will never get must not be left waiting on silence. */
|
|
117
|
+
/** An agent waiting on a result it will never get must not wait on silence. */
|
|
139
118
|
spent(record, sessionId) {
|
|
140
119
|
if (record.callbackAttempts < MAX_DELIVERY_ATTEMPTS)
|
|
141
120
|
return false;
|
|
@@ -144,7 +123,7 @@ export class Outbox {
|
|
|
144
123
|
record.callbackError = undeliverable(record.callbackAttempts, record.callbackError);
|
|
145
124
|
record.callbackNextAttemptAt = null;
|
|
146
125
|
this.write(record);
|
|
147
|
-
this.
|
|
126
|
+
this.kind.abandoned(record, sessionId, record.callbackError);
|
|
148
127
|
}
|
|
149
128
|
return true;
|
|
150
129
|
}
|
|
@@ -153,8 +132,8 @@ export class Outbox {
|
|
|
153
132
|
record.callbackNextAttemptAt = Date.now() + 1000;
|
|
154
133
|
this.kind.save(record);
|
|
155
134
|
}
|
|
156
|
-
/**
|
|
157
|
-
*
|
|
135
|
+
/** Backs off like a failure, so an input never recorded is re-sent on a
|
|
136
|
+
* curve, not once a second. */
|
|
158
137
|
sent(record) {
|
|
159
138
|
record.callbackAttempts += 1;
|
|
160
139
|
record.callbackState = "pending";
|
package/dist/tasks/routes.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// The area's HTTP surface
|
|
2
|
-
//
|
|
3
|
-
// its body, names the caller and hands the decision to TaskService — policy
|
|
4
|
-
// that lives here would be policy the task tool does not get.
|
|
1
|
+
// The area's HTTP surface. A route names the caller and hands the decision to
|
|
2
|
+
// TaskService: policy here would be policy the task tool does not get.
|
|
5
3
|
import { record, requiredString } from "./definitions.js";
|
|
6
4
|
/** Reject malformed filters instead of silently widening a global query. */
|
|
7
5
|
function runQuery(params) {
|
|
@@ -224,9 +222,8 @@ export function registerTaskRoutes(app, tasks, activity) {
|
|
|
224
222
|
const body = record(await jsonBody(c.req));
|
|
225
223
|
const input = body && "input" in body ? body.input : null;
|
|
226
224
|
try {
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
// on a reuse definition, work injected into a live session.
|
|
225
|
+
// Dropping an unsupported override would run the definition's own policy
|
|
226
|
+
// instead — on a reuse definition, work injected into a live session.
|
|
230
227
|
if (body?.sessionMode !== undefined && body.sessionMode !== "fresh") {
|
|
231
228
|
throw new Error(`unsupported sessionMode: ${String(body.sessionMode)}`);
|
|
232
229
|
}
|
package/dist/tasks/runs.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
// A definition plus an input becomes a queued run
|
|
2
|
-
//
|
|
3
|
-
// and which session hears about it. The limits that keep a chain from
|
|
4
|
-
// exploding (depth, children per root) are decided here, once, because every
|
|
5
|
-
// caller — scheduler, tool, HTTP — enqueues through this one door.
|
|
1
|
+
// A definition plus an input becomes a queued run. The depth and per-root
|
|
2
|
+
// limits are decided here because every caller enqueues through this one door.
|
|
6
3
|
import { logger } from "../log.js";
|
|
7
4
|
import { newId } from "./definitions.js";
|
|
8
5
|
const log = logger("tasks");
|
|
@@ -92,10 +89,8 @@ export class TaskRunQueue {
|
|
|
92
89
|
const { id, depth, triggerSource: source } = run;
|
|
93
90
|
const overlapped = run.skipReason === "overlap";
|
|
94
91
|
const definition = run.context.definition;
|
|
95
|
-
// Why a run exists is
|
|
96
|
-
//
|
|
97
|
-
// probe queues on every interval and mostly matches nothing, so it says so
|
|
98
|
-
// at debug and lets its settled line (execution.ts) carry the news.
|
|
92
|
+
// Why a run exists is answerable only here: the row keeps the ids, not the
|
|
93
|
+
// reason. A watch probe queues every interval, so it logs at debug.
|
|
99
94
|
const queued = `run ${id} ${run.state}: ${definition.name} via ${source}` +
|
|
100
95
|
`${overlapped ? " (overlapped)" : ""}${depth > 0 ? ` depth ${String(depth)}` : ""}`;
|
|
101
96
|
if (source === "watch" && !overlapped)
|