@timqi/pier 0.0.8 → 0.0.15
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 +26 -9
- package/dist/agent/events.js +53 -7
- package/dist/agent/listing.js +253 -0
- package/dist/agent/pi.js +279 -32
- package/dist/boards/boards.js +65 -16
- package/dist/boards/pier.css +1 -1
- package/dist/channels/attach.js +87 -0
- package/dist/channels/control.js +2 -2
- package/dist/channels/conversations.js +10 -0
- package/dist/channels/lark-api.js +38 -0
- package/dist/channels/lark-outbound.js +11 -2
- package/dist/channels/slack-api.js +36 -0
- package/dist/channels/slack-outbound.js +12 -2
- package/dist/channels/slack-tool.js +49 -9
- package/dist/channels/telegram-api.js +21 -2
- package/dist/channels/telegram.js +23 -8
- package/dist/cli.js +34 -0
- package/dist/core/identity.js +18 -0
- package/dist/core/inbound-file.js +3 -1
- package/dist/core/reply.js +2 -1
- package/dist/core/router.js +99 -11
- package/dist/db.js +87 -0
- package/dist/extensions/index.js +37 -0
- package/dist/extensions/web/anthropic.js +118 -0
- package/dist/extensions/web/artifacts.js +62 -0
- package/dist/extensions/web/content.js +130 -0
- package/dist/extensions/web/http.js +106 -0
- package/dist/extensions/web/index.js +9 -0
- package/dist/extensions/web/json.js +5 -0
- package/dist/extensions/web/language.js +47 -0
- package/dist/extensions/web/openai.js +112 -0
- package/dist/extensions/web/provider.js +121 -0
- package/dist/extensions/web/tools.js +304 -0
- package/dist/limits.js +14 -0
- package/dist/main.js +76 -10
- package/dist/paths.js +21 -1
- package/dist/settings.js +112 -13
- package/dist/tasks/agent.js +18 -4
- package/dist/tasks/callbacks.js +20 -1
- package/dist/tasks/definitions.js +56 -12
- package/dist/tasks/execution.js +5 -1
- package/dist/tasks/groups.js +4 -4
- package/dist/tasks/messages.js +4 -2
- package/dist/tasks/runs.js +2 -2
- package/dist/tasks/service.js +16 -6
- package/dist/tasks/tool.js +0 -12
- package/dist/tools-task.js +155 -0
- package/dist/tools.js +875 -0
- package/dist/web/auth.js +5 -3
- package/dist/web/explorer.js +15 -2
- package/dist/web/files.js +1 -1
- package/dist/web/instance.js +175 -22
- package/dist/web/providers.js +16 -0
- package/dist/web/public/assets/{ghostty-web-CcIc8O2I.js → ghostty-web-xcUrfRRs.js} +1 -1
- package/dist/web/public/assets/index-BWDlAMK2.js +93 -0
- package/dist/web/public/assets/index-DHqZnZr7.css +2 -0
- package/dist/web/public/index.html +5 -8
- package/dist/web/public/sw.js +4 -0
- package/dist/web/push.js +33 -9
- package/dist/web/repos.js +75 -0
- package/dist/web/server.js +170 -52
- package/dist/web/session-state.js +57 -44
- package/dist/web/terminal.js +34 -4
- package/dist/web/types.js +5 -0
- package/package.json +1 -1
- package/skills/pier-boards/SKILL.md +23 -13
- package/skills/pier-help/SKILL.md +1 -1
- package/skills/pier-slack/SKILL.md +21 -1
- package/skills/pier-tasks/SKILL.md +2 -2
- package/dist/web/public/assets/index-DmDJKOLH.js +0 -90
- package/dist/web/public/assets/index-gcSJ9QZ5.css +0 -2
package/dist/settings.js
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
import { isThinkingLevel } from "./core/types.js";
|
|
10
10
|
import { pierDb } from "./db.js";
|
|
11
11
|
import { logger } from "./log.js";
|
|
12
|
+
// The one place the custom-tool vocabulary lives (names, ubix sources, the
|
|
13
|
+
// names Pier already owns). Imported rather than copied: a second validator
|
|
14
|
+
// would be the third-copy bug one release later, and tools.ts is root-layer
|
|
15
|
+
// like this file, so nothing crosses a seam.
|
|
16
|
+
import { normalizeCustomTools } from "./tools.js";
|
|
12
17
|
const log = logger("settings");
|
|
13
18
|
/**
|
|
14
19
|
* `""` clears it, `null` rejects it. Rejecting rather than repairing: a
|
|
@@ -66,6 +71,56 @@ export function normalizeModelMenu(raw) {
|
|
|
66
71
|
}
|
|
67
72
|
return menu;
|
|
68
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* One line, typed into a shell. Same reject-don't-repair contract: a command
|
|
76
|
+
* silently truncated at a newline would run half of what the operator wrote,
|
|
77
|
+
* in every project shell, with no sign of what was dropped.
|
|
78
|
+
*/
|
|
79
|
+
export function normalizeTerminalInitCommand(raw) {
|
|
80
|
+
if (typeof raw !== "string")
|
|
81
|
+
return null;
|
|
82
|
+
const text = raw.trim();
|
|
83
|
+
if (!text)
|
|
84
|
+
return "";
|
|
85
|
+
if (text.length > 500)
|
|
86
|
+
return null;
|
|
87
|
+
// A tty reads these as keys, not text: an embedded newline is a second
|
|
88
|
+
// command nobody would see in the field it was typed in.
|
|
89
|
+
for (const ch of text)
|
|
90
|
+
if (ch < " " || ch === "\u007f")
|
|
91
|
+
return null;
|
|
92
|
+
return text;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Shape only — an unknown name is not an error here. This file must not know
|
|
96
|
+
* what Pier bundles (that catalog is code, and importing it would drag the Pi
|
|
97
|
+
* SDK into the instance layer); agent/ matches the names it recognizes and
|
|
98
|
+
* ignores the rest, which is also what keeps a downgrade from losing a
|
|
99
|
+
* setting it cannot currently explain.
|
|
100
|
+
*/
|
|
101
|
+
export function normalizeExtensions(raw) {
|
|
102
|
+
return normalizeNames(raw);
|
|
103
|
+
}
|
|
104
|
+
/** Same shape, same contract, same cap — the managed-tool set (src/tools.ts).
|
|
105
|
+
* Shape only again: tools.ts owns the catalog, and a name it does not know is
|
|
106
|
+
* ignored there rather than rejected here, so a downgrade cannot lose one. */
|
|
107
|
+
export function normalizeTools(raw) {
|
|
108
|
+
return normalizeNames(raw);
|
|
109
|
+
}
|
|
110
|
+
function normalizeNames(raw) {
|
|
111
|
+
if (!Array.isArray(raw) || raw.length > 32)
|
|
112
|
+
return null;
|
|
113
|
+
const names = new Set();
|
|
114
|
+
for (const item of raw) {
|
|
115
|
+
if (typeof item !== "string")
|
|
116
|
+
return null;
|
|
117
|
+
const name = item.trim();
|
|
118
|
+
if (!name || name.length > 64)
|
|
119
|
+
return null;
|
|
120
|
+
names.add(name);
|
|
121
|
+
}
|
|
122
|
+
return [...names];
|
|
123
|
+
}
|
|
69
124
|
export class SettingsStore {
|
|
70
125
|
#db;
|
|
71
126
|
constructor(db = pierDb()) {
|
|
@@ -74,29 +129,35 @@ export class SettingsStore {
|
|
|
74
129
|
get() {
|
|
75
130
|
return {
|
|
76
131
|
publicUrl: this.#value("publicUrl") ?? "",
|
|
77
|
-
modelMenu: this.#menu
|
|
132
|
+
modelMenu: this.#json("modelMenu", normalizeModelMenu, "a valid menu") ?? [],
|
|
78
133
|
autoUpdate: this.#value("autoUpdate") === "1",
|
|
134
|
+
terminalInitCommand: this.#value("terminalInitCommand") ?? "",
|
|
135
|
+
extensions: this.#json("extensions", normalizeExtensions, "a list of names") ?? [],
|
|
136
|
+
tools: this.#json("tools", normalizeTools, "a list of names") ?? [],
|
|
137
|
+
customTools: this.#json("customTools", normalizeCustomTools, "a list of {name, spec}") ?? [],
|
|
79
138
|
};
|
|
80
139
|
}
|
|
81
|
-
|
|
82
|
-
|
|
140
|
+
/**
|
|
141
|
+
* A JSON-valued row, validated on the way out. Only a hand-edited row can be
|
|
142
|
+
* malformed, and it is named rather than silently served as the empty value:
|
|
143
|
+
* a setting that stopped applying without saying so is the bug this logs.
|
|
144
|
+
*/
|
|
145
|
+
#json(key, normalize, expected) {
|
|
146
|
+
const raw = this.#value(key);
|
|
83
147
|
if (!raw)
|
|
84
|
-
return
|
|
148
|
+
return null;
|
|
85
149
|
let parsed;
|
|
86
150
|
try {
|
|
87
151
|
parsed = JSON.parse(raw);
|
|
88
152
|
}
|
|
89
153
|
catch {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return [];
|
|
93
|
-
}
|
|
94
|
-
const menu = normalizeModelMenu(parsed);
|
|
95
|
-
if (!menu) {
|
|
96
|
-
log.warn("settings.modelMenu is not a valid menu — ignoring it");
|
|
97
|
-
return [];
|
|
154
|
+
log.warn(`settings.${key} is not JSON — ignoring it`);
|
|
155
|
+
return null;
|
|
98
156
|
}
|
|
99
|
-
|
|
157
|
+
const value = normalize(parsed);
|
|
158
|
+
if (value === null)
|
|
159
|
+
log.warn(`settings.${key} is not ${expected} — ignoring it`);
|
|
160
|
+
return value;
|
|
100
161
|
}
|
|
101
162
|
/** Store an already-normalized value — validation belongs at the boundary
|
|
102
163
|
* that received it, so this never has to guess what the caller meant. */
|
|
@@ -109,10 +170,48 @@ export class SettingsStore {
|
|
|
109
170
|
this.#set("modelMenu", JSON.stringify(menu));
|
|
110
171
|
return this.get();
|
|
111
172
|
}
|
|
173
|
+
/** Same contract again: hand this `normalizeTerminalInitCommand`'s output. */
|
|
174
|
+
setTerminalInitCommand(command) {
|
|
175
|
+
this.#set("terminalInitCommand", command);
|
|
176
|
+
return this.get();
|
|
177
|
+
}
|
|
112
178
|
setAutoUpdate(on) {
|
|
113
179
|
this.#set("autoUpdate", on ? "1" : "0");
|
|
114
180
|
return this.get();
|
|
115
181
|
}
|
|
182
|
+
/** Same contract again: hand this `normalizeExtensions`'s output. */
|
|
183
|
+
setExtensions(names) {
|
|
184
|
+
this.#set("extensions", JSON.stringify(names));
|
|
185
|
+
return this.get();
|
|
186
|
+
}
|
|
187
|
+
/** Same contract again: hand this `normalizeTools`'s output. */
|
|
188
|
+
setTools(names) {
|
|
189
|
+
this.#set("tools", JSON.stringify(names));
|
|
190
|
+
return this.get();
|
|
191
|
+
}
|
|
192
|
+
/** Same contract again: hand this `normalizeCustomTools`'s output. */
|
|
193
|
+
setCustomTools(tools) {
|
|
194
|
+
this.#set("customTools", JSON.stringify(tools));
|
|
195
|
+
return this.get();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Several setters as one write. A request that declares a tool *and* the
|
|
199
|
+
* switch that turns it on must not be able to store one without the other:
|
|
200
|
+
* half of that pair is a switch nobody can explain — on and undeclared, or
|
|
201
|
+
* declared and invisible.
|
|
202
|
+
*/
|
|
203
|
+
transact(work) {
|
|
204
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
205
|
+
try {
|
|
206
|
+
const result = work();
|
|
207
|
+
this.#db.exec("COMMIT");
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
this.#db.exec("ROLLBACK");
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
116
215
|
#set(key, value) {
|
|
117
216
|
this.#db.prepare(`
|
|
118
217
|
INSERT INTO settings(key, value) VALUES (?, ?)
|
package/dist/tasks/agent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { quietLabel, splitReply } from "../core/reply.js";
|
|
2
2
|
import { Router } from "../core/router.js";
|
|
3
|
+
import { runSource } from "./callbacks.js";
|
|
3
4
|
import { TaskMessenger } from "./messages.js";
|
|
4
5
|
import { TaskStore } from "./store.js";
|
|
5
6
|
const MAX_ACTIVE_AGENTS = 4;
|
|
@@ -44,6 +45,12 @@ export class AgentTaskRunner {
|
|
|
44
45
|
const session = await this.resolveSession(run, action);
|
|
45
46
|
return await this.withSession(session.id, async () => {
|
|
46
47
|
await this.waitUntilIdle(session, signal);
|
|
48
|
+
// Task requests come seconds apart — the 1h Anthropic cache-write premium
|
|
49
|
+
// never earns its 2× back, so task runs use the 5m TTL. Set here, not in
|
|
50
|
+
// resolveSession: this covers create, fork, reuse and resume alike, on
|
|
51
|
+
// every attempt — and only after the session is idle, so a reused
|
|
52
|
+
// interactive session's in-flight turn keeps its 1h writes.
|
|
53
|
+
session.setCacheRetention("short");
|
|
47
54
|
start();
|
|
48
55
|
// No input is no block: `<task_input>\nnull\n</task_input>` is four
|
|
49
56
|
// lines telling the agent nothing, on every run that has no input.
|
|
@@ -55,6 +62,10 @@ export class AgentTaskRunner {
|
|
|
55
62
|
const prompt = run.context.resumePrompt ?? `${preamble(run)}${action.prompt}${input}`;
|
|
56
63
|
run.context.sessionId = session.id;
|
|
57
64
|
run.context.model = session.model;
|
|
65
|
+
// The level the session settled on, not the one the task asked for:
|
|
66
|
+
// an unspecified effort inherits the caller's, and the card in the
|
|
67
|
+
// subagent's own transcript should say which one that was.
|
|
68
|
+
run.context.thinking = session.thinkingLevel;
|
|
58
69
|
run.context.renderedPrompt = prompt;
|
|
59
70
|
this.store.saveRun(run);
|
|
60
71
|
let text = "";
|
|
@@ -83,6 +94,7 @@ export class AgentTaskRunner {
|
|
|
83
94
|
taskId: run.taskId,
|
|
84
95
|
runId: run.id,
|
|
85
96
|
sourceSessionId: run.sourceSessionId,
|
|
97
|
+
source: runSource(run),
|
|
86
98
|
}, "prompt");
|
|
87
99
|
await Promise.resolve();
|
|
88
100
|
this.messages.deliverPendingControls(run);
|
|
@@ -102,6 +114,10 @@ export class AgentTaskRunner {
|
|
|
102
114
|
return { type: "agent", text: reply.text || quietLabel(reply.silence), sessionId: session.id };
|
|
103
115
|
}
|
|
104
116
|
finally {
|
|
117
|
+
// The run is what earns "short"; a reused interactive session goes
|
|
118
|
+
// back to chat afterwards. For task-created sessions this is moot —
|
|
119
|
+
// idle until the next run downgrades them again.
|
|
120
|
+
session.setCacheRetention("long");
|
|
105
121
|
signal.removeEventListener("abort", abort);
|
|
106
122
|
unsubscribe();
|
|
107
123
|
}
|
|
@@ -115,9 +131,8 @@ export class AgentTaskRunner {
|
|
|
115
131
|
if (run.targetSessionId) {
|
|
116
132
|
return this.router.ensure({ channelId: "task", conversationId: run.targetSessionId });
|
|
117
133
|
}
|
|
118
|
-
const listed = await this.factory.list();
|
|
119
134
|
const source = run.sourceSessionId
|
|
120
|
-
?
|
|
135
|
+
? await this.factory.find(run.sourceSessionId)
|
|
121
136
|
: undefined;
|
|
122
137
|
const policy = action.session;
|
|
123
138
|
let cwd;
|
|
@@ -130,7 +145,7 @@ export class AgentTaskRunner {
|
|
|
130
145
|
cwd = policy.cwd;
|
|
131
146
|
}
|
|
132
147
|
else if (policy.mode === "reuse") {
|
|
133
|
-
cwd =
|
|
148
|
+
cwd = (await this.factory.find(policy.sessionId))?.cwd ?? "";
|
|
134
149
|
}
|
|
135
150
|
else {
|
|
136
151
|
cwd = policy.cwd ?? source?.cwd ?? "";
|
|
@@ -145,7 +160,6 @@ export class AgentTaskRunner {
|
|
|
145
160
|
model: action.launch?.model ??
|
|
146
161
|
(run.sourceSessionId ? this.router.modelOf(run.sourceSessionId) : undefined),
|
|
147
162
|
thinking: action.launch?.thinking,
|
|
148
|
-
capabilities: action.launch?.capabilities,
|
|
149
163
|
};
|
|
150
164
|
const session = run.sessionMode === "fork"
|
|
151
165
|
? await this.factory.fork(run.sourceSessionId, opts)
|
package/dist/tasks/callbacks.js
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
import { Router } from "../core/router.js";
|
|
4
4
|
import { Outbox } from "./outbox.js";
|
|
5
5
|
import { TaskStore } from "./store.js";
|
|
6
|
+
/** How a run is named to the session that gets its result: the run id, and the
|
|
7
|
+
* session that did the work — a relayer's next move is a deep link to it
|
|
8
|
+
* (`#/session/<id>`), and without this line that costs a second tool call.
|
|
9
|
+
* Shared with the group callback, which has always carried both. */
|
|
10
|
+
export const runRef = (run) => `Run: ${run.id}${run.targetSessionId ? ` / Session: ${run.targetSessionId}` : ""}`;
|
|
11
|
+
/** What the card in the recipient's transcript says the input came from. Part
|
|
12
|
+
* of the run vocabulary for the same reason `runRef` is: the callback, the
|
|
13
|
+
* subagent messages and the delegation prompt all name a run, and three
|
|
14
|
+
* spellings of "which task, on what model" would be three cards. */
|
|
15
|
+
export const runSource = (run) => ({
|
|
16
|
+
taskName: run.context.definition.name,
|
|
17
|
+
...(run.context.model ? { model: run.context.model } : {}),
|
|
18
|
+
...(run.context.thinking ? { thinking: run.context.thinking } : {}),
|
|
19
|
+
});
|
|
6
20
|
export function runResultText(run) {
|
|
7
21
|
let result = run.error ?? "No result";
|
|
8
22
|
if (run.result?.type === "agent")
|
|
@@ -35,6 +49,11 @@ export class TaskCallbacks {
|
|
|
35
49
|
runId: runs[0].id,
|
|
36
50
|
sourceSessionId: runs[0].targetSessionId,
|
|
37
51
|
runIds: runs.map((run) => run.id),
|
|
52
|
+
// Only when the input is about one run: a batch is several tasks in
|
|
53
|
+
// one delivery, and the first one's name and model as the card's
|
|
54
|
+
// caption would attribute every other result to it. The text names
|
|
55
|
+
// each run; the card says nothing rather than something false.
|
|
56
|
+
...(runs.length === 1 ? { source: runSource(runs[0]) } : {}),
|
|
38
57
|
},
|
|
39
58
|
}),
|
|
40
59
|
describe: (run) => `the result of "${run.context.definition.name}"`,
|
|
@@ -69,7 +88,7 @@ export class TaskCallbacks {
|
|
|
69
88
|
text(runs) {
|
|
70
89
|
const sections = runs.map((run) => [
|
|
71
90
|
`Task "${run.context.definition.name}" finished with state: ${run.state}`,
|
|
72
|
-
|
|
91
|
+
runRef(run),
|
|
73
92
|
"",
|
|
74
93
|
runResultText(run),
|
|
75
94
|
].join("\n"));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { stat } from "node:fs/promises";
|
|
3
3
|
import { Cron } from "croner";
|
|
4
4
|
import { isThinkingLevel } from "../core/types.js";
|
|
@@ -7,6 +7,46 @@ import { Router } from "../core/router.js";
|
|
|
7
7
|
import { TaskStore } from "./store.js";
|
|
8
8
|
const DEFAULT_TIMEOUT = 900;
|
|
9
9
|
const MIN_WATCH_SECONDS = 5;
|
|
10
|
+
/** Crockford's base32, lowercased: i, l, o and u are gone — the three that
|
|
11
|
+
* misread as 1/0 and the one that completes most accidental words — and one
|
|
12
|
+
* case throughout, so a model re-types an id it read verbatim. */
|
|
13
|
+
const ID_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
14
|
+
/** The one byte → symbol step, exported only so a test can walk all 256 byte
|
|
15
|
+
* values: a repeated or omitted character in the alphabet above biases every
|
|
16
|
+
* id ever minted, and no sample of finished ids can show it. */
|
|
17
|
+
export const idSymbol = (byte) => ID_ALPHABET.charAt(byte & 31);
|
|
18
|
+
/** The id every task record is minted with — runs, definitions, groups and
|
|
19
|
+
* messages alike — because these ids ride through model context constantly:
|
|
20
|
+
* every run summary, callback, get, steer and reply echoes one, and a UUID
|
|
21
|
+
* spends ~12 tokens where this spends ~6. Nothing parses or orders by them —
|
|
22
|
+
* every comparison in the area is string equality and every listing orders by
|
|
23
|
+
* a timestamp column — so rows minted as UUIDs before this keep working
|
|
24
|
+
* untouched; there is nothing to migrate. It lives here rather than in
|
|
25
|
+
* types.ts because the browser type-checks that file and it stays node-free.
|
|
26
|
+
*
|
|
27
|
+
* Sixteen characters, 80 bits (`& 31` is unbiased because 256 is a multiple
|
|
28
|
+
* of 32). Sixty would have read the same and cost a token less, but a watch
|
|
29
|
+
* task on a 5-second interval mints ~6M runs a year, and a collision here is
|
|
30
|
+
* not an error: `saveRun`'s ON CONFLICT DO UPDATE would quietly overwrite the
|
|
31
|
+
* older run with the newer one. Four more characters buy ~16 million times
|
|
32
|
+
* the headroom for four bytes — cheaper than the alternative fix, which is a
|
|
33
|
+
* strict INSERT and a retry loop on every one of the four mint sites. */
|
|
34
|
+
export const newId = () => Array.from(randomBytes(16), idSymbol).join("");
|
|
35
|
+
/**
|
|
36
|
+
* Seam decision (tasks/): a definition Pier's own code created is reconciled by
|
|
37
|
+
* that code, and edited by nobody.
|
|
38
|
+
*
|
|
39
|
+
* `creator` is `"http"` for the Console and `session:<id>` for the task tool.
|
|
40
|
+
* Anything else is an instance-layer owner — today the tools update task
|
|
41
|
+
* (src/tools-task.ts), whose script a switch in Settings runs on demand and
|
|
42
|
+
* cron runs nightly. Both
|
|
43
|
+
* public surfaces could rename it, point it at another script, pause it or
|
|
44
|
+
* archive it, and the switch would go on claiming Pier keeps the tools current
|
|
45
|
+
* while the run did something else entirely. The owner names itself in `by`;
|
|
46
|
+
* neither the routes nor the tool has a `by` to pass, so this one function
|
|
47
|
+
* closes both.
|
|
48
|
+
*/
|
|
49
|
+
const ownerOf = (task) => task.creator === "http" || task.creator.startsWith("session:") ? null : task.creator;
|
|
10
50
|
export const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
11
51
|
? value
|
|
12
52
|
: null;
|
|
@@ -76,12 +116,6 @@ function parseLaunch(raw) {
|
|
|
76
116
|
}
|
|
77
117
|
launch.thinking = value.thinking;
|
|
78
118
|
}
|
|
79
|
-
if (value.capabilities !== undefined) {
|
|
80
|
-
if (value.capabilities !== "read" && value.capabilities !== "write") {
|
|
81
|
-
throw new Error("agent capabilities must be read or write");
|
|
82
|
-
}
|
|
83
|
-
launch.capabilities = value.capabilities;
|
|
84
|
-
}
|
|
85
119
|
return Object.keys(launch).length ? launch : undefined;
|
|
86
120
|
}
|
|
87
121
|
export class TaskDefinitions {
|
|
@@ -110,7 +144,7 @@ export class TaskDefinitions {
|
|
|
110
144
|
const draft = await this.parseDraft(value && value.trigger === undefined ? { ...value, trigger: { type: "manual" } } : raw);
|
|
111
145
|
const now = Date.now();
|
|
112
146
|
const task = {
|
|
113
|
-
id:
|
|
147
|
+
id: newId(),
|
|
114
148
|
kind,
|
|
115
149
|
name: draft.name,
|
|
116
150
|
description: draft.description ?? "",
|
|
@@ -133,8 +167,9 @@ export class TaskDefinitions {
|
|
|
133
167
|
this.changed();
|
|
134
168
|
return task;
|
|
135
169
|
}
|
|
136
|
-
async update(id, raw) {
|
|
170
|
+
async update(id, raw, by) {
|
|
137
171
|
const old = this.get(id);
|
|
172
|
+
this.assertOwner(old, by, "edited");
|
|
138
173
|
if (old.archived)
|
|
139
174
|
throw new Error("archived tasks cannot be edited");
|
|
140
175
|
const draft = await this.parseDraft(raw);
|
|
@@ -158,8 +193,9 @@ export class TaskDefinitions {
|
|
|
158
193
|
this.changed();
|
|
159
194
|
return task;
|
|
160
195
|
}
|
|
161
|
-
setEnabled(id, enabled) {
|
|
196
|
+
setEnabled(id, enabled, by) {
|
|
162
197
|
const task = this.get(id);
|
|
198
|
+
this.assertOwner(task, by, enabled ? "resumed" : "paused");
|
|
163
199
|
if (task.archived && enabled)
|
|
164
200
|
throw new Error("archived tasks cannot be resumed");
|
|
165
201
|
task.enabled = enabled;
|
|
@@ -169,8 +205,9 @@ export class TaskDefinitions {
|
|
|
169
205
|
this.changed();
|
|
170
206
|
return task;
|
|
171
207
|
}
|
|
172
|
-
archive(id) {
|
|
208
|
+
archive(id, by) {
|
|
173
209
|
const task = this.get(id);
|
|
210
|
+
this.assertOwner(task, by, "archived");
|
|
174
211
|
task.archived = true;
|
|
175
212
|
task.enabled = false;
|
|
176
213
|
task.nextRunAt = null;
|
|
@@ -200,7 +237,14 @@ export class TaskDefinitions {
|
|
|
200
237
|
}
|
|
201
238
|
async sessionExists(sessionId) {
|
|
202
239
|
return this.router.stateOf(sessionId) !== undefined ||
|
|
203
|
-
(await this.factory.
|
|
240
|
+
(await this.factory.find(sessionId)) !== undefined;
|
|
241
|
+
}
|
|
242
|
+
/** The guard `ownerOf` exists for, on the three ways a definition changes. */
|
|
243
|
+
assertOwner(task, by, what) {
|
|
244
|
+
const owner = ownerOf(task);
|
|
245
|
+
if (owner && by !== owner) {
|
|
246
|
+
throw new Error(`"${task.name}" is Pier's own ${owner} task: it is reconciled by Pier, not ${what}`);
|
|
247
|
+
}
|
|
204
248
|
}
|
|
205
249
|
async parseDraft(raw) {
|
|
206
250
|
const value = record(raw);
|
package/dist/tasks/execution.js
CHANGED
|
@@ -56,7 +56,11 @@ export class TaskExecution {
|
|
|
56
56
|
run.result = await this.executeAction(run, controller.signal);
|
|
57
57
|
run.state = "succeeded";
|
|
58
58
|
if (definition.trigger.type === "watch" && !run.resumedFromRunId && definition.trigger.mode === "once" && run.matched) {
|
|
59
|
-
|
|
59
|
+
// As the definition's own creator: a one-shot watch retiring itself is
|
|
60
|
+
// the task layer keeping its own promise, not a surface editing
|
|
61
|
+
// somebody's task, and the owner guard (definitions.ts) would
|
|
62
|
+
// otherwise fail the run that had just succeeded.
|
|
63
|
+
this.definitions.setEnabled(definition.id, false, definition.creator);
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
catch (error) {
|
package/dist/tasks/groups.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { Router } from "../core/router.js";
|
|
3
2
|
import { logger } from "../log.js";
|
|
4
|
-
import { runResultText } from "./callbacks.js";
|
|
3
|
+
import { runRef, runResultText } from "./callbacks.js";
|
|
4
|
+
import { newId } from "./definitions.js";
|
|
5
5
|
import { Outbox } from "./outbox.js";
|
|
6
6
|
import { TaskStore } from "./store.js";
|
|
7
7
|
import { isTerminal } from "./types.js";
|
|
@@ -61,7 +61,7 @@ export class TaskGroups {
|
|
|
61
61
|
}
|
|
62
62
|
create(join, invokedBySessionId, callbackSessionId) {
|
|
63
63
|
const group = {
|
|
64
|
-
id:
|
|
64
|
+
id: newId(),
|
|
65
65
|
join,
|
|
66
66
|
invokedBySessionId,
|
|
67
67
|
callbackSessionId,
|
|
@@ -137,7 +137,7 @@ export class TaskGroups {
|
|
|
137
137
|
const sections = members.map((run) => {
|
|
138
138
|
const head = [
|
|
139
139
|
`- "${run.context.definition.name}" \u2014 state: ${run.state}`,
|
|
140
|
-
`
|
|
140
|
+
` ${runRef(run)}`,
|
|
141
141
|
];
|
|
142
142
|
const decision = this.host.openDecisionId(run.id);
|
|
143
143
|
if (decision)
|
package/dist/tasks/messages.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { EventHub } from "../core/hub.js";
|
|
3
2
|
import { Router } from "../core/router.js";
|
|
4
3
|
import { logger } from "../log.js";
|
|
4
|
+
import { runSource } from "./callbacks.js";
|
|
5
|
+
import { newId } from "./definitions.js";
|
|
5
6
|
import { TaskStore } from "./store.js";
|
|
6
7
|
import { isTerminal, MAX_DELIVERY_ATTEMPTS, retryDelay, undeliverable } from "./types.js";
|
|
7
8
|
const log = logger("tasks");
|
|
@@ -152,7 +153,7 @@ export class TaskMessenger {
|
|
|
152
153
|
}
|
|
153
154
|
create(run, kind, fromSessionId, toSessionId, content, replyTo) {
|
|
154
155
|
const message = {
|
|
155
|
-
id:
|
|
156
|
+
id: newId(),
|
|
156
157
|
runId: run.id,
|
|
157
158
|
kind,
|
|
158
159
|
fromSessionId,
|
|
@@ -319,6 +320,7 @@ export class TaskMessenger {
|
|
|
319
320
|
sourceSessionId: message.fromSessionId,
|
|
320
321
|
messageId: message.id,
|
|
321
322
|
messageKind: message.kind,
|
|
323
|
+
source: runSource(run),
|
|
322
324
|
};
|
|
323
325
|
}
|
|
324
326
|
require(id) {
|
package/dist/tasks/runs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { logger } from "../log.js";
|
|
3
2
|
import { TaskCallbacks } from "./callbacks.js";
|
|
3
|
+
import { newId } from "./definitions.js";
|
|
4
4
|
import { TaskStore } from "./store.js";
|
|
5
5
|
const log = logger("tasks");
|
|
6
6
|
const MAX_DEPTH = 2;
|
|
@@ -19,7 +19,7 @@ export class TaskRunQueue {
|
|
|
19
19
|
this.changed = changed;
|
|
20
20
|
}
|
|
21
21
|
enqueue(definition, input, source, parentRunId, provenance) {
|
|
22
|
-
const id =
|
|
22
|
+
const id = newId();
|
|
23
23
|
const parent = parentRunId ? this.getRun(parentRunId) : null;
|
|
24
24
|
const depth = provenance.depth ?? (parent ? parent.depth + 1 : 0);
|
|
25
25
|
const rootRunId = provenance.rootRunId ?? parent?.rootRunId ?? id;
|
package/dist/tasks/service.js
CHANGED
|
@@ -130,6 +130,14 @@ export class TaskService {
|
|
|
130
130
|
activeRunCount() {
|
|
131
131
|
return this.store.countActiveRuns();
|
|
132
132
|
}
|
|
133
|
+
/** The run this task has in flight, if any. The store already answers this
|
|
134
|
+
* for the overlap guard (runs.ts); a caller that has just been refused as
|
|
135
|
+
* an overlap needs the same answer to know what to wait for, and scanning
|
|
136
|
+
* run history for it finds nothing once the skipped rows outnumber the
|
|
137
|
+
* window. */
|
|
138
|
+
activeRun(taskId) {
|
|
139
|
+
return this.store.findActiveRun(taskId);
|
|
140
|
+
}
|
|
133
141
|
list() {
|
|
134
142
|
return this.definitions.list();
|
|
135
143
|
}
|
|
@@ -139,14 +147,16 @@ export class TaskService {
|
|
|
139
147
|
create(raw, creator = "http") {
|
|
140
148
|
return this.definitions.create(raw, creator);
|
|
141
149
|
}
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
/** `by` is how the code that owns a definition says so; the HTTP routes and
|
|
151
|
+
* the task tool have none, which is what closes both (definitions.ts). */
|
|
152
|
+
update(id, raw, by) {
|
|
153
|
+
return this.definitions.update(id, raw, by);
|
|
144
154
|
}
|
|
145
|
-
setEnabled(id, enabled) {
|
|
146
|
-
return this.definitions.setEnabled(id, enabled);
|
|
155
|
+
setEnabled(id, enabled, by) {
|
|
156
|
+
return this.definitions.setEnabled(id, enabled, by);
|
|
147
157
|
}
|
|
148
|
-
archive(id) {
|
|
149
|
-
return this.definitions.archive(id);
|
|
158
|
+
archive(id, by) {
|
|
159
|
+
return this.definitions.archive(id, by);
|
|
150
160
|
}
|
|
151
161
|
sessionExists(sessionId) {
|
|
152
162
|
return this.definitions.sessionExists(sessionId);
|
package/dist/tasks/tool.js
CHANGED
|
@@ -82,9 +82,6 @@ const DraftSchema = Type.Object({
|
|
|
82
82
|
Type.Object({ mode: Type.Literal("reuse"), sessionId: Type.String() }),
|
|
83
83
|
]),
|
|
84
84
|
prompt: Type.String(),
|
|
85
|
-
// No `capabilities` here on purpose: a child gets the same tools as any
|
|
86
|
-
// Pier session, so the model never spends a decision on it. Read-only
|
|
87
|
-
// children stay configurable through the Console and HTTP.
|
|
88
85
|
launch: Type.Optional(Type.Object({
|
|
89
86
|
model: Type.Optional(Type.Object({ provider: Type.String(), id: Type.String() })),
|
|
90
87
|
thinking: Type.Optional(Type.String()),
|
|
@@ -152,9 +149,6 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
152
149
|
return definitions.update(requiredString(input.task_id, "task_id"), input.task);
|
|
153
150
|
}
|
|
154
151
|
if (input.operation === "run") {
|
|
155
|
-
if (active && active.context.definition.action.type === "agent" && active.context.definition.action.launch?.capabilities === "read") {
|
|
156
|
-
throw new Error("read-only subagents cannot delegate nested work");
|
|
157
|
-
}
|
|
158
152
|
if (Array.isArray(input.tasks)) {
|
|
159
153
|
// Core-joined fan-out: members run detached, one aggregated callback.
|
|
160
154
|
if (input.task !== undefined || input.task_id !== undefined)
|
|
@@ -253,12 +247,6 @@ async function resolveDraft(definitions, draft, active, callerSessionId) {
|
|
|
253
247
|
if (draft.trigger !== undefined && record(draft.trigger)?.type !== "manual") {
|
|
254
248
|
throw new Error("inline subagent tasks must use a manual trigger");
|
|
255
249
|
}
|
|
256
|
-
// The draft parser accepts `capabilities` for Console/HTTP definitions; from
|
|
257
|
-
// the tool it is rejected rather than silently honoured, so a model working
|
|
258
|
-
// from stale memory learns the field is gone.
|
|
259
|
-
if (record(record(draft.action)?.launch)?.capabilities !== undefined) {
|
|
260
|
-
throw new Error("launch.capabilities is configured in Console or HTTP, not by the task tool");
|
|
261
|
-
}
|
|
262
250
|
if (active) {
|
|
263
251
|
const action = record(draft.action);
|
|
264
252
|
if (action?.type !== "agent")
|