@timqi/pier 0.1.3 → 0.1.5
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 +12 -7
- package/dist/agent/config.js +9 -3
- package/dist/agent/events.js +13 -5
- package/dist/agent/listing.js +10 -2
- package/dist/agent/packages.js +4 -20
- package/dist/agent/pi.js +18 -86
- package/dist/channels/attach.js +2 -2
- package/dist/channels/commands.js +10 -8
- package/dist/channels/config.js +32 -24
- package/dist/channels/control.js +60 -14
- package/dist/channels/conversations.js +60 -13
- package/dist/channels/handoff.js +90 -0
- package/dist/channels/lark-api.js +7 -0
- package/dist/channels/lark-outbound.js +19 -0
- package/dist/channels/lark-panel.js +41 -26
- package/dist/channels/lark-render.js +2 -1
- package/dist/channels/lark.js +35 -26
- package/dist/channels/lines.js +1 -1
- package/dist/channels/panel.js +324 -115
- package/dist/channels/receipts.js +25 -12
- package/dist/channels/routes.js +21 -7
- package/dist/channels/runtime.js +21 -7
- package/dist/channels/slack-api.js +19 -10
- package/dist/channels/slack-cli.js +503 -0
- package/dist/channels/slack-directory.js +2 -2
- package/dist/channels/slack-outbound.js +11 -1
- package/dist/channels/slack-panel.js +81 -40
- package/dist/channels/slack-render.js +3 -1
- package/dist/channels/slack-thread.js +41 -0
- package/dist/channels/slack-transcript.js +107 -0
- package/dist/channels/slack.js +38 -31
- package/dist/channels/types.js +1 -3
- package/dist/cli.js +166 -7
- package/dist/core/identity.js +61 -17
- package/dist/core/reply.js +19 -15
- package/dist/core/router.js +23 -3
- package/dist/db.js +33 -0
- package/dist/main.js +61 -41
- package/dist/paths.js +3 -0
- package/dist/secrets.js +2 -1
- package/dist/settings.js +3 -12
- package/dist/socket.js +99 -0
- package/dist/tasks/agent.js +5 -6
- package/dist/tasks/callbacks.js +2 -2
- package/dist/tasks/cli.js +225 -0
- package/dist/tasks/definitions.js +3 -3
- package/dist/tasks/execution.js +1 -3
- package/dist/tasks/groups.js +2 -5
- package/dist/tasks/messages.js +18 -158
- package/dist/tasks/operations.js +335 -0
- package/dist/tasks/routes.js +1 -11
- package/dist/tasks/runs.js +5 -20
- package/dist/tasks/service.js +18 -43
- package/dist/tasks/store.js +15 -15
- package/dist/tools.js +18 -3
- package/dist/vault.js +107 -0
- package/dist/web/auth.js +2 -1
- package/dist/web/public/assets/{activity-BSMeRcN2.js → activity-CrybM-E8.js} +2 -2
- package/dist/web/public/assets/activity-CrybM-E8.js.br +0 -0
- package/dist/web/public/assets/activity-CrybM-E8.js.gz +0 -0
- package/dist/web/public/assets/{boards-BCWQMZry.js → boards-Cw7_6J6L.js} +1 -1
- package/dist/web/public/assets/boards-Cw7_6J6L.js.br +0 -0
- package/dist/web/public/assets/boards-Cw7_6J6L.js.gz +0 -0
- package/dist/web/public/assets/{explorer-Cr4XTi4j.js → explorer-DV066dUD.js} +1 -1
- package/dist/web/public/assets/explorer-DV066dUD.js.br +0 -0
- package/dist/web/public/assets/explorer-DV066dUD.js.gz +0 -0
- package/dist/web/public/assets/index-BrNHu2qj.js +85 -0
- package/dist/web/public/assets/index-BrNHu2qj.js.br +0 -0
- package/dist/web/public/assets/index-BrNHu2qj.js.gz +0 -0
- package/dist/web/public/assets/index-DLszkDUV.css +2 -0
- package/dist/web/public/assets/index-DLszkDUV.css.br +0 -0
- package/dist/web/public/assets/index-DLszkDUV.css.gz +0 -0
- package/dist/web/public/assets/{runs-DdERzeac.js → runs-DDagTNaM.js} +1 -1
- package/dist/web/public/assets/runs-DDagTNaM.js.br +0 -0
- package/dist/web/public/assets/runs-DDagTNaM.js.gz +0 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js +5 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js.br +0 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-C-dGUDsH.js → task-runs-BnMack9t.js} +1 -1
- package/dist/web/public/assets/task-runs-BnMack9t.js.br +0 -0
- package/dist/web/public/assets/task-runs-BnMack9t.js.gz +0 -0
- package/dist/web/public/assets/{tasks-CY30H1u1.js → tasks-BkW7YShZ.js} +1 -1
- package/dist/web/public/assets/tasks-BkW7YShZ.js.br +0 -0
- package/dist/web/public/assets/tasks-BkW7YShZ.js.gz +0 -0
- package/dist/web/public/index.html +2 -2
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/push.js +4 -10
- package/dist/web/vault.js +68 -0
- package/dist/{extensions/web → websearch}/artifacts.js +2 -2
- package/dist/websearch/cli.js +73 -0
- package/dist/websearch/run.js +273 -0
- package/docs/deploy.md +34 -11
- package/package.json +2 -3
- package/skills/pier-help/SKILL.md +32 -16
- package/skills/pier-slack/SKILL.md +52 -71
- package/skills/pier-tasks/SKILL.md +60 -148
- package/skills/pier-vault/SKILL.md +36 -0
- package/skills/pier-web/SKILL.md +50 -0
- package/dist/channels/slack-tool.js +0 -416
- package/dist/channels/telegram-api.js +0 -86
- package/dist/channels/telegram-panel.js +0 -97
- package/dist/channels/telegram-render.js +0 -69
- package/dist/channels/telegram.js +0 -421
- package/dist/extensions/index.js +0 -10
- package/dist/extensions/web/index.js +0 -9
- package/dist/extensions/web/tools.js +0 -265
- package/dist/tasks/tool.js +0 -416
- package/dist/web/public/assets/activity-BSMeRcN2.js.br +0 -0
- package/dist/web/public/assets/activity-BSMeRcN2.js.gz +0 -0
- package/dist/web/public/assets/boards-BCWQMZry.js.br +0 -0
- package/dist/web/public/assets/boards-BCWQMZry.js.gz +0 -0
- package/dist/web/public/assets/explorer-Cr4XTi4j.js.br +0 -0
- package/dist/web/public/assets/explorer-Cr4XTi4j.js.gz +0 -0
- package/dist/web/public/assets/index-C7tA0Ufu.js +0 -85
- package/dist/web/public/assets/index-C7tA0Ufu.js.br +0 -0
- package/dist/web/public/assets/index-C7tA0Ufu.js.gz +0 -0
- package/dist/web/public/assets/index-DVIt5Gio.css +0 -2
- package/dist/web/public/assets/index-DVIt5Gio.css.br +0 -0
- package/dist/web/public/assets/index-DVIt5Gio.css.gz +0 -0
- package/dist/web/public/assets/runs-DdERzeac.js.br +0 -0
- package/dist/web/public/assets/runs-DdERzeac.js.gz +0 -0
- package/dist/web/public/assets/settings-CQDAHoMM.js +0 -5
- package/dist/web/public/assets/settings-CQDAHoMM.js.br +0 -0
- package/dist/web/public/assets/settings-CQDAHoMM.js.gz +0 -0
- package/dist/web/public/assets/task-runs-C-dGUDsH.js.br +0 -0
- package/dist/web/public/assets/task-runs-C-dGUDsH.js.gz +0 -0
- package/dist/web/public/assets/tasks-CY30H1u1.js.br +0 -0
- package/dist/web/public/assets/tasks-CY30H1u1.js.gz +0 -0
- /package/dist/{extensions/web → websearch}/anthropic.js +0 -0
- /package/dist/{extensions/web → websearch}/content.js +0 -0
- /package/dist/{extensions/web → websearch}/http.js +0 -0
- /package/dist/{extensions/web → websearch}/json.js +0 -0
- /package/dist/{extensions/web → websearch}/language.js +0 -0
- /package/dist/{extensions/web → websearch}/openai.js +0 -0
- /package/dist/{extensions/web → websearch}/provider.js +0 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// The `/task` socket route: every `pier task` operation — run, message, save,
|
|
2
|
+
// list, cancel, recover — validated here once, answered with the summaries a
|
|
3
|
+
// model reads. Scheduling and delivery stay in the service; this file decides
|
|
4
|
+
// who may ask for what.
|
|
5
|
+
import { isAbsolute, resolve } from "node:path";
|
|
6
|
+
import { logger } from "../log.js";
|
|
7
|
+
import { record, requiredString } from "./definitions.js";
|
|
8
|
+
import { isTerminal } from "./types.js";
|
|
9
|
+
const log = logger("tasks");
|
|
10
|
+
/** Absent instead of `null`: a model reads both the same way, and on a group
|
|
11
|
+
* summary the nulls are a third of the payload. The input names every field
|
|
12
|
+
* so a summary that forgot one cannot pass as "empty". */
|
|
13
|
+
const defined = (value) => Object.fromEntries(Object.entries(value).filter(([, v]) => v !== null && v !== undefined));
|
|
14
|
+
const summarize = (run) => defined({
|
|
15
|
+
runId: run.id,
|
|
16
|
+
taskId: run.taskId,
|
|
17
|
+
taskName: run.context.definition.name,
|
|
18
|
+
state: run.state,
|
|
19
|
+
triggerSource: run.triggerSource,
|
|
20
|
+
groupId: run.groupId,
|
|
21
|
+
sessionMode: run.sessionMode,
|
|
22
|
+
targetSessionId: run.targetSessionId,
|
|
23
|
+
callbackSessionId: run.callbackSessionId,
|
|
24
|
+
callbackState: run.callbackState,
|
|
25
|
+
callbackMode: run.callbackMode ?? null,
|
|
26
|
+
queuedAt: run.queuedAt,
|
|
27
|
+
startedAt: run.startedAt,
|
|
28
|
+
finishedAt: run.finishedAt,
|
|
29
|
+
result: run.result,
|
|
30
|
+
error: run.error,
|
|
31
|
+
skipReason: run.skipReason,
|
|
32
|
+
next: null,
|
|
33
|
+
});
|
|
34
|
+
/** The receipt says the callback is the whole answer, or a model that just
|
|
35
|
+
* launched work reaches for a status call. */
|
|
36
|
+
const receipt = (summary, callbackSessionId, mode, callerSessionId) => ({
|
|
37
|
+
...summary,
|
|
38
|
+
next: callbackSessionId === null
|
|
39
|
+
? "callback none: the result is not delivered to anyone"
|
|
40
|
+
: callbackSessionId !== callerSessionId
|
|
41
|
+
? `the result is delivered to session ${callbackSessionId}; this session will not receive a callback`
|
|
42
|
+
: mode === "steer"
|
|
43
|
+
? "the result interrupts your running turn as a steer message; nothing to query"
|
|
44
|
+
: "the result arrives as a callback message once your turn ends; nothing to query",
|
|
45
|
+
});
|
|
46
|
+
/** A typo in an enum field must not silently mean its default. */
|
|
47
|
+
const oneOf = (input, key, values) => {
|
|
48
|
+
const value = input[key];
|
|
49
|
+
if (value === undefined || values.includes(value))
|
|
50
|
+
return value;
|
|
51
|
+
throw new Error(`${key} must be one of ${values.join(", ")}`);
|
|
52
|
+
};
|
|
53
|
+
const callbackModeOf = (input) => oneOf(input, "callback", ["origin", "none", "steer"]) === "steer" ? "steer" : "followUp";
|
|
54
|
+
/** Who a new run's result goes to — the caller, nobody, or a named session that
|
|
55
|
+
* must exist. Shared by `run` and `message` on a finished run: a resumed run is a new run. */
|
|
56
|
+
const callbackTarget = async (input, definitions, callerSessionId) => {
|
|
57
|
+
if (input.callback_session_id === undefined)
|
|
58
|
+
return input.callback === "none" ? null : callerSessionId;
|
|
59
|
+
if (input.callback === "none")
|
|
60
|
+
throw new Error("callback none and callback_session_id conflict: pick one delivery target");
|
|
61
|
+
const target = requiredString(input.callback_session_id, "callback_session_id");
|
|
62
|
+
if (!(await definitions.sessionExists(target)))
|
|
63
|
+
throw new Error(`unknown session: ${target}`);
|
|
64
|
+
return target;
|
|
65
|
+
};
|
|
66
|
+
/** A group echoes many results at once, so each is capped; a single-run
|
|
67
|
+
* `recover` stays whole — it is the escape hatch every truncation note points at. */
|
|
68
|
+
const trimResult = (summary) => {
|
|
69
|
+
if (summary.result?.type !== "agent" || summary.result.text.length <= 2000)
|
|
70
|
+
return summary;
|
|
71
|
+
return {
|
|
72
|
+
...summary,
|
|
73
|
+
result: {
|
|
74
|
+
...summary.result,
|
|
75
|
+
text: `${summary.result.text.slice(0, 2000)}\n[truncated — pier task recover --run ${summary.runId} --reason … for the full text]`,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
/** Delivered, given up on and reported, or never owed (`callback:"none"`).
|
|
80
|
+
* Anything else is still on its way, and reading it here would be reading it twice. */
|
|
81
|
+
const settled = (callback) => callback.callbackState === null || callback.callbackState === "delivered" || callback.callbackState === "abandoned";
|
|
82
|
+
/** The same words for queued, running, pending and retrying: a refusal that
|
|
83
|
+
* named the state would be the status query this surface refuses. */
|
|
84
|
+
const notRecoverable = (what, callback) => {
|
|
85
|
+
throw new Error(callback.callbackSessionId === null
|
|
86
|
+
? `${what} is not recoverable yet: it was launched with callback none, so nothing will be delivered; recover reads finished results only and cannot wait for work`
|
|
87
|
+
: `${what} is not recoverable yet: wait for automatic delivery to session ${callback.callbackSessionId}; recover cannot wait for work`);
|
|
88
|
+
};
|
|
89
|
+
const summarizeGroup = (group, members) => defined({
|
|
90
|
+
groupId: group.id,
|
|
91
|
+
join: group.join,
|
|
92
|
+
state: group.finishedAt ? "finished" : "running",
|
|
93
|
+
callbackState: group.callbackState,
|
|
94
|
+
callbackMode: group.callbackMode ?? null,
|
|
95
|
+
winnerRunId: group.winnerRunId,
|
|
96
|
+
members: members.map((run) => trimResult(summarize(run))),
|
|
97
|
+
next: null,
|
|
98
|
+
});
|
|
99
|
+
const CANCEL_WAIT_MS = 2000;
|
|
100
|
+
/** The run once terminal, or as it stands after `ms`. */
|
|
101
|
+
function cancelledOrCurrent(host, id, ms) {
|
|
102
|
+
return Promise.race([
|
|
103
|
+
host.waitForRun(id),
|
|
104
|
+
new Promise((resolve) => setTimeout(() => resolve(host.getRun(id)), ms).unref()),
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
export async function handleTask(host, definitions, store, raw, callerSessionId) {
|
|
108
|
+
const input = record(raw);
|
|
109
|
+
if (!input)
|
|
110
|
+
throw new Error("task parameters required");
|
|
111
|
+
// Delegation is one level (docs/design/09-tasks-cli.md §Two levels, no tree):
|
|
112
|
+
// what a supervised run launched would report to a session no run owns. A
|
|
113
|
+
// queued run has not taken the session's turn, so it gates nothing yet.
|
|
114
|
+
const active = store.findActiveRunForTarget(callerSessionId);
|
|
115
|
+
if (active?.state === "running" && store.supervised(active))
|
|
116
|
+
throw new Error("a delegated run cannot delegate; ask in your result and let your supervisor run it");
|
|
117
|
+
const menu = () => host.models().then((listed) => listed.models);
|
|
118
|
+
if (input.operation === "list")
|
|
119
|
+
return definitions.list().filter((task) => task.kind !== "subagent");
|
|
120
|
+
if (input.operation === "save") {
|
|
121
|
+
const draft = await expandDraft(definitions, menu, input.task, callerSessionId);
|
|
122
|
+
if (input.task_id === undefined)
|
|
123
|
+
return definitions.create(draft, `session:${callerSessionId}`);
|
|
124
|
+
const id = requiredString(input.task_id, "task_id");
|
|
125
|
+
// A one-shot's hidden definition is a run's record, not a task anyone filed.
|
|
126
|
+
if (definitions.get(id).kind === "subagent")
|
|
127
|
+
throw new Error(`${id} is a one-shot run's own definition; save without --task-id to file a task`);
|
|
128
|
+
return definitions.update(id, draft);
|
|
129
|
+
}
|
|
130
|
+
if (input.operation === "run") {
|
|
131
|
+
// `--model ?`: the menu instead of a run, the one lookup the common case never pays.
|
|
132
|
+
if (record(input.launch)?.model === "?") {
|
|
133
|
+
const { source, models } = await host.models();
|
|
134
|
+
const head = source === "menu" ? "the operator's menu" : "the live catalog (no model pinned)";
|
|
135
|
+
return `${head} — --model takes a provider/id or a unique substring of one:\n${menuLines(models)}`;
|
|
136
|
+
}
|
|
137
|
+
const callbackMode = callbackModeOf(input);
|
|
138
|
+
if (Array.isArray(input.tasks)) {
|
|
139
|
+
// Core-joined fan-out: members run detached, one aggregated callback.
|
|
140
|
+
if (input.task !== undefined || input.task_id !== undefined)
|
|
141
|
+
throw new Error("use either task/task_id or tasks[]");
|
|
142
|
+
if (input.callback_session_id !== undefined)
|
|
143
|
+
throw new Error("callback_session_id applies to a single run only");
|
|
144
|
+
if (input.tasks.length < 2)
|
|
145
|
+
throw new Error("tasks[] needs at least 2 entries; use task for a single run");
|
|
146
|
+
const resolved = [];
|
|
147
|
+
for (const rawEntry of input.tasks) {
|
|
148
|
+
const entry = typeof rawEntry === "string" ? { prompt: rawEntry } : record(rawEntry);
|
|
149
|
+
if (!entry)
|
|
150
|
+
throw new Error("invalid tasks[] entry");
|
|
151
|
+
resolved.push(entry.task_id === undefined
|
|
152
|
+
? await resolveDraft(definitions, menu, entry, callerSessionId)
|
|
153
|
+
: definitions.get(requiredString(entry.task_id, "task_id")));
|
|
154
|
+
}
|
|
155
|
+
const groupCallbackSessionId = input.callback === "none" ? null : callerSessionId;
|
|
156
|
+
const { group, runs } = host.runGroup(resolved, oneOf(input, "join", ["all", "first"]) ?? "all", callerSessionId, groupCallbackSessionId, callbackMode);
|
|
157
|
+
return receipt(summarizeGroup(group, runs), groupCallbackSessionId, callbackMode, callerSessionId);
|
|
158
|
+
}
|
|
159
|
+
const draft = input.task_id === undefined ? inlineDraft(input) : undefined;
|
|
160
|
+
const task = draft
|
|
161
|
+
? await resolveDraft(definitions, menu, draft, callerSessionId)
|
|
162
|
+
: definitions.get(requiredString(input.task_id, "task_id"));
|
|
163
|
+
const callbackSessionId = await callbackTarget(input, definitions, callerSessionId);
|
|
164
|
+
const run = host.run(task.id, null, "agent", null, {
|
|
165
|
+
invokedBySessionId: callerSessionId,
|
|
166
|
+
sourceSessionId: callerSessionId,
|
|
167
|
+
callbackSessionId,
|
|
168
|
+
callbackMode,
|
|
169
|
+
background: true,
|
|
170
|
+
});
|
|
171
|
+
return receipt(summarize(run), callbackSessionId, callbackMode, callerSessionId);
|
|
172
|
+
}
|
|
173
|
+
if (input.operation === "recover") {
|
|
174
|
+
// History only, never status: readable once the callback has said its last
|
|
175
|
+
// word. The required reason is the friction, and the operator sees it.
|
|
176
|
+
const reason = requiredString(input.reason, "reason");
|
|
177
|
+
// A race winner need not wait for losing members to finish cancelling.
|
|
178
|
+
const groupReady = (group) => {
|
|
179
|
+
if (!group.finishedAt || !settled(group))
|
|
180
|
+
notRecoverable(`group ${group.id}`, group);
|
|
181
|
+
};
|
|
182
|
+
if (typeof input.group_id === "string") {
|
|
183
|
+
const { group, members } = host.getGroup(input.group_id);
|
|
184
|
+
groupReady(group);
|
|
185
|
+
if (!members.every((run) => isTerminal(run.state))) {
|
|
186
|
+
throw new Error(`recover cannot inspect active members; the race has settled — recover its winning result with --run ${group.winnerRunId ?? "<id from the callback>"} and a reason`);
|
|
187
|
+
}
|
|
188
|
+
log.info(`recover group ${group.id} by ${callerSessionId}: ${reason}`);
|
|
189
|
+
return summarizeGroup(group, members);
|
|
190
|
+
}
|
|
191
|
+
const run = host.getRun(requiredString(input.run_id, "run_id"));
|
|
192
|
+
if (run.groupId) {
|
|
193
|
+
const { group } = host.getGroup(run.groupId);
|
|
194
|
+
groupReady(group);
|
|
195
|
+
if (!isTerminal(run.state))
|
|
196
|
+
throw new Error("the group has reported its outcome; recover reads finished results only and cannot wait for this member");
|
|
197
|
+
}
|
|
198
|
+
else if (!isTerminal(run.state) || !settled(run)) {
|
|
199
|
+
notRecoverable(`run ${run.id}`, run);
|
|
200
|
+
}
|
|
201
|
+
log.info(`recover run ${run.id} by ${callerSessionId}: ${reason}`);
|
|
202
|
+
return summarize(run);
|
|
203
|
+
}
|
|
204
|
+
if (input.operation === "cancel") {
|
|
205
|
+
if (typeof input.group_id === "string") {
|
|
206
|
+
const { group, members } = host.getGroup(input.group_id);
|
|
207
|
+
for (const member of members)
|
|
208
|
+
assertOwns(callerSessionId, member);
|
|
209
|
+
const cancelled = host.cancelGroup(group.id);
|
|
210
|
+
return summarizeGroup(cancelled, cancelled.memberRunIds.map((id) => host.getRun(id)));
|
|
211
|
+
}
|
|
212
|
+
const run = host.getRun(requiredString(input.run_id, "run_id"));
|
|
213
|
+
assertOwns(callerSessionId, run);
|
|
214
|
+
// cancel() only aborts; the row flips ~100 ms later. A receipt still saying
|
|
215
|
+
// `running` reads as "cancel failed", so wait for the flip, briefly.
|
|
216
|
+
host.cancel(run.id);
|
|
217
|
+
return summarize(await cancelledOrCurrent(host, run.id, CANCEL_WAIT_MS));
|
|
218
|
+
}
|
|
219
|
+
if (input.operation === "message") {
|
|
220
|
+
// The one request `pier task run --run` sends: the run's state, not the
|
|
221
|
+
// caller, decides whether the text steers, queues or resumes, so a status
|
|
222
|
+
// query never has to exist.
|
|
223
|
+
const run = host.getRun(requiredString(input.run_id, "run_id"));
|
|
224
|
+
assertOwns(callerSessionId, run);
|
|
225
|
+
const message = requiredString(input.message, "message");
|
|
226
|
+
if (isTerminal(run.state)) {
|
|
227
|
+
// A resumed run is a new run with its own callback.
|
|
228
|
+
const callbackMode = callbackModeOf(input);
|
|
229
|
+
const callbackSessionId = await callbackTarget(input, definitions, callerSessionId);
|
|
230
|
+
const resumed = host.resume(run.id, message, { invokedBySessionId: callerSessionId, callbackSessionId, callbackMode, background: true });
|
|
231
|
+
return { delivery: "resume", run: receipt(summarize(resumed), callbackSessionId, callbackMode, callerSessionId) };
|
|
232
|
+
}
|
|
233
|
+
if (input.callback !== undefined || input.callback_session_id !== undefined) {
|
|
234
|
+
throw new Error(`run ${run.id} is ${run.state}: callback options apply to a resumed run only; drop them to steer or follow up`);
|
|
235
|
+
}
|
|
236
|
+
const delivery = input.after === true ? "follow_up" : "steer";
|
|
237
|
+
return { delivery, message: await host.control(run.id, callerSessionId, delivery, message) };
|
|
238
|
+
}
|
|
239
|
+
throw new Error("unknown task operation");
|
|
240
|
+
}
|
|
241
|
+
/** A single run's draft: the top-level shorthand (`prompt` …) or `task`, never both. */
|
|
242
|
+
function inlineDraft(input) {
|
|
243
|
+
const { prompt, cwd, launch, name, timeoutSeconds } = input;
|
|
244
|
+
if (prompt === undefined)
|
|
245
|
+
return record(input.task) ?? undefined;
|
|
246
|
+
if (input.task !== undefined)
|
|
247
|
+
throw new Error("use either prompt or task");
|
|
248
|
+
return { prompt, cwd, launch, name, timeoutSeconds };
|
|
249
|
+
}
|
|
250
|
+
/** A label for the Console, not an identifier. */
|
|
251
|
+
function nameFrom(text) {
|
|
252
|
+
const line = text.split("\n")
|
|
253
|
+
.map((l) => l.replace(/^[\s#>*-]+/, "").replace(/[*_`]/g, "").replace(/\s+/g, " ").trim())
|
|
254
|
+
.find(Boolean) ?? "subagent";
|
|
255
|
+
return line.length > 60 ? `${line.slice(0, 59).trimEnd()}…` : line;
|
|
256
|
+
}
|
|
257
|
+
/** `launch.model` by name (docs/design/09-tasks-cli.md §Models). Anything but
|
|
258
|
+
* one pin or an unpinned `provider/id` is refused with the lines to pick
|
|
259
|
+
* from, so the agent never guesses an id. */
|
|
260
|
+
function resolveModel(name, menu) {
|
|
261
|
+
const needle = name.trim().toLowerCase();
|
|
262
|
+
const full = (pin) => `${pin.provider}/${pin.id}`;
|
|
263
|
+
const exact = menu.find((pin) => full(pin).toLowerCase() === needle);
|
|
264
|
+
const hits = exact ? [exact] : menu.filter((pin) => `${full(pin)} ${pin.note ?? ""}`.toLowerCase().includes(needle));
|
|
265
|
+
if (hits.length === 1)
|
|
266
|
+
return { model: { provider: hits[0].provider, id: hits[0].id }, thinking: hits[0].thinking };
|
|
267
|
+
const slash = name.indexOf("/");
|
|
268
|
+
if (!hits.length && slash > 0 && slash < name.length - 1)
|
|
269
|
+
return { model: { provider: name.slice(0, slash), id: name.slice(slash + 1) } };
|
|
270
|
+
throw new Error(`model "${name}" matches ${String(hits.length)} of the menu:\n${menuLines(hits.length ? hits : menu)}`);
|
|
271
|
+
}
|
|
272
|
+
/** One pin per line, the only shape a menu is ever printed in. */
|
|
273
|
+
const menuLines = (menu) => menu.map((pin) => `${pin.provider}/${pin.id}${pin.thinking ? ` · ${pin.thinking}` : ""}${pin.note ? ` — ${pin.note}` : ""}`).join("\n")
|
|
274
|
+
|| "(no model is pinned or available)";
|
|
275
|
+
/** A `prompt` shorthand becomes a fresh Agent action in the caller's own
|
|
276
|
+
* directory; everything the caller did spell out passes through to parseDraft. */
|
|
277
|
+
async function expandDraft(definitions, menu, raw, callerSessionId) {
|
|
278
|
+
let draft = record(raw);
|
|
279
|
+
if (!draft)
|
|
280
|
+
return raw;
|
|
281
|
+
if (typeof draft.prompt === "string") {
|
|
282
|
+
if (draft.action !== undefined)
|
|
283
|
+
throw new Error("use either prompt (shorthand) or action");
|
|
284
|
+
const { prompt, cwd, launch, name, ...rest } = draft;
|
|
285
|
+
draft = { ...rest, name, action: { type: "agent", session: { mode: "fresh", cwd }, prompt, launch } };
|
|
286
|
+
}
|
|
287
|
+
const action = record(draft.action);
|
|
288
|
+
const session = record(action?.session);
|
|
289
|
+
const absolute = async (cwd) => {
|
|
290
|
+
const base = await definitions.sessionCwd(callerSessionId);
|
|
291
|
+
if (!base)
|
|
292
|
+
throw new Error(`cwd ${cwd === undefined ? "omitted" : `"${String(cwd)}" is relative`} and the calling session has no working directory; give an absolute path`);
|
|
293
|
+
return resolve(base, typeof cwd === "string" ? cwd : ".");
|
|
294
|
+
};
|
|
295
|
+
const relative = (cwd) => cwd === undefined || (typeof cwd === "string" && !isAbsolute(cwd));
|
|
296
|
+
if (action?.type === "agent" && session?.mode === "fresh" && relative(session.cwd)) {
|
|
297
|
+
draft = { ...draft, action: { ...action, session: { ...session, cwd: await absolute(session.cwd) } } };
|
|
298
|
+
}
|
|
299
|
+
if (action?.type === "bash" && relative(action.cwd)) {
|
|
300
|
+
draft = { ...draft, action: { ...action, cwd: await absolute(action.cwd) } };
|
|
301
|
+
}
|
|
302
|
+
const label = action?.prompt ?? action?.script;
|
|
303
|
+
if (draft.name === undefined && typeof label === "string")
|
|
304
|
+
draft = { ...draft, name: nameFrom(label) };
|
|
305
|
+
const launch = record(action?.launch);
|
|
306
|
+
if (typeof launch?.model === "string") {
|
|
307
|
+
const { model, thinking } = resolveModel(launch.model, await menu());
|
|
308
|
+
const resolved = { ...launch, model, ...(launch.thinking === undefined && thinking ? { thinking } : {}) };
|
|
309
|
+
draft = { ...draft, action: { ...record(draft.action), launch: resolved } };
|
|
310
|
+
}
|
|
311
|
+
return draft;
|
|
312
|
+
}
|
|
313
|
+
/** Persisted like any task (kind "subagent", filtered from default lists) so
|
|
314
|
+
* runs stay auditable and resumable. */
|
|
315
|
+
async function resolveDraft(definitions, menu, raw, callerSessionId) {
|
|
316
|
+
const draft = record(await expandDraft(definitions, menu, raw, callerSessionId));
|
|
317
|
+
if (!draft)
|
|
318
|
+
throw new Error("task definition required");
|
|
319
|
+
if (draft.trigger !== undefined && record(draft.trigger)?.type !== "manual") {
|
|
320
|
+
throw new Error("inline subagent tasks must use a manual trigger");
|
|
321
|
+
}
|
|
322
|
+
// Delivery of a one-off run is the top-level fields' business; a nested
|
|
323
|
+
// callback only means anything on a stored definition's schedule.
|
|
324
|
+
if (draft.callback !== undefined || draft.callback_session_id !== undefined) {
|
|
325
|
+
throw new Error("an inline task draft cannot set callback; use the top-level callback / callback_session_id");
|
|
326
|
+
}
|
|
327
|
+
return definitions.create({ ...draft, trigger: { type: "manual" } }, `session:${callerSessionId}`, "subagent");
|
|
328
|
+
}
|
|
329
|
+
/** The session that launched a run controls it, and so does the run's own
|
|
330
|
+
* session; a `task` action's child inherits its parent's launcher. */
|
|
331
|
+
function assertOwns(callerSessionId, target) {
|
|
332
|
+
if (target.invokedBySessionId !== callerSessionId && target.targetSessionId !== callerSessionId) {
|
|
333
|
+
throw new Error("session does not own this run");
|
|
334
|
+
}
|
|
335
|
+
}
|
package/dist/tasks/routes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The area's HTTP surface. A route names the caller and hands the decision to
|
|
2
|
-
// TaskService: policy here would be policy
|
|
2
|
+
// TaskService: policy here would be policy `pier task` does not get.
|
|
3
3
|
import { record, requiredString } from "./definitions.js";
|
|
4
4
|
/** Reject malformed filters instead of silently widening a global query. */
|
|
5
5
|
function runQuery(params) {
|
|
@@ -184,16 +184,6 @@ export function registerTaskRoutes(app, tasks, activity) {
|
|
|
184
184
|
return c.json({ error: String(err) }, 400);
|
|
185
185
|
}
|
|
186
186
|
});
|
|
187
|
-
app.post("/api/task-messages/:id/reply", async (c) => {
|
|
188
|
-
const body = record(await jsonBody(c.req));
|
|
189
|
-
try {
|
|
190
|
-
const source = requiredString(body?.sourceSessionId, "sourceSessionId");
|
|
191
|
-
return c.json(await tasks.reply(c.req.param("id"), source, requiredString(body?.message, "message")), 202);
|
|
192
|
-
}
|
|
193
|
-
catch (err) {
|
|
194
|
-
return c.json({ error: String(err) }, 400);
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
187
|
app.post("/api/task-runs/:id/cancel", (c) => {
|
|
198
188
|
try {
|
|
199
189
|
return c.json(tasks.cancel(c.req.param("id")), 202);
|
package/dist/tasks/runs.js
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
// A definition plus an input becomes a queued run
|
|
2
|
-
//
|
|
1
|
+
// A definition plus an input becomes a queued run; every caller enqueues
|
|
2
|
+
// through this one door.
|
|
3
3
|
import { logger } from "../log.js";
|
|
4
4
|
import { newId } from "./definitions.js";
|
|
5
5
|
const log = logger("tasks");
|
|
6
|
-
const MAX_DEPTH = 2;
|
|
7
|
-
const MAX_CHILDREN_PER_ROOT = 16;
|
|
8
6
|
export class TaskRunQueue {
|
|
9
7
|
store;
|
|
10
8
|
callbacks;
|
|
11
|
-
getRun;
|
|
12
9
|
execute;
|
|
13
10
|
changed;
|
|
14
|
-
constructor(store, callbacks,
|
|
11
|
+
constructor(store, callbacks, execute, changed) {
|
|
15
12
|
this.store = store;
|
|
16
13
|
this.callbacks = callbacks;
|
|
17
|
-
this.getRun = getRun;
|
|
18
14
|
this.execute = execute;
|
|
19
15
|
this.changed = changed;
|
|
20
16
|
}
|
|
21
17
|
/** Validate and persist only; callers may stage several runs in a transaction. */
|
|
22
18
|
prepare(definition, input, source, parentRunId, provenance) {
|
|
23
19
|
const id = newId();
|
|
24
|
-
const parent = parentRunId ? this.getRun(parentRunId) : null;
|
|
25
|
-
const depth = provenance.depth ?? (parent ? parent.depth + 1 : 0);
|
|
26
|
-
const rootRunId = provenance.rootRunId ?? parent?.rootRunId ?? id;
|
|
27
|
-
if (depth > MAX_DEPTH)
|
|
28
|
-
throw new Error(`subagent depth limit is ${MAX_DEPTH}`);
|
|
29
|
-
if (depth > 0 && this.store.listRunsByRoot(rootRunId, MAX_CHILDREN_PER_ROOT + 1).filter((run) => run.depth > 0).length >= MAX_CHILDREN_PER_ROOT) {
|
|
30
|
-
throw new Error(`subagent child limit is ${MAX_CHILDREN_PER_ROOT} per root run`);
|
|
31
|
-
}
|
|
32
20
|
const invokedBySessionId = provenance.invokedBySessionId ?? null;
|
|
33
21
|
const sourceSessionId = provenance.sourceSessionId ?? invokedBySessionId;
|
|
34
22
|
const sessionMode = definition.action.type === "agent"
|
|
@@ -50,8 +38,6 @@ export class TaskRunQueue {
|
|
|
50
38
|
taskRevision: definition.revision,
|
|
51
39
|
parentRunId,
|
|
52
40
|
groupId: provenance.groupId ?? null,
|
|
53
|
-
rootRunId,
|
|
54
|
-
depth,
|
|
55
41
|
resumedFromRunId: provenance.resumedFromRunId ?? null,
|
|
56
42
|
triggerSource: source,
|
|
57
43
|
invokedBySessionId,
|
|
@@ -86,13 +72,12 @@ export class TaskRunQueue {
|
|
|
86
72
|
/** Publish and execute only after all records this run relies on committed. */
|
|
87
73
|
start(run) {
|
|
88
74
|
this.changed(run);
|
|
89
|
-
const { id,
|
|
75
|
+
const { id, triggerSource: source } = run;
|
|
90
76
|
const overlapped = run.skipReason === "overlap";
|
|
91
77
|
const definition = run.context.definition;
|
|
92
78
|
// Why a run exists is answerable only here: the row keeps the ids, not the
|
|
93
79
|
// reason. A watch probe queues every interval, so it logs at debug.
|
|
94
|
-
const queued = `run ${id} ${run.state}: ${definition.name} via ${source}
|
|
95
|
-
`${overlapped ? " (overlapped)" : ""}${depth > 0 ? ` depth ${String(depth)}` : ""}`;
|
|
80
|
+
const queued = `run ${id} ${run.state}: ${definition.name} via ${source}${overlapped ? " (overlapped)" : ""}`;
|
|
96
81
|
if (source === "watch" && !overlapped)
|
|
97
82
|
log.debug(queued);
|
|
98
83
|
else
|
package/dist/tasks/service.js
CHANGED
|
@@ -9,7 +9,7 @@ import { TaskExecution } from "./execution.js";
|
|
|
9
9
|
import { TaskGroups } from "./groups.js";
|
|
10
10
|
import { TaskMessenger } from "./messages.js";
|
|
11
11
|
import { TaskRunQueue } from "./runs.js";
|
|
12
|
-
import {
|
|
12
|
+
import { handleTask } from "./operations.js";
|
|
13
13
|
import { isTerminal } from "./types.js";
|
|
14
14
|
const log = logger("tasks");
|
|
15
15
|
/** Not `renderedPrompt`: that carries the preamble and input wrapper, which the
|
|
@@ -44,14 +44,13 @@ export class TaskService {
|
|
|
44
44
|
this.hub = hub;
|
|
45
45
|
this.instance = instance;
|
|
46
46
|
const unreachable = (sessionId, what, why) => this.unreachable(sessionId, what, why);
|
|
47
|
-
this.messages = new TaskMessenger(store, router, hub,
|
|
47
|
+
this.messages = new TaskMessenger(store, router, hub, unreachable);
|
|
48
48
|
this.definitions = new TaskDefinitions(store, factory, router, hub, instance?.systemActions);
|
|
49
49
|
this.callbacks = new TaskCallbacks(store, router, (run) => this.changed(run), unreachable);
|
|
50
50
|
this.groups = new TaskGroups(store, router, {
|
|
51
51
|
getRun: (id) => this.getRun(id),
|
|
52
52
|
cancel: (id) => { this.cancel(id); },
|
|
53
|
-
|
|
54
|
-
prepareMember: (taskId, groupId, callerSessionId, parentRunId) => this.prepareRun(taskId, null, "agent", parentRunId, {
|
|
53
|
+
prepareMember: (taskId, groupId, callerSessionId) => this.prepareRun(taskId, null, "agent", null, {
|
|
55
54
|
invokedBySessionId: callerSessionId,
|
|
56
55
|
sourceSessionId: callerSessionId,
|
|
57
56
|
callbackSessionId: null,
|
|
@@ -72,9 +71,8 @@ export class TaskService {
|
|
|
72
71
|
cancel: (id) => { this.cancel(id); },
|
|
73
72
|
settled: (run) => this.settled(run),
|
|
74
73
|
changed: (run) => this.changed(run),
|
|
75
|
-
openDecisionId: (runId) => this.messages.openDecisionId(runId),
|
|
76
74
|
});
|
|
77
|
-
this.runs = new TaskRunQueue(store, this.callbacks, (
|
|
75
|
+
this.runs = new TaskRunQueue(store, this.callbacks, (run) => this.execution.start(run), (run) => this.changed(run));
|
|
78
76
|
}
|
|
79
77
|
start(tickMs = 1000) {
|
|
80
78
|
if (this.timer)
|
|
@@ -147,7 +145,7 @@ export class TaskService {
|
|
|
147
145
|
create(raw, creator = "http") {
|
|
148
146
|
return this.definitions.create(raw, creator);
|
|
149
147
|
}
|
|
150
|
-
/** `by` is how owning code says so; the routes and
|
|
148
|
+
/** `by` is how owning code says so; the routes and `pier task` have none (definitions.ts). */
|
|
151
149
|
update(id, raw, by) {
|
|
152
150
|
return this.definitions.update(id, raw, by);
|
|
153
151
|
}
|
|
@@ -169,16 +167,13 @@ export class TaskService {
|
|
|
169
167
|
}
|
|
170
168
|
getRunView(id) {
|
|
171
169
|
const run = this.getRun(id);
|
|
172
|
-
return { ...run,
|
|
170
|
+
return { ...run,
|
|
173
171
|
groupCallbackState: run.groupId ? this.store.getGroup(run.groupId)?.callbackState ?? null : null };
|
|
174
172
|
}
|
|
175
173
|
listMessages(runId) {
|
|
176
174
|
this.getRun(runId);
|
|
177
175
|
return this.messages.list(runId);
|
|
178
176
|
}
|
|
179
|
-
openDecisionId(runId) {
|
|
180
|
-
return this.messages.openDecisionId(runId);
|
|
181
|
-
}
|
|
182
177
|
queryRuns(query = {}) {
|
|
183
178
|
return this.store.queryRuns(query);
|
|
184
179
|
}
|
|
@@ -227,7 +222,8 @@ export class TaskService {
|
|
|
227
222
|
set.add(resolve);
|
|
228
223
|
});
|
|
229
224
|
}
|
|
230
|
-
/** Cascades:
|
|
225
|
+
/** Cascades down a `task` action's chain: a child must not outlive the run
|
|
226
|
+
* that waits on it. */
|
|
231
227
|
cancel(id) {
|
|
232
228
|
const run = this.getRun(id);
|
|
233
229
|
for (const target of [run, ...this.descendants(run)]) {
|
|
@@ -242,23 +238,15 @@ export class TaskService {
|
|
|
242
238
|
getGroup(id) {
|
|
243
239
|
return this.groups.members(id);
|
|
244
240
|
}
|
|
245
|
-
runGroup(definitions, join, callerSessionId,
|
|
246
|
-
this.refusePaused(
|
|
247
|
-
return this.groups.runAll(definitions, join, callerSessionId,
|
|
241
|
+
runGroup(definitions, join, callerSessionId, callbackSessionId, callbackMode) {
|
|
242
|
+
this.refusePaused();
|
|
243
|
+
return this.groups.runAll(definitions, join, callerSessionId, callbackSessionId, callbackMode);
|
|
248
244
|
}
|
|
249
245
|
descendants(run) {
|
|
250
|
-
const byParent = new Map();
|
|
251
|
-
for (const member of this.store.listRunsByRoot(run.rootRunId, 500)) {
|
|
252
|
-
if (!member.parentRunId)
|
|
253
|
-
continue;
|
|
254
|
-
const siblings = byParent.get(member.parentRunId) ?? [];
|
|
255
|
-
siblings.push(member);
|
|
256
|
-
byParent.set(member.parentRunId, siblings);
|
|
257
|
-
}
|
|
258
246
|
const collected = [];
|
|
259
247
|
const queue = [run.id];
|
|
260
248
|
while (queue.length > 0) {
|
|
261
|
-
for (const child of
|
|
249
|
+
for (const child of this.store.listChildRuns(queue.shift())) {
|
|
262
250
|
collected.push(child);
|
|
263
251
|
queue.push(child.id);
|
|
264
252
|
}
|
|
@@ -273,20 +261,7 @@ export class TaskService {
|
|
|
273
261
|
throw new Error("terminal run cannot be steered; resume it instead");
|
|
274
262
|
return this.messages.control(run, fromSessionId, mode, message);
|
|
275
263
|
}
|
|
276
|
-
reply(messageId, fromSessionId, message) {
|
|
277
|
-
return this.messages.reply(messageId, fromSessionId, message);
|
|
278
|
-
}
|
|
279
264
|
resume(id, message, provenance = {}) {
|
|
280
|
-
const { run, expired } = this.store.transact(() => ({
|
|
281
|
-
run: this.prepareResume(id, message, provenance),
|
|
282
|
-
expired: this.messages.expireDecisions(id, "superseded by a manual resume"),
|
|
283
|
-
}));
|
|
284
|
-
for (const message of expired)
|
|
285
|
-
this.messages.changed(message);
|
|
286
|
-
this.runs.start(run);
|
|
287
|
-
return run;
|
|
288
|
-
}
|
|
289
|
-
prepareResume(id, message, provenance) {
|
|
290
265
|
this.refusePaused();
|
|
291
266
|
const prior = this.getRun(id);
|
|
292
267
|
if (!isTerminal(prior.state))
|
|
@@ -295,19 +270,20 @@ export class TaskService {
|
|
|
295
270
|
throw new Error("only persisted Agent runs can be resumed");
|
|
296
271
|
}
|
|
297
272
|
const prompt = requiredString(message, "message");
|
|
298
|
-
|
|
273
|
+
const run = this.runs.prepare(prior.context.definition, null, "agent", null, {
|
|
299
274
|
...provenance,
|
|
300
275
|
sourceSessionId: provenance.invokedBySessionId ?? prior.invokedBySessionId,
|
|
301
276
|
targetSessionId: prior.targetSessionId,
|
|
302
277
|
sessionMode: "reuse",
|
|
303
278
|
resumedFromRunId: prior.id,
|
|
304
|
-
rootRunId: prior.rootRunId,
|
|
305
|
-
depth: prior.depth,
|
|
306
279
|
resumePrompt: prompt,
|
|
307
280
|
});
|
|
281
|
+
this.runs.start(run);
|
|
282
|
+
return run;
|
|
308
283
|
}
|
|
309
|
-
|
|
310
|
-
|
|
284
|
+
/** What `pier task` asks, under the calling session's identity. */
|
|
285
|
+
handle(raw, callerSessionId) {
|
|
286
|
+
return handleTask(this, this.definitions, this.store, raw, callerSessionId);
|
|
311
287
|
}
|
|
312
288
|
/** An agent picks from names that exist right now, never from memory. */
|
|
313
289
|
async models() {
|
|
@@ -375,7 +351,6 @@ export class TaskService {
|
|
|
375
351
|
state: run.state,
|
|
376
352
|
targetSessionId: run.targetSessionId,
|
|
377
353
|
sessionMode: run.sessionMode,
|
|
378
|
-
depth: run.depth,
|
|
379
354
|
prompt: runPrompt(run),
|
|
380
355
|
queuedAt: run.queuedAt,
|
|
381
356
|
startedAt: run.startedAt,
|
package/dist/tasks/store.js
CHANGED
|
@@ -104,16 +104,13 @@ export class TaskStore {
|
|
|
104
104
|
}
|
|
105
105
|
const limit = clamp(query.limit ?? 50, 200);
|
|
106
106
|
const rows = this.db.prepare(`
|
|
107
|
-
SELECT r.json, g.callback_state AS group_callback_state
|
|
108
|
-
(SELECT m.id FROM task_messages m WHERE m.run_id = r.id
|
|
109
|
-
AND m.state IN ('pending', 'delivered') AND json_extract(m.json, '$.kind') = 'decision'
|
|
110
|
-
ORDER BY m.created_at, m.id LIMIT 1) AS decision_id
|
|
107
|
+
SELECT r.json, g.callback_state AS group_callback_state
|
|
111
108
|
FROM task_runs r LEFT JOIN task_groups g ON g.id = json_extract(r.json, '$.groupId')
|
|
112
109
|
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
|
|
113
110
|
ORDER BY r.queued_at DESC, r.id DESC LIMIT ?
|
|
114
111
|
`).all(...params, limit + 1);
|
|
115
112
|
const runs = rows.slice(0, limit).map((row) => ({
|
|
116
|
-
...JSON.parse(row.json),
|
|
113
|
+
...JSON.parse(row.json), groupCallbackState: row.group_callback_state,
|
|
117
114
|
}));
|
|
118
115
|
const last = runs.at(-1);
|
|
119
116
|
return {
|
|
@@ -134,12 +131,20 @@ export class TaskStore {
|
|
|
134
131
|
`, since);
|
|
135
132
|
return [...active, ...recent].sort((a, b) => b.queuedAt - a.queuedAt || b.id.localeCompare(a.id));
|
|
136
133
|
}
|
|
137
|
-
|
|
134
|
+
/** A `task` action's child runs; a cancel walks them. */
|
|
135
|
+
listChildRuns(parentRunId) {
|
|
138
136
|
return this.#many(`
|
|
139
137
|
SELECT json FROM task_runs
|
|
140
|
-
WHERE json_extract(json, '$.
|
|
141
|
-
ORDER BY queued_at LIMIT
|
|
142
|
-
`,
|
|
138
|
+
WHERE json_extract(json, '$.parentRunId') = ?
|
|
139
|
+
ORDER BY queued_at LIMIT 500
|
|
140
|
+
`, parentRunId);
|
|
141
|
+
}
|
|
142
|
+
/** Someone waits on this run's result — its own callback or its group's — so
|
|
143
|
+
* the run is one turn of its session and may not delegate (design 09). */
|
|
144
|
+
supervised(run) {
|
|
145
|
+
if (run.callbackSessionId !== null)
|
|
146
|
+
return true;
|
|
147
|
+
return run.groupId !== null && (this.getGroup(run.groupId)?.callbackSessionId ?? null) !== null;
|
|
143
148
|
}
|
|
144
149
|
countActiveRuns() {
|
|
145
150
|
const row = this.sql("SELECT COUNT(*) AS n FROM task_runs WHERE state IN ('queued', 'running')").get();
|
|
@@ -239,13 +244,8 @@ export class TaskStore {
|
|
|
239
244
|
run: row.run_json === null ? undefined : JSON.parse(row.run_json),
|
|
240
245
|
}));
|
|
241
246
|
}
|
|
242
|
-
/** Decisions are excluded: they have no timeout and stay answerable across
|
|
243
|
-
* restarts — a reply to a terminal run resumes it. */
|
|
244
247
|
expirePendingMessages() {
|
|
245
|
-
return this.#many(
|
|
246
|
-
SELECT json FROM task_messages
|
|
247
|
-
WHERE state = 'pending' AND json_extract(json, '$.kind') != 'decision'
|
|
248
|
-
`).map((message) => {
|
|
248
|
+
return this.#many("SELECT json FROM task_messages WHERE state = 'pending'").map((message) => {
|
|
249
249
|
message.state = "expired";
|
|
250
250
|
// "Confirmed", not "completed": the proof lives in a transcript this
|
|
251
251
|
// layer cannot see.
|