@timqi/pier 0.0.29 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -125
- package/dist/agent/config.js +24 -11
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +43 -62
- package/dist/agent/listing.js +113 -68
- package/dist/agent/pi.js +204 -211
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +8 -24
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +13 -35
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -260
- package/dist/core/types.js +17 -1
- package/dist/db.js +98 -252
- package/dist/drain.js +58 -51
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +6 -14
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +87 -179
- package/dist/paths.js +10 -26
- package/dist/secrets.js +19 -46
- package/dist/service.js +33 -75
- package/dist/settings.js +42 -65
- package/dist/tasks/agent.js +129 -114
- package/dist/tasks/callbacks.js +9 -19
- package/dist/tasks/command.js +29 -14
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +46 -42
- package/dist/tasks/groups.js +41 -35
- package/dist/tasks/messages.js +121 -182
- package/dist/tasks/outbox.js +61 -55
- package/dist/tasks/routes.js +5 -11
- package/dist/tasks/runs.js +14 -13
- package/dist/tasks/service.js +53 -54
- package/dist/tasks/store.js +53 -30
- package/dist/tasks/tool.js +132 -61
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +100 -327
- package/dist/update.js +21 -44
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +45 -85
- package/dist/web/providers.js +14 -13
- package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-B89_hH7q.js} +2 -2
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js +1 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js +1 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js +5 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js +3 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +100 -130
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +2 -2
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +131 -180
- package/dist/web/session-state.js +14 -54
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +153 -160
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
- package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
- package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
- package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
- package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
- package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
- package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
- package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
- package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
package/dist/tasks/tool.js
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { isAbsolute, resolve } from "node:path";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { logger } from "../log.js";
|
|
4
|
+
import { record, requiredString } from "./definitions.js";
|
|
5
|
+
import { isTerminal } from "./types.js";
|
|
6
|
+
const log = logger("tasks");
|
|
6
7
|
// JSON-Schema enum emits ~1/3 the tokens of typebox's anyOf-of-consts.
|
|
7
8
|
const strEnum = (...values) => Type.Unsafe({ type: "string", enum: [...values] });
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* A run summary has eighteen fields and most are empty for most of a run's
|
|
12
|
-
* life; a model reads "absent" and "null" the same way. On a `get` that lists
|
|
13
|
-
* several runs this is a third of the payload.
|
|
14
|
-
*
|
|
15
|
-
* The input names every field — a summary that forgot one would otherwise pass
|
|
16
|
-
* as "that field was empty" — and the result is the type with the empty ones
|
|
17
|
-
* gone, which is why those are declared optional above.
|
|
18
|
-
*/
|
|
9
|
+
/** Absent instead of `null`: a model reads both the same way, and on a group
|
|
10
|
+
* summary the nulls are a third of the payload. The input names every field
|
|
11
|
+
* so a summary that forgot one cannot pass as "empty". */
|
|
19
12
|
const defined = (value) => Object.fromEntries(Object.entries(value).filter(([, v]) => v !== null && v !== undefined));
|
|
20
13
|
const summarize = (run, pendingDecisionId) => defined({
|
|
21
14
|
runId: run.id,
|
|
@@ -28,6 +21,7 @@ const summarize = (run, pendingDecisionId) => defined({
|
|
|
28
21
|
targetSessionId: run.targetSessionId,
|
|
29
22
|
callbackSessionId: run.callbackSessionId,
|
|
30
23
|
callbackState: run.callbackState,
|
|
24
|
+
callbackMode: run.callbackMode ?? null,
|
|
31
25
|
pendingDecisionId,
|
|
32
26
|
depth: run.depth,
|
|
33
27
|
queuedAt: run.queuedAt,
|
|
@@ -36,9 +30,35 @@ const summarize = (run, pendingDecisionId) => defined({
|
|
|
36
30
|
result: run.result,
|
|
37
31
|
error: run.error,
|
|
38
32
|
skipReason: run.skipReason,
|
|
33
|
+
next: null,
|
|
39
34
|
});
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
35
|
+
/** The receipt says the callback is the whole answer, or a model that just
|
|
36
|
+
* launched work reaches for a status call. */
|
|
37
|
+
const receipt = (summary, callbackSessionId, mode, callerSessionId) => ({
|
|
38
|
+
...summary,
|
|
39
|
+
next: callbackSessionId === null
|
|
40
|
+
? "callback none: the result is not delivered to anyone"
|
|
41
|
+
: callbackSessionId !== callerSessionId
|
|
42
|
+
? `the result is delivered to session ${callbackSessionId}; this session will not receive a callback`
|
|
43
|
+
: mode === "steer"
|
|
44
|
+
? "the result interrupts your running turn as a steer message; nothing to query"
|
|
45
|
+
: "the result arrives as a callback message once your turn ends; nothing to query",
|
|
46
|
+
});
|
|
47
|
+
const SUBAGENT_REDIRECT = "subagents cannot redirect callbacks (callback_session_id)";
|
|
48
|
+
/** Who a new run's result goes to — the caller, nobody, or a named session that
|
|
49
|
+
* must exist. Shared by `run` and `resume`: a resumed run is a new run. */
|
|
50
|
+
const callbackTarget = async (input, definitions, callerSessionId) => {
|
|
51
|
+
if (input.callback_session_id === undefined)
|
|
52
|
+
return input.callback === "none" ? null : callerSessionId;
|
|
53
|
+
if (input.callback === "none")
|
|
54
|
+
throw new Error("callback none and callback_session_id conflict: pick one delivery target");
|
|
55
|
+
const target = requiredString(input.callback_session_id, "callback_session_id");
|
|
56
|
+
if (!(await definitions.sessionExists(target)))
|
|
57
|
+
throw new Error(`unknown session: ${target}`);
|
|
58
|
+
return target;
|
|
59
|
+
};
|
|
60
|
+
/** A group echoes many results at once, so each is capped; a single-run
|
|
61
|
+
* `recover` stays whole — it is the escape hatch every truncation note points at. */
|
|
42
62
|
const trimResult = (summary) => {
|
|
43
63
|
if (summary.result?.type !== "agent" || summary.result.text.length <= 2000)
|
|
44
64
|
return summary;
|
|
@@ -46,24 +66,36 @@ const trimResult = (summary) => {
|
|
|
46
66
|
...summary,
|
|
47
67
|
result: {
|
|
48
68
|
...summary.result,
|
|
49
|
-
text: `${summary.result.text.slice(0, 2000)}\n[truncated —
|
|
69
|
+
text: `${summary.result.text.slice(0, 2000)}\n[truncated — recover run_id ${summary.runId} with a reason for the full text]`,
|
|
50
70
|
},
|
|
51
71
|
};
|
|
52
72
|
};
|
|
73
|
+
/** Delivered, given up on and reported, or never owed (`callback:"none"`).
|
|
74
|
+
* Anything else is still on its way, and reading it here would be reading it twice. */
|
|
75
|
+
const settled = (callback) => callback.callbackState === null || callback.callbackState === "delivered" || callback.callbackState === "abandoned";
|
|
76
|
+
/** The same words for queued, running, pending and retrying: a refusal that
|
|
77
|
+
* named the state would be the status query this operation replaced. */
|
|
78
|
+
const notRecoverable = (what, callback) => {
|
|
79
|
+
throw new Error(callback.callbackSessionId === null
|
|
80
|
+
? `${what} is not recoverable yet: it was launched with callback none, so nothing will be delivered; recover reads finished results only and cannot wait for work`
|
|
81
|
+
: `${what} is not recoverable yet: wait for automatic delivery to session ${callback.callbackSessionId}; recover cannot wait for work`);
|
|
82
|
+
};
|
|
53
83
|
const summarizeGroup = (group, members, messages) => defined({
|
|
54
84
|
groupId: group.id,
|
|
55
85
|
join: group.join,
|
|
56
86
|
state: group.finishedAt ? "finished" : "running",
|
|
57
87
|
callbackState: group.callbackState,
|
|
88
|
+
callbackMode: group.callbackMode ?? null,
|
|
58
89
|
winnerRunId: group.winnerRunId,
|
|
59
90
|
members: members.map((run) => trimResult(summarize(run, messages.openDecisionId(run.id)))),
|
|
91
|
+
next: null,
|
|
60
92
|
});
|
|
61
93
|
const LaunchSchema = Type.Object({
|
|
62
94
|
model: Type.Optional(Type.Object({ provider: Type.String(), id: Type.String() })),
|
|
63
95
|
thinking: Type.Optional(Type.String()),
|
|
64
96
|
});
|
|
65
|
-
//
|
|
66
|
-
//
|
|
97
|
+
// Guidance only: runtime truth stays in parseDraft, so schema drift cannot
|
|
98
|
+
// loosen boundary validation.
|
|
67
99
|
const DraftSchema = Type.Object({
|
|
68
100
|
name: Type.Optional(Type.String({ description: "Defaults to the prompt's first line." })),
|
|
69
101
|
description: Type.Optional(Type.String()),
|
|
@@ -103,34 +135,32 @@ export function taskToolSpec(execute) {
|
|
|
103
135
|
return {
|
|
104
136
|
name: "task",
|
|
105
137
|
label: "Pier Task",
|
|
106
|
-
description: "Manage durable Pier tasks and subagents. Agent tasks run in a fresh session or a reused one. create files a definition the operator sees in the Console — only for schedules or roles you will run again; a one-off is run with a prompt. Run executes a stored task by task_id, a one-shot subagent from a prompt (shorthand: prompt + optional cwd/launch/name — cwd defaults to your own directory, relative paths resolve against it, name comes from the prompt) or from a full inline task draft, or a core-joined fan-out via tasks[] with join all|first.
|
|
138
|
+
description: "Manage durable Pier tasks and subagents. Agent tasks run in a fresh session or a reused one. create files a definition the operator sees in the Console — only for schedules or roles you will run again; a one-off is run with a prompt. Run executes a stored task by task_id, a one-shot subagent from a prompt (shorthand: prompt + optional cwd/launch/name/timeoutSeconds — cwd defaults to your own directory, relative paths resolve against it, name comes from the prompt) or from a full inline task draft, or a core-joined fan-out via tasks[] with join all|first. Every operation returns immediately: results, group joins, and decision replies arrive as callback messages once your turn ends — there is no status query; pass callback 'steer' to have a result interrupt your running turn instead, or 'none' for no callback at all. recover (run_id or group_id, plus a reason) re-reads a finished result after its callback has settled — for truncated text or lost context, never to check progress. Use steer/follow_up/resume for child control and contact/reply for supervisor decisions. models lists the deployment's model menu (operator pins with intent notes, else the live catalog).",
|
|
107
139
|
parameters: Type.Object({
|
|
108
|
-
operation: strEnum("list", "create", "update", "run", "
|
|
140
|
+
operation: strEnum("list", "create", "update", "run", "recover", "cancel", "steer", "follow_up", "resume", "contact", "reply", "models"),
|
|
109
141
|
task_id: Type.Optional(Type.String()),
|
|
110
142
|
run_id: Type.Optional(Type.String()),
|
|
111
143
|
group_id: Type.Optional(Type.String()),
|
|
112
144
|
message_id: Type.Optional(Type.String()),
|
|
113
145
|
message: Type.Optional(Type.String()),
|
|
114
|
-
reason: Type.Optional(
|
|
146
|
+
reason: Type.Optional(Type.String({ description: "contact: progress | decision. recover: why the delivered callback is not enough (required)." })),
|
|
115
147
|
session_mode: Type.Optional(strEnum("fresh")),
|
|
116
|
-
// The one-shot shorthand: a prompt is the whole delegation, and the
|
|
117
|
-
// fresh session in the caller's own directory is what it means.
|
|
118
148
|
prompt: Type.Optional(Type.String()),
|
|
119
149
|
cwd: Type.Optional(Type.String()),
|
|
120
150
|
launch: Type.Optional(LaunchSchema),
|
|
121
151
|
name: Type.Optional(Type.String()),
|
|
152
|
+
timeoutSeconds: Type.Optional(Type.Number({ description: "1–86400; defaults to 3600." })),
|
|
122
153
|
task: Type.Optional(DraftSchema),
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
// one points at it, and `parseDraft` is what actually validates either.
|
|
154
|
+
// Spelled out, the draft schema costs more tokens per session than the
|
|
155
|
+
// rest of this contract; `parseDraft` validates either shape.
|
|
126
156
|
tasks: Type.Optional(Type.Unsafe({
|
|
127
157
|
type: "array",
|
|
128
|
-
description: "2+ entries, each a prompt string, {prompt, cwd?, launch?, name?}, a task draft shaped exactly like `task`, or {task_id}.",
|
|
158
|
+
description: "2+ entries, each a prompt string, {prompt, cwd?, launch?, name?, timeoutSeconds?}, a task draft shaped exactly like `task`, or {task_id}.",
|
|
129
159
|
items: { type: "object" },
|
|
130
160
|
})),
|
|
131
161
|
join: Type.Optional(strEnum("all", "first")),
|
|
132
162
|
input: Type.Optional(Type.Unknown()),
|
|
133
|
-
callback: Type.Optional(strEnum("origin", "none")),
|
|
163
|
+
callback: Type.Optional(strEnum("origin", "none", "steer")),
|
|
134
164
|
callback_session_id: Type.Optional(Type.String()),
|
|
135
165
|
}),
|
|
136
166
|
execute,
|
|
@@ -156,12 +186,19 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
156
186
|
return definitions.update(requiredString(input.task_id, "task_id"), await expandDraft(definitions, input.task, callerSessionId));
|
|
157
187
|
}
|
|
158
188
|
if (input.operation === "run") {
|
|
189
|
+
const callbackMode = input.callback === "steer" ? "steer" : "followUp";
|
|
190
|
+
// A run's own callback is its parent's link back; a child that could point
|
|
191
|
+
// it elsewhere would strand the supervisor waiting for a result.
|
|
192
|
+
if (active && input.callback_session_id !== undefined)
|
|
193
|
+
throw new Error(SUBAGENT_REDIRECT);
|
|
159
194
|
if (Array.isArray(input.tasks)) {
|
|
160
195
|
// Core-joined fan-out: members run detached, one aggregated callback.
|
|
161
196
|
if (input.task !== undefined || input.task_id !== undefined)
|
|
162
197
|
throw new Error("use either task/task_id or tasks[]");
|
|
163
198
|
if (input.session_mode !== undefined)
|
|
164
199
|
throw new Error("session_mode applies to a single run only");
|
|
200
|
+
if (input.callback_session_id !== undefined)
|
|
201
|
+
throw new Error("callback_session_id applies to a single run only");
|
|
165
202
|
if (input.tasks.length < 2)
|
|
166
203
|
throw new Error("tasks[] needs at least 2 entries; use task for a single run");
|
|
167
204
|
const resolved = [];
|
|
@@ -173,8 +210,9 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
173
210
|
? await resolveDraft(definitions, entry, active, callerSessionId)
|
|
174
211
|
: resolveStored(definitions, entry.task_id, active));
|
|
175
212
|
}
|
|
176
|
-
const
|
|
177
|
-
|
|
213
|
+
const groupCallbackSessionId = input.callback === "none" ? null : callerSessionId;
|
|
214
|
+
const { group, runs } = host.runGroup(resolved, input.join === "first" ? "first" : "all", callerSessionId, active?.id ?? null, groupCallbackSessionId, callbackMode);
|
|
215
|
+
return receipt(summarizeGroup(group, runs, messages), groupCallbackSessionId, callbackMode, callerSessionId);
|
|
178
216
|
}
|
|
179
217
|
const draft = input.task_id === undefined ? inlineDraft(input) : undefined;
|
|
180
218
|
const task = draft
|
|
@@ -186,33 +224,56 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
186
224
|
throw new Error(`unsupported session_mode: ${String(input.session_mode)}`);
|
|
187
225
|
}
|
|
188
226
|
const sessionMode = input.session_mode;
|
|
189
|
-
|
|
190
|
-
if (!active && callbackSessionId && typeof input.callback_session_id === "string") {
|
|
191
|
-
callbackSessionId = requiredString(input.callback_session_id, "callback_session_id");
|
|
192
|
-
if (!(await definitions.sessionExists(callbackSessionId)))
|
|
193
|
-
throw new Error(`unknown session: ${callbackSessionId}`);
|
|
194
|
-
}
|
|
227
|
+
const callbackSessionId = await callbackTarget(input, definitions, callerSessionId);
|
|
195
228
|
const run = host.run(task.id, input.input, "agent", active?.id ?? null, {
|
|
196
229
|
invokedBySessionId: callerSessionId,
|
|
197
230
|
sourceSessionId: callerSessionId,
|
|
198
231
|
callbackSessionId,
|
|
232
|
+
callbackMode,
|
|
199
233
|
background: true,
|
|
200
234
|
sessionMode,
|
|
201
235
|
});
|
|
202
|
-
return summarize(run, null);
|
|
236
|
+
return receipt(summarize(run, null), callbackSessionId, callbackMode, callerSessionId);
|
|
203
237
|
}
|
|
204
|
-
if (input.operation === "
|
|
238
|
+
if (input.operation === "recover") {
|
|
239
|
+
// History only, never status: readable once the callback has said its last
|
|
240
|
+
// word. The required reason is the friction, and the operator sees it.
|
|
241
|
+
const reason = requiredString(input.reason, "reason");
|
|
242
|
+
// The open question is the notification; the reply's continuation reports.
|
|
243
|
+
const decisionOpen = (run) => {
|
|
244
|
+
throw new Error(`run ${run.id} finished awaiting your decision ${messages.openDecisionId(run.id) ?? ""}; reply to it — the continuation's callback brings the result`);
|
|
245
|
+
};
|
|
246
|
+
// A race winner need not wait for losing members to finish cancelling.
|
|
247
|
+
const groupReady = (group) => {
|
|
248
|
+
if (!group.finishedAt || !settled(group))
|
|
249
|
+
notRecoverable(`group ${group.id}`, group);
|
|
250
|
+
};
|
|
205
251
|
if (typeof input.group_id === "string") {
|
|
206
252
|
const { group, members } = host.getGroup(input.group_id);
|
|
253
|
+
groupReady(group);
|
|
254
|
+
if (!members.every((run) => isTerminal(run.state))) {
|
|
255
|
+
throw new Error(`recover cannot inspect active members; the race has settled — recover its winning result with run_id ${group.winnerRunId ?? "from the callback"} and a reason`);
|
|
256
|
+
}
|
|
257
|
+
const asking = members.find((run) => messages.openDecisionId(run.id));
|
|
258
|
+
if (asking)
|
|
259
|
+
decisionOpen(asking);
|
|
260
|
+
log.info(`recover group ${group.id} by ${callerSessionId}: ${reason}`);
|
|
207
261
|
return summarizeGroup(group, members, messages);
|
|
208
262
|
}
|
|
209
|
-
// Run history by task: without it, checking what a task did (or whether a
|
|
210
|
-
// cascade landed) means leaving the tool for the database.
|
|
211
|
-
if (input.run_id === undefined && typeof input.task_id === "string") {
|
|
212
|
-
return host.listRuns(input.task_id, 10).map((run) => trimResult(summarize(run, messages.openDecisionId(run.id))));
|
|
213
|
-
}
|
|
214
263
|
const run = host.getRun(requiredString(input.run_id, "run_id"));
|
|
215
|
-
|
|
264
|
+
if (run.groupId) {
|
|
265
|
+
const { group } = host.getGroup(run.groupId);
|
|
266
|
+
groupReady(group);
|
|
267
|
+
if (!isTerminal(run.state))
|
|
268
|
+
throw new Error("the group has reported its outcome; recover reads finished results only and cannot wait for this member");
|
|
269
|
+
}
|
|
270
|
+
else if (!isTerminal(run.state) || !settled(run)) {
|
|
271
|
+
notRecoverable(`run ${run.id}`, run);
|
|
272
|
+
}
|
|
273
|
+
if (messages.openDecisionId(run.id))
|
|
274
|
+
decisionOpen(run);
|
|
275
|
+
log.info(`recover run ${run.id} by ${callerSessionId}: ${reason}`);
|
|
276
|
+
return summarize(run, null);
|
|
216
277
|
}
|
|
217
278
|
if (input.operation === "cancel") {
|
|
218
279
|
if (typeof input.group_id === "string") {
|
|
@@ -235,17 +296,28 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
235
296
|
if (input.operation === "resume") {
|
|
236
297
|
const prior = host.getRun(requiredString(input.run_id, "run_id"));
|
|
237
298
|
assertOwns(store, callerSessionId, active, prior);
|
|
299
|
+
// The resumed run is a new run, so it carries its own callback options,
|
|
300
|
+
// under the same rule as `run`: a subagent may not redirect them.
|
|
301
|
+
if (active && input.callback_session_id !== undefined)
|
|
302
|
+
throw new Error(SUBAGENT_REDIRECT);
|
|
303
|
+
const callbackMode = input.callback === "steer" ? "steer" : "followUp";
|
|
304
|
+
const callbackSessionId = await callbackTarget(input, definitions, callerSessionId);
|
|
238
305
|
const run = host.resume(prior.id, requiredString(input.message, "message"), {
|
|
239
306
|
invokedBySessionId: callerSessionId,
|
|
240
|
-
callbackSessionId
|
|
307
|
+
callbackSessionId,
|
|
308
|
+
callbackMode,
|
|
241
309
|
background: true,
|
|
242
310
|
});
|
|
243
|
-
return summarize(run, null);
|
|
311
|
+
return receipt(summarize(run, null), callbackSessionId, callbackMode, callerSessionId);
|
|
244
312
|
}
|
|
245
313
|
if (input.operation === "contact") {
|
|
246
314
|
if (!active)
|
|
247
315
|
throw new Error("contact is only available inside an active Agent run");
|
|
248
|
-
|
|
316
|
+
// The schema no longer narrows `reason` (recover shares the field), so the
|
|
317
|
+
// two names contact accepts are checked here.
|
|
318
|
+
const reason = input.reason === undefined ? "progress" : input.reason;
|
|
319
|
+
if (reason !== "progress" && reason !== "decision")
|
|
320
|
+
throw new Error(`contact reason must be progress or decision, got ${String(reason)}`);
|
|
249
321
|
return messages.contact(active, callerSessionId, reason, requiredString(input.message, "message"));
|
|
250
322
|
}
|
|
251
323
|
if (input.operation === "reply") {
|
|
@@ -255,28 +327,22 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
255
327
|
}
|
|
256
328
|
/** A single run's draft: the top-level shorthand (`prompt` …) or `task`, never both. */
|
|
257
329
|
function inlineDraft(input) {
|
|
258
|
-
const { prompt, cwd, launch, name } = input;
|
|
330
|
+
const { prompt, cwd, launch, name, timeoutSeconds } = input;
|
|
259
331
|
if (prompt === undefined)
|
|
260
332
|
return record(input.task) ?? undefined;
|
|
261
333
|
if (input.task !== undefined)
|
|
262
334
|
throw new Error("use either prompt or task");
|
|
263
|
-
return { prompt, cwd, launch, name };
|
|
335
|
+
return { prompt, cwd, launch, name, timeoutSeconds };
|
|
264
336
|
}
|
|
265
|
-
/**
|
|
266
|
-
* not an identifier — the run's id is what anything addresses. */
|
|
337
|
+
/** A label for the Console, not an identifier. */
|
|
267
338
|
function nameFromPrompt(prompt) {
|
|
268
339
|
const line = prompt.split("\n")
|
|
269
340
|
.map((l) => l.replace(/^[\s#>*-]+/, "").replace(/[*_`]/g, "").replace(/\s+/g, " ").trim())
|
|
270
341
|
.find(Boolean) ?? "subagent";
|
|
271
342
|
return line.length > 60 ? `${line.slice(0, 59).trimEnd()}…` : line;
|
|
272
343
|
}
|
|
273
|
-
/**
|
|
274
|
-
*
|
|
275
|
-
* A `prompt` shorthand becomes a fresh Agent action; a fresh session's cwd
|
|
276
|
-
* resolves against the caller's own directory (and is that directory when
|
|
277
|
-
* omitted); a missing name is the prompt's first line. Everything the caller
|
|
278
|
-
* did spell out passes through untouched — parseDraft still judges it.
|
|
279
|
-
*/
|
|
344
|
+
/** A `prompt` shorthand becomes a fresh Agent action in the caller's own
|
|
345
|
+
* directory; everything the caller did spell out passes through to parseDraft. */
|
|
280
346
|
async function expandDraft(definitions, raw, callerSessionId) {
|
|
281
347
|
let draft = record(raw);
|
|
282
348
|
if (!draft)
|
|
@@ -299,8 +365,8 @@ async function expandDraft(definitions, raw, callerSessionId) {
|
|
|
299
365
|
draft = { ...draft, name: nameFromPrompt(action.prompt) };
|
|
300
366
|
return draft;
|
|
301
367
|
}
|
|
302
|
-
/**
|
|
303
|
-
*
|
|
368
|
+
/** Persisted like any task (kind "subagent", filtered from default lists) so
|
|
369
|
+
* runs stay auditable and resumable. */
|
|
304
370
|
async function resolveDraft(definitions, raw, active, callerSessionId) {
|
|
305
371
|
const draft = record(await expandDraft(definitions, raw, callerSessionId));
|
|
306
372
|
if (!draft)
|
|
@@ -308,6 +374,11 @@ async function resolveDraft(definitions, raw, active, callerSessionId) {
|
|
|
308
374
|
if (draft.trigger !== undefined && record(draft.trigger)?.type !== "manual") {
|
|
309
375
|
throw new Error("inline subagent tasks must use a manual trigger");
|
|
310
376
|
}
|
|
377
|
+
// Delivery of a one-off run is the top-level fields' business; a nested
|
|
378
|
+
// callback only means anything on a stored definition's schedule.
|
|
379
|
+
if (draft.callback !== undefined || draft.callback_session_id !== undefined) {
|
|
380
|
+
throw new Error("an inline task draft cannot set callback; use the top-level callback / callback_session_id");
|
|
381
|
+
}
|
|
311
382
|
if (active) {
|
|
312
383
|
const action = record(draft.action);
|
|
313
384
|
if (action?.type !== "agent")
|
package/dist/tools-task.js
CHANGED
|
@@ -1,36 +1,21 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// It lives beside tools.ts rather than inside it because tools.ts may not
|
|
6
|
-
// import tasks/, and outside main.ts because main.ts is wiring: this is the
|
|
7
|
-
// only rule in the instance layer that is neither construction nor a callback.
|
|
8
|
-
// The task's run history *is* the tools status surface — the install, the daily
|
|
9
|
-
// update and every failure are runs with output, so there is no second place to
|
|
10
|
-
// look (§5b).
|
|
1
|
+
// A tools switch becomes exactly one run of the one task Pier owns. Beside
|
|
2
|
+
// tools.ts because tools.ts may not import tasks/; the task's run history is
|
|
3
|
+
// the tools status surface (§5).
|
|
11
4
|
import { existsSync } from "node:fs";
|
|
12
5
|
import { fileURLToPath } from "node:url";
|
|
13
6
|
import { logger } from "./log.js";
|
|
14
7
|
import { PIER_HOME } from "./paths.js";
|
|
15
8
|
import { isTerminal } from "./tasks/types.js";
|
|
16
9
|
import { coalescedSync } from "./tools.js";
|
|
17
|
-
/**
|
|
18
|
-
* rather than one a person wrote, and names itself with it when it writes the
|
|
19
|
-
* definition back (the owner guard in tasks/definitions.ts). */
|
|
10
|
+
/** The owner guard in tasks/definitions.ts: only this creator may write it back. */
|
|
20
11
|
const TOOLS_TASK_CREATOR = "tools";
|
|
21
|
-
//
|
|
22
|
-
// anything an operator greps the journal for.
|
|
12
|
+
// "pier", not "tools": what an operator greps the journal for.
|
|
23
13
|
const log = logger("pier");
|
|
24
|
-
/** POSIX single quotes:
|
|
25
|
-
* double quotes, and this string is run by bash months from now. */
|
|
14
|
+
/** POSIX single quotes: this string is run by bash months from now. */
|
|
26
15
|
const shellQuote = (value) => `'${value.replaceAll("'", `'\\''`)}'`;
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* types through imports that still say `.js`, so it is the same command under
|
|
31
|
-
* tsx — which is what a source checkout has. Neither available is a refusal
|
|
32
|
-
* with a reason, never a task whose script cannot run.
|
|
33
|
-
*/
|
|
16
|
+
/** From a source checkout there is no `cli.js` and node cannot strip types
|
|
17
|
+
* through `.js` imports, so it is the same command under tsx. Neither
|
|
18
|
+
* available is a refusal with a reason, never a task whose script cannot run. */
|
|
34
19
|
const toolsSyncScript = () => {
|
|
35
20
|
const built = fileURLToPath(new URL("./cli.js", import.meta.url));
|
|
36
21
|
if (existsSync(built))
|
|
@@ -49,22 +34,10 @@ const toolsSyncScript = () => {
|
|
|
49
34
|
}
|
|
50
35
|
};
|
|
51
36
|
export function toolsTask(tasks) {
|
|
52
|
-
/** Which task is Pier's, and the id every managed run goes through. */
|
|
53
37
|
let toolsTaskId = null;
|
|
54
|
-
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* Created once and never retired: a task that comes and goes is a state class
|
|
58
|
-
* of its own (two boots racing to create it, a retirement racing a switch),
|
|
59
|
-
* and the run it would have been retired for already says "no tools switched
|
|
60
|
-
* on".
|
|
61
|
-
*
|
|
62
|
-
* It repairs rather than trusts because of what came before the owner guard
|
|
63
|
-
* (tasks/definitions.ts): a definition edited by an older Pier, or by a
|
|
64
|
-
* release where the routes could still write it, is brought back to the one
|
|
65
|
-
* Pier owns here. Nothing can edit it any more — so this is a boot-time
|
|
66
|
-
* repair of state that already exists, not a defence.
|
|
67
|
-
*/
|
|
38
|
+
/** Created once and never retired: a task that comes and goes is a state
|
|
39
|
+
* class of its own, and a run with nothing on already says so. Repairs a
|
|
40
|
+
* definition an older Pier could still edit. */
|
|
68
41
|
const ensureToolsTask = async () => {
|
|
69
42
|
const command = toolsSyncScript();
|
|
70
43
|
if ("problem" in command)
|
|
@@ -83,40 +56,31 @@ export function toolsTask(tasks) {
|
|
|
83
56
|
callback: { type: "none" },
|
|
84
57
|
timeoutSeconds: 1800,
|
|
85
58
|
};
|
|
86
|
-
//
|
|
87
|
-
// replacement is a new one and the old one keeps its history.
|
|
59
|
+
// Nothing can un-archive a task, so an archived one is replaced, history kept.
|
|
88
60
|
const owned = tasks.list().filter((task) => task.creator === TOOLS_TASK_CREATOR && !task.archived);
|
|
89
|
-
//
|
|
90
|
-
// reporting the other's run as an overlap.
|
|
61
|
+
// Two would fight over ubix's state lock every night.
|
|
91
62
|
for (const extra of owned.slice(1)) {
|
|
92
63
|
log.warn(`archiving a second tools update task (${extra.id})`);
|
|
93
64
|
tasks.archive(extra.id, TOOLS_TASK_CREATOR);
|
|
94
65
|
}
|
|
95
66
|
const task = owned[0];
|
|
96
|
-
// Every field
|
|
97
|
-
//
|
|
98
|
-
// while the daily run never happens.
|
|
67
|
+
// Every field, not just the command: a paused or renamed task still claims
|
|
68
|
+
// to keep the tools current while the daily run never happens.
|
|
99
69
|
if (task && Object.entries(draft).some(([key, value]) => JSON.stringify(task[key]) !== JSON.stringify(value))) {
|
|
100
70
|
log.warn("the tools update task was edited — restoring the definition Pier owns");
|
|
101
|
-
// Named as the owner: this is the one path allowed to write it back
|
|
102
|
-
// (tasks/definitions.ts).
|
|
103
71
|
await tasks.update(task.id, draft, TOOLS_TASK_CREATOR);
|
|
104
72
|
}
|
|
105
73
|
const id = task ? task.id : (await tasks.create(draft, TOOLS_TASK_CREATOR)).id;
|
|
106
74
|
toolsTaskId = id;
|
|
107
75
|
return { id };
|
|
108
76
|
};
|
|
109
|
-
/** The half of `coalescedSync` (tools.ts
|
|
110
|
-
* knows what a task is: start a run, and hand back what to wait for — our own
|
|
111
|
-
* run, or the one already in flight that made ours a `skipped` row. */
|
|
77
|
+
/** The half of `coalescedSync` (tools.ts) that knows what a task is. */
|
|
112
78
|
const requestSync = coalescedSync(() => {
|
|
113
79
|
if (!toolsTaskId)
|
|
114
80
|
throw new Error("no tools update task to run");
|
|
115
81
|
const settled = (id) => tasks.waitForRun(id).then(() => undefined);
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
// something to spin on. Three refusals in a row with nothing running is a
|
|
119
|
-
// bug, and it is reported as one rather than retried forever.
|
|
82
|
+
// A run can finish between being in flight and being asked about; three
|
|
83
|
+
// refusals with nothing running is a bug, reported rather than retried.
|
|
120
84
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
121
85
|
const mine = tasks.run(toolsTaskId, null, "manual");
|
|
122
86
|
if (!isTerminal(mine.state))
|
|
@@ -127,8 +91,6 @@ export function toolsTask(tasks) {
|
|
|
127
91
|
}
|
|
128
92
|
throw new Error("the tools sync was refused as an overlap three times with nothing running");
|
|
129
93
|
}, (err) => log.error("the tools sync could not be run", err));
|
|
130
|
-
/** A switch was flipped: make sure the task is the one Pier means, then ask
|
|
131
|
-
* for a sync. Answers with what that switch should say about it. */
|
|
132
94
|
const toolsChanged = async () => {
|
|
133
95
|
try {
|
|
134
96
|
const task = await ensureToolsTask();
|
|
@@ -144,10 +106,8 @@ export function toolsTask(tasks) {
|
|
|
144
106
|
}
|
|
145
107
|
};
|
|
146
108
|
return {
|
|
147
|
-
/**
|
|
148
|
-
* otherwise both find no task and create one each. */
|
|
109
|
+
/** At boot, before any route exists: two first flips could otherwise both create one. */
|
|
149
110
|
reconcile: ensureToolsTask,
|
|
150
|
-
/** The task whose runs are the status surface, null until there is one. */
|
|
151
111
|
id: () => toolsTaskId,
|
|
152
112
|
changed: toolsChanged,
|
|
153
113
|
};
|