@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/service.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// like it makes belongs to a file beside it (definitions, runs, execution,
|
|
5
|
-
// groups, messages, callbacks) — what is genuinely here is scheduling and the
|
|
6
|
-
// facade, so the HTTP routes and the task tool cannot drift apart.
|
|
1
|
+
// The facade the rest of Pier talks to about tasks, and the clock behind it:
|
|
2
|
+
// the tick, the boot recovery that writes off interrupted runs, and the pause a
|
|
3
|
+
// drain needs. Decisions belong to the files beside it.
|
|
7
4
|
import { logger } from "../log.js";
|
|
8
5
|
import { AgentTaskRunner } from "./agent.js";
|
|
9
6
|
import { TaskCallbacks } from "./callbacks.js";
|
|
@@ -15,9 +12,8 @@ import { TaskRunQueue } from "./runs.js";
|
|
|
15
12
|
import { handleTaskTool } from "./tool.js";
|
|
16
13
|
import { isTerminal } from "./types.js";
|
|
17
14
|
const log = logger("tasks");
|
|
18
|
-
/**
|
|
19
|
-
*
|
|
20
|
-
* preamble and the input wrapper, which the delegating session did not send. */
|
|
15
|
+
/** Not `renderedPrompt`: that carries the preamble and input wrapper, which the
|
|
16
|
+
* delegating session did not send. */
|
|
21
17
|
const runPrompt = (run) => {
|
|
22
18
|
if (run.context.resumePrompt)
|
|
23
19
|
return run.context.resumePrompt;
|
|
@@ -40,8 +36,7 @@ export class TaskService {
|
|
|
40
36
|
runs;
|
|
41
37
|
execution;
|
|
42
38
|
constructor(store, factory, router, hub,
|
|
43
|
-
/** Structural
|
|
44
|
-
* hands in a closure over the store instead. Absent in bare test rigs. */
|
|
39
|
+
/** Structural: tasks/ must not import settings.ts. Absent in bare test rigs. */
|
|
45
40
|
instance) {
|
|
46
41
|
this.store = store;
|
|
47
42
|
this.factory = factory;
|
|
@@ -108,10 +103,8 @@ export class TaskService {
|
|
|
108
103
|
}, tickMs);
|
|
109
104
|
this.timer.unref();
|
|
110
105
|
}
|
|
111
|
-
/**
|
|
112
|
-
*
|
|
113
|
-
* leave the scheduler switched off. Deliberately not `start()`: the boot
|
|
114
|
-
* recovery in there would write off runs this process is still running. */
|
|
106
|
+
/** For a handover that never started. Not `start()`: its boot recovery would
|
|
107
|
+
* write off runs this process is still running. */
|
|
115
108
|
unpause(tickMs = 1000) {
|
|
116
109
|
if (this.timer)
|
|
117
110
|
return;
|
|
@@ -122,11 +115,8 @@ export class TaskService {
|
|
|
122
115
|
this.pause();
|
|
123
116
|
this.execution.stop();
|
|
124
117
|
}
|
|
125
|
-
/**
|
|
126
|
-
*
|
|
127
|
-
* scheduler timer goes, and new root runs are refused; children of a run
|
|
128
|
-
* that is still finishing stay allowed, because refusing them would fail
|
|
129
|
-
* the very work the drain is waiting for. */
|
|
118
|
+
/** New root runs are refused, running ones left for the drain to wait on;
|
|
119
|
+
* children of a finishing run stay allowed, or the drain fails its own work. */
|
|
130
120
|
pause() {
|
|
131
121
|
this.paused = true;
|
|
132
122
|
if (this.timer)
|
|
@@ -143,11 +133,8 @@ export class TaskService {
|
|
|
143
133
|
activeRunCount() {
|
|
144
134
|
return this.store.countActiveRuns();
|
|
145
135
|
}
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
148
|
-
* an overlap needs the same answer to know what to wait for, and scanning
|
|
149
|
-
* run history for it finds nothing once the skipped rows outnumber the
|
|
150
|
-
* window. */
|
|
136
|
+
/** A caller refused as an overlap needs this to know what to wait for;
|
|
137
|
+
* scanning run history finds nothing once skipped rows outnumber the window. */
|
|
151
138
|
activeRun(taskId) {
|
|
152
139
|
return this.store.findActiveRun(taskId);
|
|
153
140
|
}
|
|
@@ -160,8 +147,7 @@ export class TaskService {
|
|
|
160
147
|
create(raw, creator = "http") {
|
|
161
148
|
return this.definitions.create(raw, creator);
|
|
162
149
|
}
|
|
163
|
-
/** `by` is how
|
|
164
|
-
* the task tool have none, which is what closes both (definitions.ts). */
|
|
150
|
+
/** `by` is how owning code says so; the routes and the tool have none (definitions.ts). */
|
|
165
151
|
update(id, raw, by) {
|
|
166
152
|
return this.definitions.update(id, raw, by);
|
|
167
153
|
}
|
|
@@ -202,16 +188,13 @@ export class TaskService {
|
|
|
202
188
|
recentMessages(since) {
|
|
203
189
|
return this.messages.recent(since);
|
|
204
190
|
}
|
|
205
|
-
/** Every run
|
|
206
|
-
* it, and a message does not leave the transcript because the run ended. */
|
|
191
|
+
/** Every run, not the last hour's: the card is a message in the transcript. */
|
|
207
192
|
backgroundRuns(sessionId) {
|
|
208
193
|
return this.store.listRunsForSession(sessionId, 200)
|
|
209
194
|
.filter((run) => run.background)
|
|
210
195
|
.reverse()
|
|
211
196
|
.map((run) => this.backgroundRun(run));
|
|
212
197
|
}
|
|
213
|
-
/** The same runs `backgroundRuns` reports as in flight, counted per session
|
|
214
|
-
* in one query — a list needs the number, not the runs. */
|
|
215
198
|
activeBackgroundRunCounts() {
|
|
216
199
|
return this.store.countActiveBackgroundRunsBySession();
|
|
217
200
|
}
|
|
@@ -326,9 +309,7 @@ export class TaskService {
|
|
|
326
309
|
tool(raw, callerSessionId) {
|
|
327
310
|
return handleTaskTool(this, this.definitions, this.store, this.messages, raw, callerSessionId);
|
|
328
311
|
}
|
|
329
|
-
/**
|
|
330
|
-
* set, the curated live catalog otherwise — an agent picks from names that
|
|
331
|
-
* exist right now, never from memory. */
|
|
312
|
+
/** An agent picks from names that exist right now, never from memory. */
|
|
332
313
|
async models() {
|
|
333
314
|
const menu = this.instance?.modelMenu() ?? [];
|
|
334
315
|
if (menu.length)
|
|
@@ -342,8 +323,17 @@ export class TaskService {
|
|
|
342
323
|
try {
|
|
343
324
|
const now = Date.now();
|
|
344
325
|
this.sweep("schedule", () => {
|
|
345
|
-
|
|
346
|
-
|
|
326
|
+
// Per task: one enqueue that throws must not spend the other due tasks'
|
|
327
|
+
// occurrence, and its own stays due for the next tick.
|
|
328
|
+
for (const task of this.definitions.due(now)) {
|
|
329
|
+
this.sweep(`schedule ${task.name}`, () => {
|
|
330
|
+
const run = this.store.transact(() => {
|
|
331
|
+
this.definitions.advance(task, now);
|
|
332
|
+
return this.prepareRun(task.id, null, task.trigger.type === "watch" ? "watch" : "cron", null, {});
|
|
333
|
+
});
|
|
334
|
+
this.hub.emitWorkspace({ type: "tasks-changed" });
|
|
335
|
+
this.runs.start(run);
|
|
336
|
+
});
|
|
347
337
|
}
|
|
348
338
|
});
|
|
349
339
|
this.sweep("run callbacks", () => this.callbacks.recover(now));
|
|
@@ -354,16 +344,13 @@ export class TaskService {
|
|
|
354
344
|
this.ticking = false;
|
|
355
345
|
}
|
|
356
346
|
}
|
|
357
|
-
/**
|
|
358
|
-
*
|
|
359
|
-
* the operator's log, the record the tool and Console read, and the event
|
|
360
|
-
* stream of the session that was supposed to receive it (§5b). */
|
|
347
|
+
/** Retrying forever costs the same silence as dropping, so it stops and says
|
|
348
|
+
* so on the log, the record and the recipient's event stream (§5). */
|
|
361
349
|
unreachable(sessionId, what, why) {
|
|
362
350
|
log.error(`gave up delivering ${what} to session ${sessionId}: ${why}`);
|
|
363
351
|
this.router.reportTo(sessionId, `${what} could not be delivered — ${why}`);
|
|
364
352
|
}
|
|
365
|
-
/**
|
|
366
|
-
* is gone throws on every pass) must not starve the retries behind it. */
|
|
353
|
+
/** Isolated: one sweep throwing on every pass must not starve the others. */
|
|
367
354
|
sweep(what, run) {
|
|
368
355
|
try {
|
|
369
356
|
run();
|
package/dist/tasks/store.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Every query this area makes against pier.db, and nothing else
|
|
2
|
-
//
|
|
3
|
-
// connection db.ts opened. A store owns its queries, never its own tables or
|
|
4
|
-
// its own handle — the schema is db.ts's migration list.
|
|
1
|
+
// Every query this area makes against pier.db, and nothing else; the schema is
|
|
2
|
+
// db.ts's migration list.
|
|
5
3
|
import { pierDb, statements, transact } from "../db.js";
|
|
6
4
|
const clamp = (limit, cap) => Math.min(Math.max(limit, 1), cap);
|
|
5
|
+
/** Kept unmatched probes per watch: one page of history, which is all any
|
|
6
|
+
* surface lists of a probe that found nothing. */
|
|
7
|
+
const KEPT_PROBES = 50;
|
|
7
8
|
export class TaskStore {
|
|
8
9
|
db;
|
|
9
10
|
/** Every query below is a fixed string, so each is compiled once. */
|
|
@@ -16,9 +17,7 @@ export class TaskStore {
|
|
|
16
17
|
transact(work) {
|
|
17
18
|
return transact(this.db, work);
|
|
18
19
|
}
|
|
19
|
-
//
|
|
20
|
-
// readers, and the one seam where a future schema change normalizes old rows
|
|
21
|
-
// (pre-v1 databases are refused outright in db.ts).
|
|
20
|
+
// The only readers of the JSON columns, and where a schema change normalizes old rows.
|
|
22
21
|
#one(sql, ...params) {
|
|
23
22
|
const row = this.sql(sql).get(...params);
|
|
24
23
|
return row ? JSON.parse(row.json) : undefined;
|
|
@@ -39,9 +38,7 @@ export class TaskStore {
|
|
|
39
38
|
ON CONFLICT(id) DO UPDATE SET updated_at=excluded.updated_at, json=excluded.json
|
|
40
39
|
`).run(task.id, task.updatedAt, JSON.stringify(task));
|
|
41
40
|
}
|
|
42
|
-
/**
|
|
43
|
-
* document: a task with no next run (disabled, archived, manual) is not in
|
|
44
|
-
* it, so an idle second reads no row. */
|
|
41
|
+
/** Asked of the index, not every document: an idle second reads no row. */
|
|
45
42
|
listDueTasks(now) {
|
|
46
43
|
return this.#many("SELECT json FROM tasks WHERE next_run_at IS NOT NULL AND next_run_at <= ?", now);
|
|
47
44
|
}
|
|
@@ -62,9 +59,28 @@ export class TaskStore {
|
|
|
62
59
|
state=excluded.state, callback_state=excluded.callback_state, json=excluded.json
|
|
63
60
|
`).run(run.id, run.taskId, run.queuedAt, run.state, run.callbackState, JSON.stringify(run));
|
|
64
61
|
}
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
62
|
+
/** A watch at the five-second floor mints 17k rows a day, ~1.6 kB each, that
|
|
63
|
+
* no list shows; only rows nothing can dangle from are dropped — a child
|
|
64
|
+
* run is named in its parent's stored result. */
|
|
65
|
+
pruneUnmatchedProbes(taskId) {
|
|
66
|
+
this.sql(`
|
|
67
|
+
DELETE FROM task_runs WHERE id IN (
|
|
68
|
+
SELECT r.id FROM task_runs r
|
|
69
|
+
WHERE r.task_id = ? AND r.state = 'succeeded' AND json_extract(r.json, '$.matched') IS 0
|
|
70
|
+
AND r.callback_state IS NULL AND json_extract(r.json, '$.groupId') IS NULL
|
|
71
|
+
AND json_extract(r.json, '$.parentRunId') IS NULL
|
|
72
|
+
AND NOT EXISTS (SELECT 1 FROM task_messages m WHERE m.run_id = r.id)
|
|
73
|
+
-- A NULL in this list would make NOT IN unknown for every candidate.
|
|
74
|
+
AND r.id NOT IN (
|
|
75
|
+
SELECT json_extract(c.json, '$.resumedFromRunId') FROM task_runs c
|
|
76
|
+
WHERE c.task_id = ? AND json_extract(c.json, '$.resumedFromRunId') IS NOT NULL
|
|
77
|
+
)
|
|
78
|
+
ORDER BY r.queued_at DESC, r.id DESC LIMIT -1 OFFSET ?
|
|
79
|
+
)
|
|
80
|
+
`).run(taskId, taskId, KEPT_PROBES);
|
|
81
|
+
}
|
|
82
|
+
/** The id breaks timestamp ties so a page boundary never repeats or skips a
|
|
83
|
+
* row. Built per call, not cached: the filter set makes it. */
|
|
68
84
|
queryRuns(query = {}) {
|
|
69
85
|
const where = [];
|
|
70
86
|
const params = [];
|
|
@@ -143,9 +159,7 @@ export class TaskStore {
|
|
|
143
159
|
ORDER BY CASE state WHEN 'running' THEN 0 ELSE 1 END, queued_at DESC LIMIT 1
|
|
144
160
|
`, sessionId);
|
|
145
161
|
}
|
|
146
|
-
/**
|
|
147
|
-
* draws one dot per row: the state column narrows the scan to the handful
|
|
148
|
-
* of live runs, and no row's JSON is parsed. */
|
|
162
|
+
/** The state column narrows the scan to live runs; no JSON is parsed. */
|
|
149
163
|
countActiveBackgroundRunsBySession() {
|
|
150
164
|
const rows = this.sql(`
|
|
151
165
|
SELECT json_extract(json, '$.invokedBySessionId') AS session_id, COUNT(*) AS n
|
|
@@ -157,10 +171,8 @@ export class TaskStore {
|
|
|
157
171
|
`).all();
|
|
158
172
|
return new Map(rows.map((row) => [row.session_id, row.n]));
|
|
159
173
|
}
|
|
160
|
-
/**
|
|
161
|
-
*
|
|
162
|
-
* session its `fresh` predecessor already stamped. These are the agent's
|
|
163
|
-
* conversations with itself, which the rail does not list. */
|
|
174
|
+
/** `fresh` is the one mode that makes a session rather than borrowing one;
|
|
175
|
+
* these are the agent's conversations with itself, which the rail does not list. */
|
|
164
176
|
taskOwnedSessionIds() {
|
|
165
177
|
const rows = this.sql(`
|
|
166
178
|
SELECT DISTINCT json_extract(json, '$.context.sessionId') AS id
|
|
@@ -214,10 +226,8 @@ export class TaskStore {
|
|
|
214
226
|
listRecentMessages(since) {
|
|
215
227
|
return this.#many("SELECT json FROM task_messages WHERE created_at >= ? ORDER BY created_at DESC LIMIT 200", since);
|
|
216
228
|
}
|
|
217
|
-
/**
|
|
218
|
-
*
|
|
219
|
-
* sweep cost a query per undelivered message. A message whose run is gone
|
|
220
|
-
* comes back with `run: undefined` — the sweep drops it. */
|
|
229
|
+
/** Each beside its run, so the sweep costs one query. A message whose run is
|
|
230
|
+
* gone comes back with `run: undefined`. */
|
|
221
231
|
listUndeliveredMessages() {
|
|
222
232
|
const rows = this.sql(`
|
|
223
233
|
SELECT m.json AS json, r.json AS run_json
|
|
@@ -237,9 +247,8 @@ export class TaskStore {
|
|
|
237
247
|
WHERE state = 'pending' AND json_extract(json, '$.kind') != 'decision'
|
|
238
248
|
`).map((message) => {
|
|
239
249
|
message.state = "expired";
|
|
240
|
-
// "Confirmed", not "completed": the
|
|
241
|
-
//
|
|
242
|
-
// cannot see, and the run it steered is interrupted by the same restart.
|
|
250
|
+
// "Confirmed", not "completed": the proof lives in a transcript this
|
|
251
|
+
// layer cannot see.
|
|
243
252
|
message.error = "Pier restarted before delivery could be confirmed";
|
|
244
253
|
this.saveMessage(message);
|
|
245
254
|
return message;
|
package/dist/tasks/tool.js
CHANGED
|
@@ -6,17 +6,9 @@ import { isTerminal } from "./types.js";
|
|
|
6
6
|
const log = logger("tasks");
|
|
7
7
|
// JSON-Schema enum emits ~1/3 the tokens of typebox's anyOf-of-consts.
|
|
8
8
|
const strEnum = (...values) => Type.Unsafe({ type: "string", enum: [...values] });
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* A run summary has eighteen fields and most are empty for most of a run's
|
|
13
|
-
* life; a model reads "absent" and "null" the same way. On a group summary
|
|
14
|
-
* that lists several runs this is a third of the payload.
|
|
15
|
-
*
|
|
16
|
-
* The input names every field — a summary that forgot one would otherwise pass
|
|
17
|
-
* as "that field was empty" — and the result is the type with the empty ones
|
|
18
|
-
* gone, which is why those are declared optional above.
|
|
19
|
-
*/
|
|
9
|
+
/** Absent instead of `null`: a model reads both the same way, and on a group
|
|
10
|
+
* summary the nulls are a third of the payload. The input names every field
|
|
11
|
+
* so a summary that forgot one cannot pass as "empty". */
|
|
20
12
|
const defined = (value) => Object.fromEntries(Object.entries(value).filter(([, v]) => v !== null && v !== undefined));
|
|
21
13
|
const summarize = (run, pendingDecisionId) => defined({
|
|
22
14
|
runId: run.id,
|
|
@@ -40,9 +32,8 @@ const summarize = (run, pendingDecisionId) => defined({
|
|
|
40
32
|
skipReason: run.skipReason,
|
|
41
33
|
next: null,
|
|
42
34
|
});
|
|
43
|
-
/**
|
|
44
|
-
*
|
|
45
|
-
* for a status call. `none` is stated as what it is: no delivery at all. */
|
|
35
|
+
/** The receipt says the callback is the whole answer, or a model that just
|
|
36
|
+
* launched work reaches for a status call. */
|
|
46
37
|
const receipt = (summary, callbackSessionId, mode, callerSessionId) => ({
|
|
47
38
|
...summary,
|
|
48
39
|
next: callbackSessionId === null
|
|
@@ -53,6 +44,19 @@ const receipt = (summary, callbackSessionId, mode, callerSessionId) => ({
|
|
|
53
44
|
? "the result interrupts your running turn as a steer message; nothing to query"
|
|
54
45
|
: "the result arrives as a callback message once your turn ends; nothing to query",
|
|
55
46
|
});
|
|
47
|
+
const SUBAGENT_REDIRECT = "subagents cannot redirect callbacks (callback_session_id)";
|
|
48
|
+
/** Who a new run's result goes to — the caller, nobody, or a named session that
|
|
49
|
+
* must exist. Shared by `run` and `resume`: a resumed run is a new run. */
|
|
50
|
+
const callbackTarget = async (input, definitions, callerSessionId) => {
|
|
51
|
+
if (input.callback_session_id === undefined)
|
|
52
|
+
return input.callback === "none" ? null : callerSessionId;
|
|
53
|
+
if (input.callback === "none")
|
|
54
|
+
throw new Error("callback none and callback_session_id conflict: pick one delivery target");
|
|
55
|
+
const target = requiredString(input.callback_session_id, "callback_session_id");
|
|
56
|
+
if (!(await definitions.sessionExists(target)))
|
|
57
|
+
throw new Error(`unknown session: ${target}`);
|
|
58
|
+
return target;
|
|
59
|
+
};
|
|
56
60
|
/** A group echoes many results at once, so each is capped; a single-run
|
|
57
61
|
* `recover` stays whole — it is the escape hatch every truncation note points at. */
|
|
58
62
|
const trimResult = (summary) => {
|
|
@@ -66,13 +70,10 @@ const trimResult = (summary) => {
|
|
|
66
70
|
},
|
|
67
71
|
};
|
|
68
72
|
};
|
|
69
|
-
/**
|
|
70
|
-
*
|
|
71
|
-
* was never one to wait for (`callback:"none"`). Anything else is still on
|
|
72
|
-
* its way, and reading the result here would be reading it twice. */
|
|
73
|
+
/** Delivered, given up on and reported, or never owed (`callback:"none"`).
|
|
74
|
+
* Anything else is still on its way, and reading it here would be reading it twice. */
|
|
73
75
|
const settled = (callback) => callback.callbackState === null || callback.callbackState === "delivered" || callback.callbackState === "abandoned";
|
|
74
|
-
/** The
|
|
75
|
-
* the same words for queued, running, pending and retrying: a refusal that
|
|
76
|
+
/** The same words for queued, running, pending and retrying: a refusal that
|
|
76
77
|
* named the state would be the status query this operation replaced. */
|
|
77
78
|
const notRecoverable = (what, callback) => {
|
|
78
79
|
throw new Error(callback.callbackSessionId === null
|
|
@@ -93,8 +94,8 @@ const LaunchSchema = Type.Object({
|
|
|
93
94
|
model: Type.Optional(Type.Object({ provider: Type.String(), id: Type.String() })),
|
|
94
95
|
thinking: Type.Optional(Type.String()),
|
|
95
96
|
});
|
|
96
|
-
//
|
|
97
|
-
//
|
|
97
|
+
// Guidance only: runtime truth stays in parseDraft, so schema drift cannot
|
|
98
|
+
// loosen boundary validation.
|
|
98
99
|
const DraftSchema = Type.Object({
|
|
99
100
|
name: Type.Optional(Type.String({ description: "Defaults to the prompt's first line." })),
|
|
100
101
|
description: Type.Optional(Type.String()),
|
|
@@ -134,7 +135,7 @@ export function taskToolSpec(execute) {
|
|
|
134
135
|
return {
|
|
135
136
|
name: "task",
|
|
136
137
|
label: "Pier Task",
|
|
137
|
-
description: "Manage durable Pier tasks and subagents. Agent tasks run in a fresh session or a reused one. create files a definition the operator sees in the Console — only for schedules or roles you will run again; a one-off is run with a prompt. Run executes a stored task by task_id, a one-shot subagent from a prompt (shorthand: prompt + optional cwd/launch/name — cwd defaults to your own directory, relative paths resolve against it, name comes from the prompt) or from a full inline task draft, or a core-joined fan-out via tasks[] with join all|first. Every operation returns immediately: results, group joins, and decision replies arrive as callback messages once your turn ends — there is no status query; pass callback 'steer' to have a result interrupt your running turn instead, or 'none' for no callback at all. recover (run_id or group_id, plus a reason) re-reads a finished result after its callback has settled — for truncated text or lost context, never to check progress. Use steer/follow_up/resume for child control and contact/reply for supervisor decisions. models lists the deployment's model menu (operator pins with intent notes, else the live catalog).",
|
|
138
|
+
description: "Manage durable Pier tasks and subagents. Agent tasks run in a fresh session or a reused one. create files a definition the operator sees in the Console — only for schedules or roles you will run again; a one-off is run with a prompt. Run executes a stored task by task_id, a one-shot subagent from a prompt (shorthand: prompt + optional cwd/launch/name/timeoutSeconds — cwd defaults to your own directory, relative paths resolve against it, name comes from the prompt) or from a full inline task draft, or a core-joined fan-out via tasks[] with join all|first. Every operation returns immediately: results, group joins, and decision replies arrive as callback messages once your turn ends — there is no status query; pass callback 'steer' to have a result interrupt your running turn instead, or 'none' for no callback at all. recover (run_id or group_id, plus a reason) re-reads a finished result after its callback has settled — for truncated text or lost context, never to check progress. Use steer/follow_up/resume for child control and contact/reply for supervisor decisions. models lists the deployment's model menu (operator pins with intent notes, else the live catalog).",
|
|
138
139
|
parameters: Type.Object({
|
|
139
140
|
operation: strEnum("list", "create", "update", "run", "recover", "cancel", "steer", "follow_up", "resume", "contact", "reply", "models"),
|
|
140
141
|
task_id: Type.Optional(Type.String()),
|
|
@@ -144,19 +145,17 @@ export function taskToolSpec(execute) {
|
|
|
144
145
|
message: Type.Optional(Type.String()),
|
|
145
146
|
reason: Type.Optional(Type.String({ description: "contact: progress | decision. recover: why the delivered callback is not enough (required)." })),
|
|
146
147
|
session_mode: Type.Optional(strEnum("fresh")),
|
|
147
|
-
// The one-shot shorthand: a prompt is the whole delegation, and the
|
|
148
|
-
// fresh session in the caller's own directory is what it means.
|
|
149
148
|
prompt: Type.Optional(Type.String()),
|
|
150
149
|
cwd: Type.Optional(Type.String()),
|
|
151
150
|
launch: Type.Optional(LaunchSchema),
|
|
152
151
|
name: Type.Optional(Type.String()),
|
|
152
|
+
timeoutSeconds: Type.Optional(Type.Number({ description: "1–86400; defaults to 3600." })),
|
|
153
153
|
task: Type.Optional(DraftSchema),
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
// one points at it, and `parseDraft` is what actually validates either.
|
|
154
|
+
// Spelled out, the draft schema costs more tokens per session than the
|
|
155
|
+
// rest of this contract; `parseDraft` validates either shape.
|
|
157
156
|
tasks: Type.Optional(Type.Unsafe({
|
|
158
157
|
type: "array",
|
|
159
|
-
description: "2+ entries, each a prompt string, {prompt, cwd?, launch?, name?}, a task draft shaped exactly like `task`, or {task_id}.",
|
|
158
|
+
description: "2+ entries, each a prompt string, {prompt, cwd?, launch?, name?, timeoutSeconds?}, a task draft shaped exactly like `task`, or {task_id}.",
|
|
160
159
|
items: { type: "object" },
|
|
161
160
|
})),
|
|
162
161
|
join: Type.Optional(strEnum("all", "first")),
|
|
@@ -187,15 +186,19 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
187
186
|
return definitions.update(requiredString(input.task_id, "task_id"), await expandDraft(definitions, input.task, callerSessionId));
|
|
188
187
|
}
|
|
189
188
|
if (input.operation === "run") {
|
|
190
|
-
// One reading of `callback` for both shapes below: a run and a fan-out
|
|
191
|
-
// choose the same way, and two readings are two things to keep in step.
|
|
192
189
|
const callbackMode = input.callback === "steer" ? "steer" : "followUp";
|
|
190
|
+
// A run's own callback is its parent's link back; a child that could point
|
|
191
|
+
// it elsewhere would strand the supervisor waiting for a result.
|
|
192
|
+
if (active && input.callback_session_id !== undefined)
|
|
193
|
+
throw new Error(SUBAGENT_REDIRECT);
|
|
193
194
|
if (Array.isArray(input.tasks)) {
|
|
194
195
|
// Core-joined fan-out: members run detached, one aggregated callback.
|
|
195
196
|
if (input.task !== undefined || input.task_id !== undefined)
|
|
196
197
|
throw new Error("use either task/task_id or tasks[]");
|
|
197
198
|
if (input.session_mode !== undefined)
|
|
198
199
|
throw new Error("session_mode applies to a single run only");
|
|
200
|
+
if (input.callback_session_id !== undefined)
|
|
201
|
+
throw new Error("callback_session_id applies to a single run only");
|
|
199
202
|
if (input.tasks.length < 2)
|
|
200
203
|
throw new Error("tasks[] needs at least 2 entries; use task for a single run");
|
|
201
204
|
const resolved = [];
|
|
@@ -221,12 +224,7 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
221
224
|
throw new Error(`unsupported session_mode: ${String(input.session_mode)}`);
|
|
222
225
|
}
|
|
223
226
|
const sessionMode = input.session_mode;
|
|
224
|
-
|
|
225
|
-
if (!active && callbackSessionId && typeof input.callback_session_id === "string") {
|
|
226
|
-
callbackSessionId = requiredString(input.callback_session_id, "callback_session_id");
|
|
227
|
-
if (!(await definitions.sessionExists(callbackSessionId)))
|
|
228
|
-
throw new Error(`unknown session: ${callbackSessionId}`);
|
|
229
|
-
}
|
|
227
|
+
const callbackSessionId = await callbackTarget(input, definitions, callerSessionId);
|
|
230
228
|
const run = host.run(task.id, input.input, "agent", active?.id ?? null, {
|
|
231
229
|
invokedBySessionId: callerSessionId,
|
|
232
230
|
sourceSessionId: callerSessionId,
|
|
@@ -238,18 +236,14 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
238
236
|
return receipt(summarize(run, null), callbackSessionId, callbackMode, callerSessionId);
|
|
239
237
|
}
|
|
240
238
|
if (input.operation === "recover") {
|
|
241
|
-
// History only, never status:
|
|
242
|
-
//
|
|
243
|
-
// something it would not have been told. The reason is the friction — a
|
|
244
|
-
// caller states why the callback did not suffice, and the operator sees it.
|
|
239
|
+
// History only, never status: readable once the callback has said its last
|
|
240
|
+
// word. The required reason is the friction, and the operator sees it.
|
|
245
241
|
const reason = requiredString(input.reason, "reason");
|
|
246
|
-
//
|
|
247
|
-
// question is the notification, and the reply's continuation reports.
|
|
242
|
+
// The open question is the notification; the reply's continuation reports.
|
|
248
243
|
const decisionOpen = (run) => {
|
|
249
244
|
throw new Error(`run ${run.id} finished awaiting your decision ${messages.openDecisionId(run.id) ?? ""}; reply to it — the continuation's callback brings the result`);
|
|
250
245
|
};
|
|
251
|
-
// A
|
|
252
|
-
// wait for losing members to finish cancelling before recovering its text.
|
|
246
|
+
// A race winner need not wait for losing members to finish cancelling.
|
|
253
247
|
const groupReady = (group) => {
|
|
254
248
|
if (!group.finishedAt || !settled(group))
|
|
255
249
|
notRecoverable(`group ${group.id}`, group);
|
|
@@ -302,13 +296,19 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
302
296
|
if (input.operation === "resume") {
|
|
303
297
|
const prior = host.getRun(requiredString(input.run_id, "run_id"));
|
|
304
298
|
assertOwns(store, callerSessionId, active, prior);
|
|
305
|
-
|
|
299
|
+
// The resumed run is a new run, so it carries its own callback options,
|
|
300
|
+
// under the same rule as `run`: a subagent may not redirect them.
|
|
301
|
+
if (active && input.callback_session_id !== undefined)
|
|
302
|
+
throw new Error(SUBAGENT_REDIRECT);
|
|
303
|
+
const callbackMode = input.callback === "steer" ? "steer" : "followUp";
|
|
304
|
+
const callbackSessionId = await callbackTarget(input, definitions, callerSessionId);
|
|
306
305
|
const run = host.resume(prior.id, requiredString(input.message, "message"), {
|
|
307
306
|
invokedBySessionId: callerSessionId,
|
|
308
307
|
callbackSessionId,
|
|
308
|
+
callbackMode,
|
|
309
309
|
background: true,
|
|
310
310
|
});
|
|
311
|
-
return receipt(summarize(run, null), callbackSessionId,
|
|
311
|
+
return receipt(summarize(run, null), callbackSessionId, callbackMode, callerSessionId);
|
|
312
312
|
}
|
|
313
313
|
if (input.operation === "contact") {
|
|
314
314
|
if (!active)
|
|
@@ -327,28 +327,22 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
327
327
|
}
|
|
328
328
|
/** A single run's draft: the top-level shorthand (`prompt` …) or `task`, never both. */
|
|
329
329
|
function inlineDraft(input) {
|
|
330
|
-
const { prompt, cwd, launch, name } = input;
|
|
330
|
+
const { prompt, cwd, launch, name, timeoutSeconds } = input;
|
|
331
331
|
if (prompt === undefined)
|
|
332
332
|
return record(input.task) ?? undefined;
|
|
333
333
|
if (input.task !== undefined)
|
|
334
334
|
throw new Error("use either prompt or task");
|
|
335
|
-
return { prompt, cwd, launch, name };
|
|
335
|
+
return { prompt, cwd, launch, name, timeoutSeconds };
|
|
336
336
|
}
|
|
337
|
-
/**
|
|
338
|
-
* not an identifier — the run's id is what anything addresses. */
|
|
337
|
+
/** A label for the Console, not an identifier. */
|
|
339
338
|
function nameFromPrompt(prompt) {
|
|
340
339
|
const line = prompt.split("\n")
|
|
341
340
|
.map((l) => l.replace(/^[\s#>*-]+/, "").replace(/[*_`]/g, "").replace(/\s+/g, " ").trim())
|
|
342
341
|
.find(Boolean) ?? "subagent";
|
|
343
342
|
return line.length > 60 ? `${line.slice(0, 59).trimEnd()}…` : line;
|
|
344
343
|
}
|
|
345
|
-
/**
|
|
346
|
-
*
|
|
347
|
-
* A `prompt` shorthand becomes a fresh Agent action; a fresh session's cwd
|
|
348
|
-
* resolves against the caller's own directory (and is that directory when
|
|
349
|
-
* omitted); a missing name is the prompt's first line. Everything the caller
|
|
350
|
-
* did spell out passes through untouched — parseDraft still judges it.
|
|
351
|
-
*/
|
|
344
|
+
/** A `prompt` shorthand becomes a fresh Agent action in the caller's own
|
|
345
|
+
* directory; everything the caller did spell out passes through to parseDraft. */
|
|
352
346
|
async function expandDraft(definitions, raw, callerSessionId) {
|
|
353
347
|
let draft = record(raw);
|
|
354
348
|
if (!draft)
|
|
@@ -371,8 +365,8 @@ async function expandDraft(definitions, raw, callerSessionId) {
|
|
|
371
365
|
draft = { ...draft, name: nameFromPrompt(action.prompt) };
|
|
372
366
|
return draft;
|
|
373
367
|
}
|
|
374
|
-
/**
|
|
375
|
-
*
|
|
368
|
+
/** Persisted like any task (kind "subagent", filtered from default lists) so
|
|
369
|
+
* runs stay auditable and resumable. */
|
|
376
370
|
async function resolveDraft(definitions, raw, active, callerSessionId) {
|
|
377
371
|
const draft = record(await expandDraft(definitions, raw, callerSessionId));
|
|
378
372
|
if (!draft)
|
|
@@ -380,6 +374,11 @@ async function resolveDraft(definitions, raw, active, callerSessionId) {
|
|
|
380
374
|
if (draft.trigger !== undefined && record(draft.trigger)?.type !== "manual") {
|
|
381
375
|
throw new Error("inline subagent tasks must use a manual trigger");
|
|
382
376
|
}
|
|
377
|
+
// Delivery of a one-off run is the top-level fields' business; a nested
|
|
378
|
+
// callback only means anything on a stored definition's schedule.
|
|
379
|
+
if (draft.callback !== undefined || draft.callback_session_id !== undefined) {
|
|
380
|
+
throw new Error("an inline task draft cannot set callback; use the top-level callback / callback_session_id");
|
|
381
|
+
}
|
|
383
382
|
if (active) {
|
|
384
383
|
const action = record(draft.action);
|
|
385
384
|
if (action?.type !== "agent")
|