@timqi/pier 0.1.2 → 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 +63 -59
- package/dist/agent/events.js +1 -2
- package/dist/agent/listing.js +38 -15
- package/dist/agent/packages.js +347 -0
- package/dist/agent/pi.js +33 -59
- 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/core/types.js +7 -0
- package/dist/db.js +28 -0
- package/dist/extensions/index.js +1 -20
- package/dist/main.js +34 -42
- package/dist/paths.js +3 -0
- package/dist/secrets.js +2 -1
- package/dist/settings.js +5 -0
- 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 +63 -85
- package/dist/vault.js +107 -0
- package/dist/web/auth.js +2 -1
- package/dist/web/config.js +25 -11
- package/dist/web/explorer.js +5 -3
- package/dist/web/instance.js +6 -18
- package/dist/web/packages.js +84 -0
- package/dist/web/public/assets/{activity-DOr8dWeX.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-DleegoiC.js +1 -0
- 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-vJvV1Sx0.js +4 -0
- 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-Bs-gol9o.css +2 -0
- package/dist/web/public/assets/index-Bs-gol9o.css.br +0 -0
- package/dist/web/public/assets/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-C5FveZ75.js +1 -0
- 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-CJJ2j5Ks.js +3 -0
- 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-R2Pv0Abg.js +4 -0
- 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/server.js +3 -1
- package/dist/web/vault.js +68 -0
- package/docs/deploy.md +42 -8
- 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-DOr8dWeX.js.br +0 -0
- package/dist/web/public/assets/activity-DOr8dWeX.js.gz +0 -0
- package/dist/web/public/assets/boards-CneyR23E.js +0 -1
- package/dist/web/public/assets/boards-CneyR23E.js.br +0 -0
- package/dist/web/public/assets/boards-CneyR23E.js.gz +0 -0
- package/dist/web/public/assets/explorer-D0srXT1c.js +0 -4
- package/dist/web/public/assets/explorer-D0srXT1c.js.br +0 -0
- package/dist/web/public/assets/explorer-D0srXT1c.js.gz +0 -0
- package/dist/web/public/assets/index-DbFu15NN.js +0 -85
- package/dist/web/public/assets/index-DbFu15NN.js.br +0 -0
- package/dist/web/public/assets/index-DbFu15NN.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +0 -2
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cv0A-e08.js +0 -1
- package/dist/web/public/assets/runs-Cv0A-e08.js.br +0 -0
- package/dist/web/public/assets/runs-Cv0A-e08.js.gz +0 -0
- package/dist/web/public/assets/settings-VmCjhGBd.js +0 -5
- package/dist/web/public/assets/settings-VmCjhGBd.js.br +0 -0
- package/dist/web/public/assets/settings-VmCjhGBd.js.gz +0 -0
- package/dist/web/public/assets/task-runs-CqpTV644.js +0 -3
- package/dist/web/public/assets/task-runs-CqpTV644.js.br +0 -0
- package/dist/web/public/assets/task-runs-CqpTV644.js.gz +0 -0
- package/dist/web/public/assets/tasks-BffPVgXg.js +0 -4
- package/dist/web/public/assets/tasks-BffPVgXg.js.br +0 -0
- package/dist/web/public/assets/tasks-BffPVgXg.js.gz +0 -0
package/dist/main.js
CHANGED
|
@@ -9,6 +9,7 @@ import { ConfigSync } from "./config-sync.js";
|
|
|
9
9
|
import { configSyncTask } from "./config-sync-task.js";
|
|
10
10
|
import { CredentialStore } from "./agent/credentials.js";
|
|
11
11
|
import { IndexedListing } from "./agent/listing.js";
|
|
12
|
+
import { PiPackageStore } from "./agent/packages.js";
|
|
12
13
|
import { PiAgentFactory } from "./agent/pi.js";
|
|
13
14
|
import { defaultBoardsDir, registerBoardRoutes } from "./boards/boards.js";
|
|
14
15
|
import { ChannelStore } from "./channels/config.js";
|
|
@@ -16,15 +17,11 @@ import { createControl } from "./channels/control.js";
|
|
|
16
17
|
import { ConversationStore, resolveConversation } from "./channels/conversations.js";
|
|
17
18
|
import { registerChannelRoutes } from "./channels/routes.js";
|
|
18
19
|
import { ChannelRuntime } from "./channels/runtime.js";
|
|
19
|
-
import { SlackApi } from "./channels/slack-api.js";
|
|
20
|
-
import { SlackDirectory } from "./channels/slack-directory.js";
|
|
21
|
-
import { handleSlackTool, slackToolAvailable, slackToolSpec } from "./channels/slack-tool.js";
|
|
22
|
-
import { parseConversation as parseSlackConversation } from "./channels/slack.js";
|
|
23
20
|
import { EventHub } from "./core/hub.js";
|
|
24
21
|
import { splitSpeaker } from "./core/identity.js";
|
|
25
22
|
import { pierDb } from "./db.js";
|
|
26
23
|
import { deliverLedger, drainForRestart, RestartLedger } from "./drain.js";
|
|
27
|
-
import {
|
|
24
|
+
import { BUNDLED } from "./extensions/index.js";
|
|
28
25
|
import { surfacePrompt } from "./core/reply.js";
|
|
29
26
|
import { Router } from "./core/router.js";
|
|
30
27
|
import { acquireInstanceLock } from "./lock.js";
|
|
@@ -32,19 +29,21 @@ import { logger } from "./log.js";
|
|
|
32
29
|
import { registerTaskRoutes } from "./tasks/routes.js";
|
|
33
30
|
import { TaskService } from "./tasks/service.js";
|
|
34
31
|
import { TaskStore } from "./tasks/store.js";
|
|
35
|
-
import { taskToolSpec } from "./tasks/tool.js";
|
|
36
32
|
import { PIER_HOME, pierPath, resolveAgentDir } from "./paths.js";
|
|
37
|
-
import { CUSTOM_TOOL_RULES, MANAGED, ManagedTools, normalizeCustomTools, prependPath } from "./tools.js";
|
|
33
|
+
import { CUSTOM_TOOL_RULES, MANAGED, ManagedTools, normalizeCustomTools, prependPath, writePierShim } from "./tools.js";
|
|
38
34
|
import { toolsTask } from "./tools-task.js";
|
|
39
35
|
import { Secrets } from "./secrets.js";
|
|
40
36
|
import { startUpdate, unitPath, updaterProblem } from "./service.js";
|
|
41
37
|
import { SettingsStore } from "./settings.js";
|
|
42
|
-
import { startAutoUpdate, UpdateCheck } from "./update.js";
|
|
38
|
+
import { currentVersion, startAutoUpdate, UpdateCheck } from "./update.js";
|
|
39
|
+
import { Vault } from "./vault.js";
|
|
40
|
+
import { servePier } from "./socket.js";
|
|
43
41
|
import { AuthStore, registerAuthRoutes, requireAuth } from "./web/auth.js";
|
|
44
42
|
import { registerConfigShareRoute, registerConfigSyncRoutes } from "./web/config-sync.js";
|
|
45
43
|
import { PushStore, registerPushRoutes } from "./web/push.js";
|
|
46
44
|
import { SessionStateStore } from "./web/session-state.js";
|
|
47
45
|
import { createServer } from "./web/server.js";
|
|
46
|
+
import { registerVaultRoutes } from "./web/vault.js";
|
|
48
47
|
const log = logger("pier");
|
|
49
48
|
// Before any SDK call resolves a path, so everything Pi derives from its agent
|
|
50
49
|
// dir lands under PIER_HOME. PIER_AGENT_DIR marks the value as ours: a second
|
|
@@ -53,6 +52,7 @@ const log = logger("pier");
|
|
|
53
52
|
process.env.PI_CODING_AGENT_DIR = resolveAgentDir(process.env);
|
|
54
53
|
process.env.PIER_AGENT_DIR = process.env.PI_CODING_AGENT_DIR;
|
|
55
54
|
prependPath(process.env);
|
|
55
|
+
writePierShim();
|
|
56
56
|
// Before the database and before task recovery, which would mark the running
|
|
57
57
|
// instance's queued and active runs interrupted; the port is discovered far too
|
|
58
58
|
// late, and a second Pier on another port would never notice at all.
|
|
@@ -76,44 +76,26 @@ const settings = new SettingsStore(db);
|
|
|
76
76
|
// Unlocked below: vt mode waits on a human approval, and nothing that needs a
|
|
77
77
|
// token may run before the key arrives.
|
|
78
78
|
const secrets = new Secrets();
|
|
79
|
+
const vault = new Vault(secrets, db);
|
|
79
80
|
let tasks;
|
|
80
81
|
const conversations = new ConversationStore(db);
|
|
81
82
|
let resolveIm;
|
|
82
83
|
let channelStore;
|
|
83
|
-
// Shared by the adapter and the tool: one display-name lookup per process.
|
|
84
|
-
const slackDirectory = new SlackDirectory((m) => logger("slack").warn(m));
|
|
85
84
|
let readyForConfigReload = false;
|
|
86
85
|
const piConfig = new PiConfigStore();
|
|
86
|
+
// Before anything reads settings.json: a first boot gets Pier's seed file.
|
|
87
|
+
await piConfig.seedSettings();
|
|
87
88
|
const configSync = new ConfigSync({
|
|
88
89
|
db, settings, config: piConfig, normalizeAgent: normalizeAgentSnapshot,
|
|
89
90
|
reload: () => readyForConfigReload ? reloadInstance() : Promise.resolve(),
|
|
90
91
|
});
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
slackToolSpec((params, callerSessionId) => handleSlackTool({
|
|
94
|
-
store: channelStore,
|
|
95
|
-
directory: slackDirectory,
|
|
96
|
-
// Per call: the Console can change the token underneath us.
|
|
97
|
-
client: () => {
|
|
98
|
-
const config = channelStore.get("slack");
|
|
99
|
-
return config.token ? new SlackApi(config.token, config.appToken) : null;
|
|
100
|
-
},
|
|
101
|
-
// Per call: the mapping is durable, the session is not.
|
|
102
|
-
here: (sessionId) => {
|
|
103
|
-
const key = router.conversationOf(sessionId);
|
|
104
|
-
if (key?.channelId !== "slack")
|
|
105
|
-
return null;
|
|
106
|
-
const { channel, threadTs } = parseSlackConversation(key.conversationId);
|
|
107
|
-
return channel && threadTs ? { channel, threadTs } : null;
|
|
108
|
-
},
|
|
109
|
-
log: (m) => logger("slack.tool").warn(m),
|
|
110
|
-
}, params, callerSessionId), () => slackToolAvailable(channelStore)),
|
|
111
|
-
],
|
|
92
|
+
const skillsDir = fileURLToPath(new URL("../skills", import.meta.url));
|
|
93
|
+
const factory = new PiAgentFactory(
|
|
112
94
|
// Getters, read per session open: a Console change reaches the next session
|
|
113
95
|
// without a restart.
|
|
114
96
|
() => surfacePrompt({ boardsDir: defaultBoardsDir(), publicUrl: settings.get().publicUrl }),
|
|
115
97
|
// Documents Pier's own tools, so it loads only inside a Pier session.
|
|
116
|
-
[
|
|
98
|
+
[skillsDir], new CredentialStore(db, secrets), piConfig, () => settings.get().modelMenu, () => settings.get(), () => settings.get().titleModel,
|
|
117
99
|
// Transcripts carry the speaker header core wrote for the model.
|
|
118
100
|
new IndexedListing(undefined, undefined, (text) => splitSpeaker(text).text));
|
|
119
101
|
const hub = new EventHub();
|
|
@@ -137,7 +119,10 @@ const toolsUpdate = toolsTask(tasks);
|
|
|
137
119
|
const reconciled = await toolsUpdate.reconcile();
|
|
138
120
|
if ("problem" in reconciled)
|
|
139
121
|
log.error(`tools cannot be managed: ${reconciled.problem}`);
|
|
140
|
-
|
|
122
|
+
const packages = new PiPackageStore(piConfig, { version: currentVersion(), settings }, [skillsDir]);
|
|
123
|
+
// At boot, not lazily: the answer waits for the next Console open (update.ts).
|
|
124
|
+
packages.watchUpdates();
|
|
125
|
+
channelStore = new ChannelStore(db, vault);
|
|
141
126
|
const control = createControl({ router, factory, conversations, store: channelStore });
|
|
142
127
|
const channels = new ChannelRuntime(channelStore, router, control);
|
|
143
128
|
resolveIm = resolveConversation(conversations, factory, control.launchFor, (message) => logger("channels").warn(message));
|
|
@@ -264,6 +249,7 @@ registerConfigSyncRoutes(app, {
|
|
|
264
249
|
});
|
|
265
250
|
registerTaskRoutes(app, tasks, { factory, router });
|
|
266
251
|
registerChannelRoutes(app, channelStore, channels);
|
|
252
|
+
registerVaultRoutes(app, { vault, doctor: () => secrets.doctor() });
|
|
267
253
|
registerBoardRoutes(app);
|
|
268
254
|
const sessionState = new SessionStateStore(db);
|
|
269
255
|
registerPushRoutes(app, {
|
|
@@ -280,23 +266,20 @@ app.route("/", createServer({
|
|
|
280
266
|
hub,
|
|
281
267
|
sessions: sessionState,
|
|
282
268
|
config: piConfig,
|
|
269
|
+
packages,
|
|
283
270
|
providers: factory,
|
|
284
271
|
settings,
|
|
285
|
-
// Assembled here
|
|
286
|
-
// imports the Pi SDK; extensions and binaries are one kind of switch.
|
|
272
|
+
// Assembled here: the catalog spawns ubix, which web/ may not.
|
|
287
273
|
catalog: async () => {
|
|
288
|
-
const {
|
|
289
|
-
return {
|
|
290
|
-
entries: [...bundledInfo(extensions), ...await managedTools.status(tools, customTools)],
|
|
291
|
-
toolsTaskId: toolsUpdate.id(),
|
|
292
|
-
};
|
|
274
|
+
const { tools, customTools } = settings.get();
|
|
275
|
+
return { entries: await managedTools.status(tools, customTools), toolsTaskId: toolsUpdate.id() };
|
|
293
276
|
},
|
|
294
277
|
// A switch is validated against what this Pier *can* switch, never against
|
|
295
278
|
// a catalog whose custom half the request may be rewriting.
|
|
296
|
-
names:
|
|
279
|
+
names: MANAGED.map((tool) => tool.name),
|
|
297
280
|
onToolsChanged: toolsUpdate.changed,
|
|
298
281
|
validateCustomTools: (raw) => {
|
|
299
|
-
const validated = normalizeCustomTools(raw,
|
|
282
|
+
const validated = normalizeCustomTools(raw, BUNDLED.map((ext) => ext.name));
|
|
300
283
|
return validated ? { tools: validated } : { error: CUSTOM_TOOL_RULES };
|
|
301
284
|
},
|
|
302
285
|
secrets,
|
|
@@ -311,6 +294,15 @@ app.route("/", createServer({
|
|
|
311
294
|
}));
|
|
312
295
|
const port = Number(process.env.PORT ?? 3141);
|
|
313
296
|
const hostname = process.env.HOST ?? "127.0.0.1";
|
|
297
|
+
servePier({
|
|
298
|
+
vault,
|
|
299
|
+
// The deep link an agent's "no secret named X" error carries; loopback when
|
|
300
|
+
// no public URL is set, since nothing in the process can discover one.
|
|
301
|
+
fileUrl: (name) => `${settings.get().publicUrl || `http://127.0.0.1:${String(port)}`}/#/settings/vault?name=${name}`,
|
|
302
|
+
task: (params, callerSessionId) => tasks.handle(params, callerSessionId),
|
|
303
|
+
// Live in the router, or on disk: the same two places a callback target is looked for.
|
|
304
|
+
knows: async (id) => router.stateOf(id) !== undefined || (await factory.find(id)) !== undefined,
|
|
305
|
+
});
|
|
314
306
|
// Every command a turn runs inherits this env: `NODE_ENV=production` makes an
|
|
315
307
|
// agent's `npm install` skip devDependencies, and PORT/HOST would aim its dev
|
|
316
308
|
// server at Pier's socket.
|
package/dist/paths.js
CHANGED
|
@@ -12,6 +12,9 @@ export const PIER_DB = pierPath("db", "pier.db");
|
|
|
12
12
|
/** At the root of PIER_HOME: the claim is the whole instance directory, not the
|
|
13
13
|
* database (lock.ts). */
|
|
14
14
|
export const PIER_LOCK = pierPath("pier.lock");
|
|
15
|
+
/** Where the `pier` CLI reaches the running Pier; the CLI needs the name
|
|
16
|
+
* without opening the database (socket.ts). */
|
|
17
|
+
export const PIER_SOCK = pierPath("pier.sock");
|
|
15
18
|
/** `PI_CODING_AGENT_DIR` is an operator override — unless it equals
|
|
16
19
|
* `PIER_AGENT_DIR`, the value Pier itself set: then a Pier spawned from inside
|
|
17
20
|
* another inherited it, and a leak is not an instruction. */
|
package/dist/secrets.js
CHANGED
|
@@ -146,7 +146,8 @@ function open(key, sealed, aad) {
|
|
|
146
146
|
decipher.setAuthTag(tag);
|
|
147
147
|
return Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
148
148
|
}
|
|
149
|
-
|
|
149
|
+
/** The real vt; vault.ts creates its `approve` records through the same client. */
|
|
150
|
+
export const vtCli = {
|
|
150
151
|
read: (record) => run("vt", ["read", record]),
|
|
151
152
|
create: async (plaintext) => {
|
|
152
153
|
const out = await run("vt", ["create"], plaintext);
|
package/dist/settings.js
CHANGED
|
@@ -99,6 +99,7 @@ export class SettingsStore {
|
|
|
99
99
|
...(titleModel ? { titleModel } : {}),
|
|
100
100
|
autoUpdate: this.#value("autoUpdate") === "1",
|
|
101
101
|
extensions: this.#json("extensions", normalizeExtensions, "a list of names") ?? [],
|
|
102
|
+
skillsOff: this.#json("skillsOff", normalizeNames, "a list of names") ?? [],
|
|
102
103
|
tools: this.#json("tools", normalizeTools, "a list of names") ?? [],
|
|
103
104
|
// `"drop"`: a row the bundled catalog has since taken is redundant, not malformed.
|
|
104
105
|
customTools: this.#json("customTools", (raw) => normalizeCustomTools(raw, [], "drop"), "a list of {name, toml}") ?? [],
|
|
@@ -144,6 +145,10 @@ export class SettingsStore {
|
|
|
144
145
|
this.#set("extensions", JSON.stringify(names));
|
|
145
146
|
return this.get();
|
|
146
147
|
}
|
|
148
|
+
setSkillsOff(names) {
|
|
149
|
+
this.#set("skillsOff", JSON.stringify(names));
|
|
150
|
+
return this.get();
|
|
151
|
+
}
|
|
147
152
|
setTools(names) {
|
|
148
153
|
this.#set("tools", JSON.stringify(names));
|
|
149
154
|
return this.get();
|
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");
|