@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/agent.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
// A run that *is* a Pi session: which session it opens
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// agents specifically — a bash run costs a process, an agent run costs a
|
|
5
|
-
// model's context and someone's rate limit.
|
|
1
|
+
// A run that *is* a Pi session: which session it opens, what the child is told
|
|
2
|
+
// before the prompt, and how many may run at once (an agent run costs a
|
|
3
|
+
// model's context and someone's rate limit, so the caps are here).
|
|
6
4
|
import { quietLabel, splitReply } from "../core/reply.js";
|
|
7
|
-
import {
|
|
5
|
+
import { logger } from "../log.js";
|
|
8
6
|
import { runSource } from "./callbacks.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const MAX_ACTIVE_AGENTS =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* says which of it does not apply
|
|
15
|
-
* agent that is waiting on it. Skipped on resume: the session already saw it. */
|
|
7
|
+
// Agent runs are I/O-bound: the cap is there for API pressure and runaway
|
|
8
|
+
// fan-out, not for this machine's CPU.
|
|
9
|
+
const MAX_ACTIVE_AGENTS = 6;
|
|
10
|
+
const log = logger("tasks");
|
|
11
|
+
/** Every session gets the chat-surface contract, task runs included, so the
|
|
12
|
+
* delegation prompt says which of it does not apply. Skipped on resume. */
|
|
16
13
|
const preamble = (run) => {
|
|
17
14
|
// A cron/watch task with a session callback is read by an agent too.
|
|
18
15
|
const audience = run.invokedBySessionId
|
|
@@ -44,40 +41,37 @@ export class AgentTaskRunner {
|
|
|
44
41
|
this.changed = changed;
|
|
45
42
|
}
|
|
46
43
|
async execute(run, action, signal, start) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
// Reserve reuse order before resolving the session: a slow resume must not
|
|
45
|
+
// let a later run pass it. Fresh runs each have their own queue key.
|
|
46
|
+
return this.withSession(run.targetSessionId ?? run.id, signal, async () => {
|
|
47
|
+
const reused = run.targetSessionId ? await this.resolveSession(run, action, signal) : undefined;
|
|
48
|
+
if (reused)
|
|
49
|
+
await this.waitUntilIdle(reused, signal);
|
|
50
|
+
await this.acquireSlot(run, signal);
|
|
51
|
+
try {
|
|
52
|
+
const session = reused ?? await this.resolveSession(run, action, signal);
|
|
53
|
+
// A reused session may have become busy while we waited for a slot.
|
|
51
54
|
await this.waitUntilIdle(session, signal);
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// every attempt — and only after the session is idle, so a reused
|
|
56
|
-
// interactive session's in-flight turn keeps its 1h writes.
|
|
55
|
+
signal.throwIfAborted();
|
|
56
|
+
// Task requests come seconds apart, so the 1h cache-write premium never
|
|
57
|
+
// earns back; after idle, so a reused session's in-flight turn keeps its 1h.
|
|
57
58
|
session.setCacheRetention("short");
|
|
58
59
|
start();
|
|
59
|
-
// No input is no block
|
|
60
|
-
//
|
|
61
|
-
// Compact for the same reason tool results are (agent/pi.ts), and
|
|
62
|
-
// `<\/` is the same JSON — a value cannot close the fence early.
|
|
60
|
+
// No input is no block. `<\/` is the same JSON, so a value cannot close
|
|
61
|
+
// the fence early.
|
|
63
62
|
const input = run.input === undefined || run.input === null
|
|
64
63
|
? ""
|
|
65
64
|
: `\n\n<task_input>\n${JSON.stringify(run.input).replaceAll("</task_input>", "<\\/task_input>")}\n</task_input>`;
|
|
66
65
|
const prompt = run.context.resumePrompt ?? `${preamble(run)}${action.prompt}${input}`;
|
|
67
66
|
run.context.sessionId = session.id;
|
|
68
67
|
run.context.model = session.model;
|
|
69
|
-
// The level the session settled on
|
|
70
|
-
// an unspecified effort inherits the caller's, and the card in the
|
|
71
|
-
// subagent's own transcript should say which one that was.
|
|
68
|
+
// The level the session settled on: an unspecified effort inherits the caller's.
|
|
72
69
|
run.context.thinking = session.thinkingLevel;
|
|
73
70
|
run.context.renderedPrompt = prompt;
|
|
74
71
|
this.store.saveRun(run);
|
|
75
72
|
let text = "";
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// reply" — the one wording for a turn that *chose* to say nothing, so
|
|
79
|
-
// the outage arrives at the caller looking like an answer (§5b). Pi has
|
|
80
|
-
// already retried by the time this lands; the run only reports.
|
|
73
|
+
// How it ended, too: a provider outage ends with an empty reply, which
|
|
74
|
+
// would otherwise report as a turn that chose to say nothing (§5).
|
|
81
75
|
let failure;
|
|
82
76
|
const unsubscribe = session.subscribe((event) => {
|
|
83
77
|
if (event.type === "turn-end") {
|
|
@@ -85,22 +79,16 @@ export class AgentTaskRunner {
|
|
|
85
79
|
failure = event.error;
|
|
86
80
|
}
|
|
87
81
|
});
|
|
88
|
-
//
|
|
89
|
-
// a hung turn must not hold one of the 4 slots (and its waiters)
|
|
90
|
-
// forever — the same guard execution.ts gives task-type children.
|
|
91
|
-
let rejectAborted = () => { };
|
|
92
|
-
const abortedTurn = new Promise((_, reject) => { rejectAborted = reject; });
|
|
93
|
-
abortedTurn.catch(() => { }); // handled via the race; never unhandled
|
|
82
|
+
// Cancel the SDK turn, but release our wait even if its abort hangs.
|
|
94
83
|
const abort = () => {
|
|
95
|
-
void session.abort()
|
|
96
|
-
|
|
84
|
+
void Promise.resolve().then(() => session.abort())
|
|
85
|
+
.catch((error) => log.warn(`run ${run.id} session abort failed`, error));
|
|
97
86
|
};
|
|
98
87
|
signal.addEventListener("abort", abort, { once: true });
|
|
99
|
-
// A pre-aborted signal never fires the listener: check before the
|
|
100
|
-
// prompt starts or a cancelled run would hang until its timeout.
|
|
101
|
-
if (signal.aborted)
|
|
102
|
-
throw new Error("cancelled");
|
|
103
88
|
try {
|
|
89
|
+
// Pre-abort does not fire a newly registered listener; keep the check
|
|
90
|
+
// inside finally's scope so it also restores retention/subscriptions.
|
|
91
|
+
signal.throwIfAborted();
|
|
104
92
|
const turn = session.systemInput(prompt, {
|
|
105
93
|
kind: "task-delegation",
|
|
106
94
|
taskId: run.taskId,
|
|
@@ -110,54 +98,49 @@ export class AgentTaskRunner {
|
|
|
110
98
|
}, "prompt");
|
|
111
99
|
await Promise.resolve();
|
|
112
100
|
this.messages.deliverPendingControls(run);
|
|
113
|
-
await
|
|
101
|
+
await this.untilAborted(turn, signal);
|
|
114
102
|
if (signal.aborted)
|
|
115
103
|
throw new Error("cancelled");
|
|
116
|
-
// Before the fallback
|
|
117
|
-
//
|
|
118
|
-
// this run's result.
|
|
104
|
+
// Before the fallback: on a reused session it would read the previous
|
|
105
|
+
// turn's answer back as this run's result.
|
|
119
106
|
if (failure)
|
|
120
107
|
throw new Error(failure);
|
|
121
108
|
if (!text) {
|
|
122
|
-
const history = await session.history();
|
|
109
|
+
const history = await this.untilAborted(session.history(), signal);
|
|
123
110
|
text = [...history].reverse().find((turn) => turn.role === "assistant")?.text ?? "";
|
|
124
111
|
}
|
|
125
|
-
// The
|
|
126
|
-
//
|
|
127
|
-
// supervisor or the Console, never a chat renderer: buttons are
|
|
128
|
-
// dropped, and a turn that said nothing names which kind of nothing
|
|
129
|
-
// it was (principle 5b) instead of storing an empty result.
|
|
112
|
+
// The result is read by a supervisor, never a chat renderer: buttons
|
|
113
|
+
// are dropped, and an empty turn names which kind of nothing (§5).
|
|
130
114
|
const reply = splitReply(text);
|
|
131
115
|
return { type: "agent", text: reply.text || quietLabel(reply.silence), sessionId: session.id };
|
|
132
116
|
}
|
|
133
117
|
finally {
|
|
134
|
-
//
|
|
135
|
-
// back to chat afterwards. For task-created sessions this is moot —
|
|
136
|
-
// idle until the next run downgrades them again.
|
|
118
|
+
// A reused interactive session goes back to chat afterwards.
|
|
137
119
|
session.setCacheRetention("long");
|
|
138
120
|
signal.removeEventListener("abort", abort);
|
|
139
121
|
unsubscribe();
|
|
140
122
|
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
this.releaseSlot(run.id);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
146
128
|
}
|
|
147
|
-
async resolveSession(run, action) {
|
|
129
|
+
async resolveSession(run, action, signal) {
|
|
130
|
+
signal.throwIfAborted();
|
|
148
131
|
if (run.targetSessionId) {
|
|
149
|
-
return this.router.ensure({ channelId: "task", conversationId: run.targetSessionId });
|
|
132
|
+
return this.untilAborted(this.router.ensure({ channelId: "task", conversationId: run.targetSessionId }), signal);
|
|
150
133
|
}
|
|
151
134
|
const policy = action.session;
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
// author meant, and a child in the wrong tree edits real files.
|
|
135
|
+
// Stored definitions may still say `"fork"`. Refused by name: any directory
|
|
136
|
+
// picked instead is a guess, and a child in the wrong tree edits real files.
|
|
155
137
|
if (run.sessionMode === "fork" || policy.mode === "fork") {
|
|
156
138
|
throw new Error(`task "${run.context.definition.name}" uses the removed fork session mode; recreate it with {"mode":"fresh","cwd":"/abs/path"}`);
|
|
157
139
|
}
|
|
158
140
|
const cwd = policy.mode === "fresh"
|
|
159
141
|
? policy.cwd
|
|
160
|
-
: (await this.factory.find(policy.sessionId))?.cwd ?? "";
|
|
142
|
+
: (await this.untilAborted(this.factory.find(policy.sessionId), signal))?.cwd ?? "";
|
|
143
|
+
signal.throwIfAborted();
|
|
161
144
|
if (!cwd)
|
|
162
145
|
throw new Error("could not resolve child working directory");
|
|
163
146
|
const opts = {
|
|
@@ -169,33 +152,50 @@ export class AgentTaskRunner {
|
|
|
169
152
|
(run.sourceSessionId ? this.router.modelOf(run.sourceSessionId) : undefined),
|
|
170
153
|
thinking: action.launch?.thinking,
|
|
171
154
|
};
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
155
|
+
const opening = this.factory.create(opts).then(async (session) => {
|
|
156
|
+
// SDK creation cannot be cancelled; a late session still belongs to this
|
|
157
|
+
// run. Cancellation's final save can race this callback, so both records get it.
|
|
158
|
+
run.targetSessionId = session.id;
|
|
159
|
+
run.context.sessionId = session.id;
|
|
160
|
+
run.context.cwd = cwd;
|
|
161
|
+
const owner = signal.aborted ? this.store.getRun(run.id) ?? run : run;
|
|
162
|
+
owner.targetSessionId = run.targetSessionId;
|
|
163
|
+
owner.context = run.context;
|
|
164
|
+
try {
|
|
165
|
+
this.store.saveRun(owner);
|
|
166
|
+
this.changed(owner);
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
if (signal.aborted)
|
|
170
|
+
await session.dispose();
|
|
171
|
+
else
|
|
172
|
+
this.router.attach({ channelId: "task", conversationId: session.id }, session);
|
|
173
|
+
}
|
|
174
|
+
return session;
|
|
175
|
+
});
|
|
176
|
+
// A late rejection cannot reach execute's already-cancelled race.
|
|
177
|
+
void opening.catch((error) => {
|
|
178
|
+
if (signal.aborted)
|
|
179
|
+
log.warn(`session creation for cancelled run ${run.id} failed`, error);
|
|
180
|
+
});
|
|
181
|
+
const session = await this.untilAborted(opening, signal);
|
|
182
|
+
signal.throwIfAborted();
|
|
179
183
|
return session;
|
|
180
184
|
}
|
|
181
185
|
async acquireSlot(run, signal) {
|
|
182
186
|
while (this.active.size >= MAX_ACTIVE_AGENTS) {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
reject(new Error("cancelled"));
|
|
194
|
-
};
|
|
195
|
-
this.slotWaiters.add(wake);
|
|
196
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
197
|
-
});
|
|
187
|
+
let wake = () => { };
|
|
188
|
+
try {
|
|
189
|
+
await this.untilAborted(new Promise((resolve) => {
|
|
190
|
+
wake = resolve;
|
|
191
|
+
this.slotWaiters.add(wake);
|
|
192
|
+
}), signal);
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
this.slotWaiters.delete(wake);
|
|
196
|
+
}
|
|
198
197
|
}
|
|
198
|
+
signal.throwIfAborted();
|
|
199
199
|
this.active.add(run.id);
|
|
200
200
|
}
|
|
201
201
|
releaseSlot(id) {
|
|
@@ -203,43 +203,58 @@ export class AgentTaskRunner {
|
|
|
203
203
|
for (const wake of this.slotWaiters)
|
|
204
204
|
wake();
|
|
205
205
|
}
|
|
206
|
-
async
|
|
206
|
+
async untilAborted(promise, signal) {
|
|
207
|
+
let abort = () => { };
|
|
208
|
+
const cancelled = new Promise((_, reject) => {
|
|
209
|
+
abort = () => reject(new Error("cancelled"));
|
|
210
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
211
|
+
if (signal.aborted)
|
|
212
|
+
abort();
|
|
213
|
+
});
|
|
214
|
+
try {
|
|
215
|
+
return await Promise.race([promise, cancelled]);
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
signal.removeEventListener("abort", abort);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async withSession(sessionId, signal, fn) {
|
|
222
|
+
signal.throwIfAborted();
|
|
207
223
|
const previous = this.sessionTails.get(sessionId) ?? Promise.resolve();
|
|
208
224
|
let release = () => { };
|
|
209
225
|
const gate = new Promise((resolve) => { release = resolve; });
|
|
210
226
|
const tail = previous.then(() => gate);
|
|
211
227
|
this.sessionTails.set(sessionId, tail);
|
|
212
|
-
|
|
228
|
+
// Even if this waiter is cancelled, its tail still includes its predecessor.
|
|
229
|
+
// Only delete after that tail drains, or a new arrival could skip the queue.
|
|
230
|
+
void tail.then(() => {
|
|
231
|
+
if (this.sessionTails.get(sessionId) === tail)
|
|
232
|
+
this.sessionTails.delete(sessionId);
|
|
233
|
+
});
|
|
213
234
|
try {
|
|
235
|
+
await this.untilAborted(previous, signal);
|
|
236
|
+
signal.throwIfAborted();
|
|
214
237
|
return await fn();
|
|
215
238
|
}
|
|
216
239
|
finally {
|
|
217
240
|
release();
|
|
218
|
-
if (this.sessionTails.get(sessionId) === tail)
|
|
219
|
-
this.sessionTails.delete(sessionId);
|
|
220
241
|
}
|
|
221
242
|
}
|
|
222
243
|
async waitUntilIdle(session, signal) {
|
|
223
244
|
// The session's own event stream is the only busy/idle signal — no polling.
|
|
224
245
|
while (session.state === "streaming") {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const unsubscribe = session.subscribe((event) => {
|
|
238
|
-
if (event.type === "state" && event.state === "idle")
|
|
239
|
-
settle();
|
|
240
|
-
});
|
|
241
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
242
|
-
});
|
|
246
|
+
let unsubscribe = () => { };
|
|
247
|
+
try {
|
|
248
|
+
await this.untilAborted(new Promise((resolve) => {
|
|
249
|
+
unsubscribe = session.subscribe((event) => {
|
|
250
|
+
if (event.type === "state" && event.state === "idle")
|
|
251
|
+
resolve();
|
|
252
|
+
});
|
|
253
|
+
}), signal);
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
unsubscribe();
|
|
257
|
+
}
|
|
243
258
|
}
|
|
244
259
|
}
|
|
245
260
|
}
|
package/dist/tasks/callbacks.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
// What a finished run says to the session that delegated it. Delivery itself
|
|
2
2
|
// belongs to outbox.ts; this file owns the run vocabulary and the batching.
|
|
3
|
-
import { Router } from "../core/router.js";
|
|
4
3
|
import { Outbox } from "./outbox.js";
|
|
5
|
-
|
|
6
|
-
|
|
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. */
|
|
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 tool call. */
|
|
10
6
|
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.
|
|
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. */
|
|
7
|
+
/** What the card in the recipient's transcript says the input came from. */
|
|
15
8
|
export const runSource = (run) => ({
|
|
16
9
|
taskName: run.context.definition.name,
|
|
17
10
|
...(run.context.model ? { model: run.context.model } : {}),
|
|
@@ -28,7 +21,7 @@ export function runResultText(run) {
|
|
|
28
21
|
if (run.result?.type === "watch")
|
|
29
22
|
result = "Watch condition did not match";
|
|
30
23
|
if (result.length > 8000)
|
|
31
|
-
result = `${result.slice(0, 8000)}\n[truncated — task tool
|
|
24
|
+
result = `${result.slice(0, 8000)}\n[truncated — task tool recover run_id ${run.id} with a reason returns the full text]`;
|
|
32
25
|
return result;
|
|
33
26
|
}
|
|
34
27
|
export class TaskCallbacks {
|
|
@@ -49,15 +42,13 @@ export class TaskCallbacks {
|
|
|
49
42
|
runId: runs[0].id,
|
|
50
43
|
sourceSessionId: runs[0].targetSessionId,
|
|
51
44
|
runIds: runs.map((run) => run.id),
|
|
52
|
-
// Only
|
|
53
|
-
//
|
|
54
|
-
// caption would attribute every other result to it. The text names
|
|
55
|
-
// each run; the card says nothing rather than something false.
|
|
45
|
+
// Only for one run: a batch's caption would attribute every result
|
|
46
|
+
// to the first run's name and model.
|
|
56
47
|
...(runs.length === 1 ? { source: runSource(runs[0]), state: runs[0].state } : {}),
|
|
57
48
|
},
|
|
58
49
|
}),
|
|
59
|
-
|
|
60
|
-
}
|
|
50
|
+
abandoned: (run, sessionId, why) => unreachable(sessionId, `the result of "${run.context.definition.name}"`, why),
|
|
51
|
+
});
|
|
61
52
|
}
|
|
62
53
|
target(callback, origin) {
|
|
63
54
|
if (callback.type === "session")
|
|
@@ -77,8 +68,7 @@ export class TaskCallbacks {
|
|
|
77
68
|
if (!first?.callbackSessionId || (first.callbackState !== "pending" && first.callbackState !== "failed"))
|
|
78
69
|
return;
|
|
79
70
|
const sessionId = first.callbackSessionId;
|
|
80
|
-
//
|
|
81
|
-
// deliverable, everything pending for the session rides along.
|
|
71
|
+
// Once one callback is deliverable, everything pending for the session rides along.
|
|
82
72
|
const batch = this.store.listPendingCallbacks(Number.MAX_SAFE_INTEGER)
|
|
83
73
|
.filter((run) => run.callbackSessionId === sessionId);
|
|
84
74
|
if (!batch.some((run) => run.id === first.id))
|
package/dist/tasks/command.js
CHANGED
|
@@ -5,6 +5,7 @@ import { spawn } from "node:child_process";
|
|
|
5
5
|
import { logger } from "../log.js";
|
|
6
6
|
const log = logger("tasks");
|
|
7
7
|
const OUTPUT_LIMIT = 1024 * 1024;
|
|
8
|
+
const KILL_GRACE_MS = 250;
|
|
8
9
|
class CappedOutput {
|
|
9
10
|
chunks = [];
|
|
10
11
|
bytes = 0;
|
|
@@ -38,34 +39,52 @@ export function runBash(script, cwd, input, signal) {
|
|
|
38
39
|
const stdout = new CappedOutput();
|
|
39
40
|
const stderr = new CappedOutput();
|
|
40
41
|
let settled = false;
|
|
41
|
-
|
|
42
|
+
let killTimer;
|
|
43
|
+
const kill = (signal) => {
|
|
42
44
|
if (!child.pid)
|
|
43
45
|
return;
|
|
44
46
|
try {
|
|
45
47
|
if (process.platform === "win32")
|
|
46
|
-
child.kill(
|
|
48
|
+
child.kill(signal);
|
|
47
49
|
else
|
|
48
|
-
process.kill(-child.pid,
|
|
50
|
+
process.kill(-child.pid, signal);
|
|
49
51
|
}
|
|
50
|
-
catch {
|
|
51
|
-
//
|
|
52
|
+
catch (err) {
|
|
53
|
+
// ESRCH means the owned process group already exited.
|
|
54
|
+
if (err.code !== "ESRCH")
|
|
55
|
+
log.warn(`run ${signal} failed: ${String(err)}`);
|
|
52
56
|
}
|
|
53
57
|
};
|
|
54
|
-
|
|
58
|
+
const abort = () => {
|
|
59
|
+
kill("SIGTERM");
|
|
60
|
+
killTimer = setTimeout(() => kill("SIGKILL"), KILL_GRACE_MS);
|
|
61
|
+
killTimer.unref();
|
|
62
|
+
};
|
|
63
|
+
const cleanup = () => {
|
|
64
|
+
clearTimeout(killTimer);
|
|
65
|
+
signal.removeEventListener("abort", abort);
|
|
66
|
+
};
|
|
67
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
68
|
+
if (signal.aborted)
|
|
69
|
+
abort();
|
|
55
70
|
child.stdout.on("data", (chunk) => stdout.add(chunk));
|
|
56
71
|
child.stderr.on("data", (chunk) => stderr.add(chunk));
|
|
57
72
|
child.on("error", (err) => {
|
|
58
73
|
if (settled)
|
|
59
74
|
return;
|
|
60
75
|
settled = true;
|
|
61
|
-
|
|
76
|
+
cleanup();
|
|
62
77
|
reject(err);
|
|
63
78
|
});
|
|
64
79
|
child.on("close", (code) => {
|
|
65
80
|
if (settled)
|
|
66
81
|
return;
|
|
67
82
|
settled = true;
|
|
68
|
-
|
|
83
|
+
// A TERM trap may exit the shell while a descendant ignores TERM and
|
|
84
|
+
// has redirected its pipes. Closing the shell must not spare that group.
|
|
85
|
+
if (signal.aborted)
|
|
86
|
+
kill("SIGKILL");
|
|
87
|
+
cleanup();
|
|
69
88
|
resolve({
|
|
70
89
|
exitCode: code,
|
|
71
90
|
stdout: stdout.text(),
|
|
@@ -74,12 +93,8 @@ export function runBash(script, cwd, input, signal) {
|
|
|
74
93
|
stderrTruncated: stderr.truncated,
|
|
75
94
|
});
|
|
76
95
|
});
|
|
77
|
-
// A script that never reads stdin
|
|
78
|
-
//
|
|
79
|
-
// Unhandled, that is an `error` event on a stream, which is an uncaught
|
|
80
|
-
// exception, which is main.ts exiting the process: one task script could
|
|
81
|
-
// take every session and every other run down with it. The input not being
|
|
82
|
-
// wanted is not a failure of the run — anything else still gets said.
|
|
96
|
+
// A script that never reads stdin raises EPIPE here; unhandled, that is an
|
|
97
|
+
// uncaught exception that exits the process. Not wanting the input is not a failure.
|
|
83
98
|
child.stdin.on("error", (err) => {
|
|
84
99
|
if (err.code !== "EPIPE")
|
|
85
100
|
log.warn(`run input could not be written: ${err.message}`);
|