@timqi/pier 0.1.3 → 0.1.4
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 +8 -3
- package/dist/agent/config.js +9 -3
- package/dist/agent/events.js +1 -2
- package/dist/agent/packages.js +1 -7
- package/dist/agent/pi.js +6 -47
- package/dist/channels/config.js +30 -20
- package/dist/channels/routes.js +0 -3
- 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 +1 -1
- package/dist/channels/slack-thread.js +41 -0
- package/dist/channels/slack-transcript.js +107 -0
- package/dist/channels/slack.js +9 -17
- package/dist/channels/types.js +0 -1
- package/dist/cli.js +158 -7
- package/dist/core/identity.js +20 -11
- package/dist/core/reply.js +16 -15
- package/dist/core/router.js +4 -1
- package/dist/db.js +28 -0
- package/dist/main.js +19 -32
- package/dist/paths.js +3 -0
- package/dist/secrets.js +2 -1
- package/dist/socket.js +95 -0
- package/dist/tasks/agent.js +5 -6
- package/dist/tasks/callbacks.js +2 -2
- package/dist/tasks/cli.js +215 -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 +323 -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 +17 -0
- package/dist/vault.js +107 -0
- package/dist/web/auth.js +2 -1
- package/dist/web/public/assets/{activity-BSMeRcN2.js → activity-NLS8W9yl.js} +2 -2
- package/dist/web/public/assets/activity-NLS8W9yl.js.br +0 -0
- package/dist/web/public/assets/activity-NLS8W9yl.js.gz +0 -0
- package/dist/web/public/assets/{boards-BCWQMZry.js → boards-DleegoiC.js} +1 -1
- package/dist/web/public/assets/boards-DleegoiC.js.br +0 -0
- package/dist/web/public/assets/boards-DleegoiC.js.gz +0 -0
- package/dist/web/public/assets/{explorer-Cr4XTi4j.js → explorer-vJvV1Sx0.js} +1 -1
- package/dist/web/public/assets/explorer-vJvV1Sx0.js.br +0 -0
- package/dist/web/public/assets/explorer-vJvV1Sx0.js.gz +0 -0
- package/dist/web/public/assets/{index-DVIt5Gio.css → index-Bs-gol9o.css} +1 -1
- package/dist/web/public/assets/index-Bs-gol9o.css.br +0 -0
- package/dist/web/public/assets/{index-DVIt5Gio.css.gz → index-Bs-gol9o.css.gz} +0 -0
- package/dist/web/public/assets/index-Cyp2DKKF.js +85 -0
- package/dist/web/public/assets/index-Cyp2DKKF.js.br +0 -0
- package/dist/web/public/assets/index-Cyp2DKKF.js.gz +0 -0
- package/dist/web/public/assets/{runs-DdERzeac.js → runs-C5FveZ75.js} +1 -1
- package/dist/web/public/assets/runs-C5FveZ75.js.br +0 -0
- package/dist/web/public/assets/runs-C5FveZ75.js.gz +0 -0
- package/dist/web/public/assets/settings-nJHIBbAz.js +5 -0
- package/dist/web/public/assets/settings-nJHIBbAz.js.br +0 -0
- package/dist/web/public/assets/settings-nJHIBbAz.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-C-dGUDsH.js → task-runs-CJJ2j5Ks.js} +1 -1
- package/dist/web/public/assets/task-runs-CJJ2j5Ks.js.br +0 -0
- package/dist/web/public/assets/task-runs-CJJ2j5Ks.js.gz +0 -0
- package/dist/web/public/assets/{tasks-CY30H1u1.js → tasks-R2Pv0Abg.js} +1 -1
- package/dist/web/public/assets/tasks-R2Pv0Abg.js.br +0 -0
- package/dist/web/public/assets/tasks-R2Pv0Abg.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/vault.js +68 -0
- package/docs/deploy.md +33 -10
- package/package.json +1 -1
- package/skills/pier-help/SKILL.md +10 -6
- package/skills/pier-slack/SKILL.md +52 -71
- package/skills/pier-tasks/SKILL.md +48 -145
- package/skills/pier-vault/SKILL.md +36 -0
- package/dist/channels/slack-tool.js +0 -416
- 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.br +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/socket.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// The local socket the `pier` CLI reaches the running instance through: one
|
|
2
|
+
// route per verb the CLI has, the socket's permission bits as the whole auth.
|
|
3
|
+
// Not a workbench route — that server sits behind a proxy at a public
|
|
4
|
+
// hostname — so plaintext crosses this socket into a local process of Pier's
|
|
5
|
+
// user and nowhere else.
|
|
6
|
+
import { chmodSync, rmSync } from "node:fs";
|
|
7
|
+
import { createServer } from "node:http";
|
|
8
|
+
import { logger } from "./log.js";
|
|
9
|
+
import { PIER_SOCK } from "./paths.js";
|
|
10
|
+
import { isVaultName, UnknownSecret, VaultLocked } from "./vault.js";
|
|
11
|
+
const log = logger("socket");
|
|
12
|
+
/** Generous for a task draft; a `--prompt -` past it is told so, not hung up on. */
|
|
13
|
+
const MAX_BODY = 64 * 1024;
|
|
14
|
+
const ROUTES = {
|
|
15
|
+
async "/resolve"({ vault, fileUrl }, { names }, sessionId, answer) {
|
|
16
|
+
if (!Array.isArray(names) || !names.length || !names.every((n) => typeof n === "string" && isVaultName(n))) {
|
|
17
|
+
return answer(400, { error: "names must be a non-empty list of vault names" });
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
answer(200, { values: vault.resolve(names, `session ${sessionId}`) });
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
if (err instanceof UnknownSecret)
|
|
24
|
+
return answer(404, { error: err.message, file: fileUrl(err.secret) });
|
|
25
|
+
if (err instanceof VaultLocked)
|
|
26
|
+
return answer(423, { error: err.message });
|
|
27
|
+
log.error(`resolve ${names.join(",")} failed`, err);
|
|
28
|
+
answer(500, { error: String(err) });
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
// 422, not 400: the request was well-formed; what the operation refused is
|
|
32
|
+
// the caller's to read.
|
|
33
|
+
async "/task"({ task }, { params }, sessionId, answer) {
|
|
34
|
+
try {
|
|
35
|
+
answer(200, { result: await task(params, sessionId) });
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
answer(422, { error: err instanceof Error ? err.message : String(err) });
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
export function servePier(hosts, path = PIER_SOCK) {
|
|
43
|
+
const server = createServer((req, res) => void handle(hosts, req, res).catch((err) => {
|
|
44
|
+
log.error(`${req.method ?? "?"} ${req.url ?? "?"} failed`, err);
|
|
45
|
+
if (!res.headersSent)
|
|
46
|
+
res.writeHead(500).end(JSON.stringify({ error: String(err) }));
|
|
47
|
+
}));
|
|
48
|
+
// A crash leaves the old socket file behind, and listen() on it is EADDRINUSE.
|
|
49
|
+
rmSync(path, { force: true });
|
|
50
|
+
// bind() runs inside listen(): the umask covers the instant the file exists
|
|
51
|
+
// with default bits; the chmod sets the exact mode once it is bound. A bind
|
|
52
|
+
// that failed reports through the error event and left no file to chmod.
|
|
53
|
+
const umask = process.umask(0o077);
|
|
54
|
+
try {
|
|
55
|
+
server.listen(path, () => log.info(`cli socket at ${path}`));
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
process.umask(umask);
|
|
59
|
+
}
|
|
60
|
+
if (server.listening)
|
|
61
|
+
chmodSync(path, 0o600);
|
|
62
|
+
server.on("error", (err) => log.error(`cli socket ${path} failed`, err));
|
|
63
|
+
process.once("exit", () => rmSync(path, { force: true }));
|
|
64
|
+
return server;
|
|
65
|
+
}
|
|
66
|
+
async function handle(hosts, req, res) {
|
|
67
|
+
const answer = (status, body) => {
|
|
68
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
69
|
+
res.end(JSON.stringify(body));
|
|
70
|
+
};
|
|
71
|
+
const route = req.method === "POST" ? ROUTES[req.url ?? ""] : undefined;
|
|
72
|
+
if (!route)
|
|
73
|
+
return answer(404, { error: `POST ${Object.keys(ROUTES).join(" | ")} only` });
|
|
74
|
+
let raw = "";
|
|
75
|
+
for await (const chunk of req) {
|
|
76
|
+
raw += chunk.toString();
|
|
77
|
+
// Answered before the body is drained: leaving the loop destroys the
|
|
78
|
+
// request stream, and the response already queued still reaches the client.
|
|
79
|
+
if (raw.length > MAX_BODY)
|
|
80
|
+
return answer(413, { error: `body exceeds ${String(MAX_BODY / 1024)} KiB` });
|
|
81
|
+
}
|
|
82
|
+
let body;
|
|
83
|
+
try {
|
|
84
|
+
body = JSON.parse(raw);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return answer(400, { error: "body must be a JSON object" });
|
|
88
|
+
}
|
|
89
|
+
const { sessionId, ...fields } = typeof body === "object" && body !== null ? body : {};
|
|
90
|
+
if (typeof sessionId !== "string" || !sessionId)
|
|
91
|
+
return answer(400, { error: "PIER_SESSION_ID is required" });
|
|
92
|
+
if (!(await hosts.knows(sessionId)))
|
|
93
|
+
return answer(403, { error: `${sessionId} is not a session of this Pier` });
|
|
94
|
+
await route(hosts, fields, sessionId, answer);
|
|
95
|
+
}
|
package/dist/tasks/agent.js
CHANGED
|
@@ -10,19 +10,18 @@ const MAX_ACTIVE_AGENTS = 6;
|
|
|
10
10
|
const log = logger("tasks");
|
|
11
11
|
/** Every session gets the chat-surface contract, task runs included, so the
|
|
12
12
|
* delegation prompt says which of it does not apply. Skipped on resume. */
|
|
13
|
-
const preamble = (run) => {
|
|
13
|
+
const preamble = (run, supervised) => {
|
|
14
14
|
// A cron/watch task with a session callback is read by an agent too.
|
|
15
15
|
const audience = run.invokedBySessionId
|
|
16
16
|
? "read by the agent that delegated this run"
|
|
17
17
|
: run.callbackSessionId
|
|
18
18
|
? "read by the agent session it is delivered to"
|
|
19
19
|
: "read by the operator";
|
|
20
|
-
const contact = run.invokedBySessionId
|
|
21
|
-
? ' Mid-run, the task tool\'s contact operation reaches that agent: reason "progress" is fire-and-forget, "decision" waits for a reply — state what you await and end your turn.'
|
|
22
|
-
: "";
|
|
23
20
|
return `[Pier task run ${run.id} — "${run.context.definition.name}"] ` +
|
|
24
21
|
`Your final reply is recorded verbatim as the run result, ${audience}; ` +
|
|
25
|
-
`next-step buttons and file:// attachments do not render there
|
|
22
|
+
`next-step buttons and file:// attachments do not render there. A question only that reader can answer is your result: state it and end your turn; the answer resumes this session.` +
|
|
23
|
+
(supervised ? " You cannot delegate from here — `pier task` is refused; if the work needs another agent, say so in your result and your supervisor will run it." : "") +
|
|
24
|
+
"\n\n";
|
|
26
25
|
};
|
|
27
26
|
export class AgentTaskRunner {
|
|
28
27
|
factory;
|
|
@@ -62,7 +61,7 @@ export class AgentTaskRunner {
|
|
|
62
61
|
const input = run.input === undefined || run.input === null
|
|
63
62
|
? ""
|
|
64
63
|
: `\n\n<task_input>\n${JSON.stringify(run.input).replaceAll("</task_input>", "<\\/task_input>")}\n</task_input>`;
|
|
65
|
-
const prompt = run.context.resumePrompt ?? `${preamble(run)}${action.prompt}${input}`;
|
|
64
|
+
const prompt = run.context.resumePrompt ?? `${preamble(run, this.store.supervised(run))}${action.prompt}${input}`;
|
|
66
65
|
run.context.sessionId = session.id;
|
|
67
66
|
run.context.model = session.model;
|
|
68
67
|
// The level the session settled on: an unspecified effort inherits the caller's.
|
package/dist/tasks/callbacks.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// belongs to outbox.ts; this file owns the run vocabulary and the batching.
|
|
3
3
|
import { Outbox } from "./outbox.js";
|
|
4
4
|
/** The run id and the session that did the work: a relayer's next move is a
|
|
5
|
-
* deep link to it, and without this that costs a second
|
|
5
|
+
* deep link to it, and without this that costs a second call. */
|
|
6
6
|
export const runRef = (run) => `Run: ${run.id}${run.targetSessionId ? ` / Session: ${run.targetSessionId}` : ""}`;
|
|
7
7
|
/** What the card in the recipient's transcript says the input came from. */
|
|
8
8
|
export const runSource = (run) => ({
|
|
@@ -21,7 +21,7 @@ export function runResultText(run) {
|
|
|
21
21
|
if (run.result?.type === "watch")
|
|
22
22
|
result = "Watch condition did not match";
|
|
23
23
|
if (result.length > 8000)
|
|
24
|
-
result = `${result.slice(0, 8000)}\n[truncated — task
|
|
24
|
+
result = `${result.slice(0, 8000)}\n[truncated — pier task recover --run ${run.id} --reason … returns the full text]`;
|
|
25
25
|
return result;
|
|
26
26
|
}
|
|
27
27
|
export class TaskCallbacks {
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// `pier task <command> …`: argv → the params object `/task` takes. Argv shape
|
|
2
|
+
// is the only thing checked here; the server validates the operation, and its
|
|
3
|
+
// answer comes back verbatim.
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
const OPTIONS = {
|
|
7
|
+
prompt: { type: "string" }, run: { type: "string" }, after: { type: "boolean" },
|
|
8
|
+
"task-id": { type: "string" }, session: { type: "string" },
|
|
9
|
+
model: { type: "string" }, thinking: { type: "string" }, cwd: { type: "string" }, name: { type: "string" },
|
|
10
|
+
timeout: { type: "string" }, callback: { type: "string" }, "callback-session": { type: "string" }, join: { type: "string" },
|
|
11
|
+
bash: { type: "string" }, cron: { type: "string" }, tz: { type: "string" },
|
|
12
|
+
watch: { type: "string" }, every: { type: "string" }, repeat: { type: "boolean" },
|
|
13
|
+
group: { type: "string" }, reason: { type: "string" },
|
|
14
|
+
help: { type: "boolean", short: "h" },
|
|
15
|
+
};
|
|
16
|
+
/** The usage line is the contract (docs/design/09-tasks-cli.md); the flags a
|
|
17
|
+
* command accepts are read off it, so the two cannot drift. */
|
|
18
|
+
const COMMANDS = {
|
|
19
|
+
run: {
|
|
20
|
+
usage: "run [--prompt <text|->] [--run <id> [--after]] [--task-id <id>] [--session <id>]\n" +
|
|
21
|
+
" [--model <name|?>] [--thinking <level>] [--cwd <dir>] [--name <text>] [--timeout <seconds>]\n" +
|
|
22
|
+
" [--callback origin|none|steer] [--callback-session <id>] [--join all|first] [--member <flags…>]…",
|
|
23
|
+
help: "a prompt on a run: a new one (--prompt | --task-id | --session … --prompt), a batch (--member), or an existing one (--run)",
|
|
24
|
+
},
|
|
25
|
+
save: {
|
|
26
|
+
usage: "save [--task-id <id>] --name <text> (--prompt <text|-> | --bash <script>)\n" +
|
|
27
|
+
" [--cron \"<expr>\" --tz <zone> | --watch <script> --every <seconds> [--repeat]]\n" +
|
|
28
|
+
" [--cwd <dir>] [--timeout <seconds>] [--model <name>] [--thinking <level>] [--callback-session <id>]",
|
|
29
|
+
help: "file a definition the operator sees, or update one by --task-id; no trigger means manual",
|
|
30
|
+
},
|
|
31
|
+
list: { usage: "list", help: "stored definitions, as JSON" },
|
|
32
|
+
cancel: { usage: "cancel (--run <id> | --group <id>)", help: "a run or a group, descendants included" },
|
|
33
|
+
recover: { usage: "recover (--run <id> | --group <id>) --reason <text>", help: "a finished result after its callback settled; never a progress check" },
|
|
34
|
+
};
|
|
35
|
+
const USAGE = [
|
|
36
|
+
"usage: pier task <command> … — subagents and scheduled tasks (skills/pier-tasks)",
|
|
37
|
+
...Object.values(COMMANDS).map(({ usage, help }) => ` ${usage}\n ${help}`),
|
|
38
|
+
"`--prompt -` reads stdin. The receipt is one line of JSON, exit 0; a refusal is one `task:` line, exit 1.",
|
|
39
|
+
].join("\n");
|
|
40
|
+
/** Flags that belong to the batch, never to one member. */
|
|
41
|
+
const GROUP_FLAGS = ["join", "callback", "callback-session"];
|
|
42
|
+
const processIo = {
|
|
43
|
+
stdout: (line) => process.stdout.write(`${line}\n`),
|
|
44
|
+
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
45
|
+
stdin: () => readFileSync(0, "utf8"),
|
|
46
|
+
};
|
|
47
|
+
class ArgvError extends Error {
|
|
48
|
+
}
|
|
49
|
+
const refuse = (message) => {
|
|
50
|
+
throw new ArgvError(message);
|
|
51
|
+
};
|
|
52
|
+
const compact = (value) => Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
|
|
53
|
+
const flagsOf = (values) => Object.keys(values).filter((flag) => flag !== "help");
|
|
54
|
+
const seconds = (flag, raw) => {
|
|
55
|
+
if (raw === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
const n = Number(raw);
|
|
58
|
+
return Number.isInteger(n) ? n : refuse(`--${flag} must be a whole number of seconds`);
|
|
59
|
+
};
|
|
60
|
+
/** `launch` as the server takes it: `model` is a menu name it resolves. */
|
|
61
|
+
const launchOf = (v) => v.model === undefined && v.thinking === undefined ? undefined : compact({ model: v.model, thinking: v.thinking });
|
|
62
|
+
/** Argv is split at each bare `--member`; a value equal to it is unreachable,
|
|
63
|
+
* since parseArgs (strict) refuses option-like values anyway. */
|
|
64
|
+
function segments(argv) {
|
|
65
|
+
const out = [[]];
|
|
66
|
+
for (const arg of argv) {
|
|
67
|
+
if (arg === "--member")
|
|
68
|
+
out.push([]);
|
|
69
|
+
else
|
|
70
|
+
out.at(-1).push(arg);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
export async function runTaskCli(argv, post, io = processIo) {
|
|
75
|
+
const [name, ...rest] = argv;
|
|
76
|
+
if (!name || name === "--help" || name === "-h") {
|
|
77
|
+
io.stdout(USAGE);
|
|
78
|
+
return name ? 0 : 2;
|
|
79
|
+
}
|
|
80
|
+
const cmd = COMMANDS[name];
|
|
81
|
+
const usage = (message) => {
|
|
82
|
+
io.stderr(`task: ${message}\n${cmd ? `pier task ${cmd.usage}` : USAGE}`);
|
|
83
|
+
return 2;
|
|
84
|
+
};
|
|
85
|
+
if (!cmd)
|
|
86
|
+
return usage(`unknown command "${name}"`);
|
|
87
|
+
const allowed = new Set((cmd.usage.match(/--[a-z-]+/g) ?? []).map((flag) => flag.slice(2)));
|
|
88
|
+
let params;
|
|
89
|
+
try {
|
|
90
|
+
const parsed = (name === "run" ? segments(rest) : [rest]).map((args) => {
|
|
91
|
+
const values = parseArgs({ args, options: OPTIONS, allowPositionals: false, strict: true }).values;
|
|
92
|
+
const stray = flagsOf(values).find((flag) => !allowed.has(flag));
|
|
93
|
+
return stray ? refuse(`--${stray} is not an option of ${name}`) : values;
|
|
94
|
+
});
|
|
95
|
+
if (parsed.some((values) => values.help)) {
|
|
96
|
+
io.stdout(`pier task ${cmd.usage}\n ${cmd.help}`);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
params = build(name, parsed, io);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return usage(err instanceof Error ? err.message : String(err));
|
|
103
|
+
}
|
|
104
|
+
const { status, body } = await post(params);
|
|
105
|
+
if (status === 200) {
|
|
106
|
+
// Compact: the reader is a model, and the ids are what it keeps.
|
|
107
|
+
io.stdout(JSON.stringify(body.result));
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
io.stderr(`task: ${body.error ?? `socket answered ${String(status)}`}`);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
/** Argv → the exact params object, or an ArgvError before the socket is touched. */
|
|
114
|
+
function build(name, parsed, io) {
|
|
115
|
+
let stdinReads = 0;
|
|
116
|
+
const text = (raw) => {
|
|
117
|
+
if (raw !== "-")
|
|
118
|
+
return raw === undefined ? undefined : String(raw);
|
|
119
|
+
if (stdinReads++)
|
|
120
|
+
refuse("only one --prompt may read stdin (-)");
|
|
121
|
+
// An agent's shell has no TTY: a `-` with nothing piped is an empty prompt, named here rather than by the server.
|
|
122
|
+
return io.stdin() || refuse("--prompt - read nothing from stdin");
|
|
123
|
+
};
|
|
124
|
+
if (parsed.some((v) => v.model === "?"))
|
|
125
|
+
return { operation: "run", launch: { model: "?" } };
|
|
126
|
+
const [values, ...members] = parsed;
|
|
127
|
+
if (name === "list")
|
|
128
|
+
return { operation: "list" };
|
|
129
|
+
if (name === "cancel" || name === "recover") {
|
|
130
|
+
if ((values.run === undefined) === (values.group === undefined))
|
|
131
|
+
refuse(`${name} takes exactly one of --run or --group`);
|
|
132
|
+
if (name === "recover" && values.reason === undefined)
|
|
133
|
+
refuse("recover needs --reason");
|
|
134
|
+
return compact({ operation: name, run_id: values.run, group_id: values.group, reason: values.reason });
|
|
135
|
+
}
|
|
136
|
+
if (name === "save")
|
|
137
|
+
return saveParams(values, text);
|
|
138
|
+
/** One new run, in the three shapes `tasks[]` accepts. A saved
|
|
139
|
+
* definition runs as is: batch defaults pass it by, its own flags are refused. */
|
|
140
|
+
const entry = (v, own = v) => {
|
|
141
|
+
const launch = launchOf(v);
|
|
142
|
+
const timeoutSeconds = seconds("timeout", v.timeout);
|
|
143
|
+
if (v["task-id"] !== undefined) {
|
|
144
|
+
const extra = flagsOf(own).find((flag) => ["prompt", "session", "cwd", "model", "thinking", "name", "timeout"].includes(flag));
|
|
145
|
+
if (extra)
|
|
146
|
+
refuse(`--${extra} does not apply to a saved definition (--task-id)`);
|
|
147
|
+
return { task_id: v["task-id"] };
|
|
148
|
+
}
|
|
149
|
+
const prompt = text(v.prompt) ?? refuse("a new run needs --prompt or --task-id");
|
|
150
|
+
if (v.session === undefined)
|
|
151
|
+
return compact({ prompt, cwd: v.cwd, launch, name: v.name, timeoutSeconds });
|
|
152
|
+
if (v.cwd !== undefined)
|
|
153
|
+
refuse("--cwd applies to a fresh session, not --session");
|
|
154
|
+
return compact({ name: v.name, timeoutSeconds, action: compact({ type: "agent", session: { mode: "reuse", sessionId: v.session }, prompt, launch }) });
|
|
155
|
+
};
|
|
156
|
+
const delivery = compact({ callback: values.callback, callback_session_id: values["callback-session"] });
|
|
157
|
+
if (values.run !== undefined) {
|
|
158
|
+
if (members.length)
|
|
159
|
+
refuse("--run addresses one existing run; --member starts new ones");
|
|
160
|
+
const extra = flagsOf(values).find((flag) => !["run", "after", "prompt", "callback", "callback-session"].includes(flag));
|
|
161
|
+
if (extra)
|
|
162
|
+
refuse(`--${extra} does not apply to an existing run (--run)`);
|
|
163
|
+
const message = text(values.prompt) ?? refuse("--run needs --prompt");
|
|
164
|
+
return compact({ operation: "message", run_id: values.run, message, after: values.after || undefined, ...delivery });
|
|
165
|
+
}
|
|
166
|
+
if (values.after)
|
|
167
|
+
refuse("--after applies to --run only");
|
|
168
|
+
if (members.length) {
|
|
169
|
+
if (members.length < 2)
|
|
170
|
+
refuse("a batch needs at least two --member");
|
|
171
|
+
if (values["task-id"] !== undefined)
|
|
172
|
+
refuse("--task-id names one member's definition; put it after a --member");
|
|
173
|
+
const grouped = members.find((m) => flagsOf(m).some((flag) => GROUP_FLAGS.includes(flag)));
|
|
174
|
+
if (grouped)
|
|
175
|
+
refuse(`--${flagsOf(grouped).find((flag) => GROUP_FLAGS.includes(flag))} belongs before the first --member`);
|
|
176
|
+
const defaults = Object.fromEntries(Object.entries(values).filter(([flag]) => !GROUP_FLAGS.includes(flag)));
|
|
177
|
+
return compact({ operation: "run", tasks: members.map((m) => entry({ ...defaults, ...m }, m)), join: values.join, ...delivery });
|
|
178
|
+
}
|
|
179
|
+
if (values.join !== undefined)
|
|
180
|
+
refuse("--join applies to a batch (--member)");
|
|
181
|
+
const single = entry(values);
|
|
182
|
+
if (single.task_id !== undefined || single.action === undefined)
|
|
183
|
+
return { operation: "run", ...single, ...delivery };
|
|
184
|
+
return { operation: "run", task: single, ...delivery };
|
|
185
|
+
}
|
|
186
|
+
function saveParams(v, text) {
|
|
187
|
+
if ((v.prompt === undefined) === (v.bash === undefined))
|
|
188
|
+
refuse("save takes exactly one of --prompt or --bash");
|
|
189
|
+
const cron = v.cron !== undefined || v.tz !== undefined;
|
|
190
|
+
const watch = v.watch !== undefined || v.every !== undefined || v.repeat !== undefined;
|
|
191
|
+
if (cron && watch)
|
|
192
|
+
refuse("a task has one trigger: --cron/--tz or --watch/--every/--repeat");
|
|
193
|
+
if (cron && (v.cron === undefined || v.tz === undefined))
|
|
194
|
+
refuse("--cron and --tz go together");
|
|
195
|
+
if (watch && (v.watch === undefined || v.every === undefined))
|
|
196
|
+
refuse("--watch and --every go together");
|
|
197
|
+
const launch = launchOf(v);
|
|
198
|
+
if (launch && v.bash !== undefined)
|
|
199
|
+
refuse("--model/--thinking apply to a prompt, not --bash");
|
|
200
|
+
const trigger = cron
|
|
201
|
+
? { type: "cron", expression: v.cron, timezone: v.tz }
|
|
202
|
+
: watch
|
|
203
|
+
? compact({ type: "watch", script: v.watch, cwd: v.cwd, intervalSeconds: seconds("every", v.every), mode: v.repeat ? "repeat" : "once" })
|
|
204
|
+
: { type: "manual" };
|
|
205
|
+
const task = compact({
|
|
206
|
+
name: v.name,
|
|
207
|
+
timeoutSeconds: seconds("timeout", v.timeout),
|
|
208
|
+
trigger,
|
|
209
|
+
callback: v["callback-session"] === undefined ? undefined : { type: "session", sessionId: v["callback-session"] },
|
|
210
|
+
...(v.bash === undefined
|
|
211
|
+
? { prompt: text(v.prompt), cwd: v.cwd, launch }
|
|
212
|
+
: { action: compact({ type: "bash", script: v.bash, cwd: v.cwd }) }),
|
|
213
|
+
});
|
|
214
|
+
return compact({ operation: "save", task_id: v["task-id"], task });
|
|
215
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// What a task *is* before it ever runs: the id it is minted with, the draft
|
|
2
2
|
// validated into a definition, and when its trigger is next due. Every way a
|
|
3
|
-
// definition can be created — HTTP,
|
|
3
|
+
// definition can be created — HTTP, `pier task`, Pier's own owned task —
|
|
4
4
|
// arrives here, so a field is checked in one place or nowhere.
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { stat } from "node:fs/promises";
|
|
@@ -24,7 +24,7 @@ export const idSymbol = (byte) => ID_ALPHABET.charAt(byte & 31);
|
|
|
24
24
|
* not an error — `saveRun`'s ON CONFLICT DO UPDATE would quietly overwrite. */
|
|
25
25
|
export const newId = () => Array.from(randomBytes(16), idSymbol).join("");
|
|
26
26
|
/** A definition Pier's own code created is reconciled by that code and edited
|
|
27
|
-
* by nobody: `creator` is `"http"` (Console) or `session:<id>` (task
|
|
27
|
+
* by nobody: `creator` is `"http"` (Console) or `session:<id>` (`pier task`);
|
|
28
28
|
* anything else is an instance-layer owner, which names itself in `by`.
|
|
29
29
|
* Otherwise a public surface could repoint the tools task while its switch
|
|
30
30
|
* went on claiming Pier keeps the tools current. */
|
|
@@ -258,7 +258,7 @@ export class TaskDefinitions {
|
|
|
258
258
|
if (trigger.type === "watch")
|
|
259
259
|
await this.assertDirectory(trigger.cwd);
|
|
260
260
|
const actionRaw = record(value.action);
|
|
261
|
-
// Self-documenting:
|
|
261
|
+
// Self-documenting: a model recovers from this in one retry.
|
|
262
262
|
if (!actionRaw) {
|
|
263
263
|
throw new Error('action required, e.g. {"type":"agent","session":{"mode":"fresh","cwd":"/abs/path"},"prompt":"..."}');
|
|
264
264
|
}
|
package/dist/tasks/execution.js
CHANGED
|
@@ -92,9 +92,7 @@ export class TaskExecution {
|
|
|
92
92
|
log.debug(`${settled} (watch did not match)`);
|
|
93
93
|
else
|
|
94
94
|
log.info(settled);
|
|
95
|
-
|
|
96
|
-
// completion callback: the pending question is the notification.
|
|
97
|
-
if (run.callbackSessionId && !this.host.openDecisionId(run.id))
|
|
95
|
+
if (run.callbackSessionId)
|
|
98
96
|
run.callbackState = "pending";
|
|
99
97
|
this.controllers.delete(run.id);
|
|
100
98
|
try {
|
package/dist/tasks/groups.js
CHANGED
|
@@ -35,12 +35,12 @@ export class TaskGroups {
|
|
|
35
35
|
}
|
|
36
36
|
/** Enqueues every member or none: a partially started group is worse than
|
|
37
37
|
* a rejected one. */
|
|
38
|
-
runAll(definitions, join, callerSessionId,
|
|
38
|
+
runAll(definitions, join, callerSessionId, callbackSessionId, callbackMode) {
|
|
39
39
|
if (definitions.length < 2)
|
|
40
40
|
throw new Error("a task group needs at least 2 members");
|
|
41
41
|
const { group, runs } = this.store.transact(() => {
|
|
42
42
|
const group = this.create(join, callerSessionId, callbackSessionId, callbackMode);
|
|
43
|
-
const runs = definitions.map((definition) => this.host.prepareMember(definition.id, group.id, callerSessionId
|
|
43
|
+
const runs = definitions.map((definition) => this.host.prepareMember(definition.id, group.id, callerSessionId));
|
|
44
44
|
group.memberRunIds = runs.map((run) => run.id);
|
|
45
45
|
this.store.saveGroup(group);
|
|
46
46
|
return { group, runs };
|
|
@@ -150,9 +150,6 @@ export class TaskGroups {
|
|
|
150
150
|
`- "${run.context.definition.name}" \u2014 state: ${run.state}`,
|
|
151
151
|
` ${runRef(run)}`,
|
|
152
152
|
];
|
|
153
|
-
const decision = this.host.openDecisionId(run.id);
|
|
154
|
-
if (decision)
|
|
155
|
-
head.push(` Needs a decision: reply to message ${decision}`);
|
|
156
153
|
if (group.join === "first" && run.id !== group.winnerRunId) {
|
|
157
154
|
head.push(" Cancelled after the winning run; resume its session to recover partial work.");
|
|
158
155
|
return head.join("\n");
|