pi-roundtable 0.21.0 → 0.22.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/CHANGELOG.md +14 -0
- package/docs/plugins.md +54 -5
- package/package.json +1 -1
- package/src/core/agents/team-turns.ts +2 -7
- package/src/core/builtin/skills.ts +11 -0
- package/src/core/builtin/worker-sessions.ts +44 -0
- package/src/core/config/config.ts +4 -6
- package/src/core/config/skills-config.ts +23 -0
- package/src/core/define-roundtable.ts +8 -0
- package/src/core/discord/owner-card-rows.ts +7 -3
- package/src/core/discord/owner-card-types.ts +4 -0
- package/src/core/discord/owner-cards.ts +10 -3
- package/src/core/domain/turn-timeout.ts +38 -0
- package/src/core/i18n/en.ts +2 -0
- package/src/core/i18n/turn-timeout.ts +131 -0
- package/src/core/i18n/zh-tw.ts +2 -0
- package/src/core/interactions/prompts.ts +17 -0
- package/src/core/modules/delegation/web-research-worker.ts +10 -38
- package/src/core/modules/delegation/worker-session.ts +111 -0
- package/src/core/modules/skills/package-skills-fixture.ts +33 -0
- package/src/core/modules/skills/package-skills.ts +121 -0
- package/src/core/modules/skills/skill-listing.ts +11 -0
- package/src/core/modules/skills/skill-registry.ts +47 -3
- package/src/core/modules/skills/skill-rules.ts +13 -1
- package/src/core/presentation/failure-notice.ts +13 -0
- package/src/core/routing/conversation-turns.ts +2 -7
- package/src/core/runtime/pi-agent-runtime.ts +10 -10
- package/src/core/runtime/prompt-slot.ts +8 -0
- package/src/core/runtime/runtime-types.ts +11 -3
- package/src/core/runtime/turn-activity.ts +42 -0
- package/src/core/runtime/turn-budget.ts +163 -19
- package/src/core/runtime/turn-limits.ts +61 -0
- package/src/core/services.ts +44 -1
- package/src/index.ts +11 -0
|
@@ -28,7 +28,8 @@ export interface SkillSet {
|
|
|
28
28
|
export type SkillSource =
|
|
29
29
|
| { kind: "builtin" }
|
|
30
30
|
| { kind: "linked"; repo: string; path: string }
|
|
31
|
-
| { kind: "written" }
|
|
31
|
+
| { kind: "written" }
|
|
32
|
+
| { kind: "package"; package: string };
|
|
32
33
|
|
|
33
34
|
/** One skill as the web app lists it; a skill whose file cannot be read has `missing` instead. */
|
|
34
35
|
export interface SkillCatalogEntry {
|
|
@@ -43,6 +44,15 @@ export interface SkillCatalogEntry {
|
|
|
43
44
|
carriers: string[];
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/** A skill an installed npm package offers through its `pi.skills` folders. */
|
|
48
|
+
export interface PackageSkill {
|
|
49
|
+
name: string;
|
|
50
|
+
/** The npm package that holds it. */
|
|
51
|
+
package: string;
|
|
52
|
+
/** Its SKILL.md in the installed package. */
|
|
53
|
+
file: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
export interface SkillRegistryOptions {
|
|
47
57
|
store: SkillStore;
|
|
48
58
|
/** Managed repositories, as `<dir>/<owner>/<repo>`. */
|
|
@@ -51,6 +61,8 @@ export interface SkillRegistryOptions {
|
|
|
51
61
|
writtenDir: string;
|
|
52
62
|
/** The release's built-in skills, as `<dir>/<name>/SKILL.md`. */
|
|
53
63
|
builtinDir: string;
|
|
64
|
+
/** Skills of installed packages, as `discoverPackageSkills` found them at startup. */
|
|
65
|
+
packageSkills?: readonly PackageSkill[];
|
|
54
66
|
logger: Logger;
|
|
55
67
|
}
|
|
56
68
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TurnTimeoutError } from "../domain/turn-timeout.ts";
|
|
2
|
+
import { messages } from "../i18n/index.ts";
|
|
3
|
+
|
|
4
|
+
/** What a failed turn posts: the stopped notice for a stop, the reason for a time limit, else the failure notice. */
|
|
5
|
+
export function failureNotice(result: {
|
|
6
|
+
error: Error;
|
|
7
|
+
stopped?: true;
|
|
8
|
+
}): string {
|
|
9
|
+
if (result.stopped) return messages().stoppedNotice;
|
|
10
|
+
if (result.error instanceof TurnTimeoutError)
|
|
11
|
+
return messages().turnTimedOutNotice(result.error.timeout);
|
|
12
|
+
return messages().failureNotice;
|
|
13
|
+
}
|
|
@@ -10,10 +10,10 @@ import type { TurnResult } from "../domain/conversation.ts";
|
|
|
10
10
|
import type { TurnConversation } from "../domain/ports.ts";
|
|
11
11
|
import type { TurnProgress } from "../domain/progress.ts";
|
|
12
12
|
import { HostStoppingError, PluginError } from "../errors.ts";
|
|
13
|
-
import { messages } from "../i18n/index.ts";
|
|
14
13
|
import { SYSTEM_PRINCIPAL } from "../identity/principal-store.ts";
|
|
15
14
|
import type { Logger } from "../log.ts";
|
|
16
15
|
import type { EventSink } from "../plugin.ts";
|
|
16
|
+
import { failureNotice } from "../presentation/failure-notice.ts";
|
|
17
17
|
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
18
18
|
import { thinkingLine } from "../presentation/thinking-line.ts";
|
|
19
19
|
import { withReplyFiles } from "../reply-files.ts";
|
|
@@ -239,12 +239,7 @@ export function conversationTurns(
|
|
|
239
239
|
|
|
240
240
|
/** The default post: the answer with its thinking line, or the failure or stopped notice. */
|
|
241
241
|
function replyOf(result: TurnResult) {
|
|
242
|
-
if (!result.ok)
|
|
243
|
-
return {
|
|
244
|
-
chunks: [
|
|
245
|
-
result.stopped ? messages().stoppedNotice : messages().failureNotice,
|
|
246
|
-
],
|
|
247
|
-
};
|
|
242
|
+
if (!result.ok) return { chunks: [failureNotice(result)] };
|
|
248
243
|
const thinking = result.thinking ? thinkingLine(result.thinking) : undefined;
|
|
249
244
|
return {
|
|
250
245
|
...(thinking ? { thinking } : {}),
|
|
@@ -48,7 +48,8 @@ import { promptImages, SteerableRun } from "./steerable-run.ts";
|
|
|
48
48
|
import { thinkingLevel } from "./thinking-level.ts";
|
|
49
49
|
import { runningCalls } from "./tool-call-scope.ts";
|
|
50
50
|
import { transcriptOf, turnAnswer, unspokenTurn } from "./turn-answer.ts";
|
|
51
|
-
import {
|
|
51
|
+
import type { TurnBudget } from "./turn-budget.ts";
|
|
52
|
+
import { promptFailed, turnLimits } from "./turn-limits.ts";
|
|
52
53
|
import { progressReporter } from "./turn-progress.ts";
|
|
53
54
|
import { wrapUpTurn } from "./turn-wrapup.ts";
|
|
54
55
|
import { workerReport } from "./worker-task.ts";
|
|
@@ -240,6 +241,10 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
240
241
|
const interim = interimPoster(request, this.#options);
|
|
241
242
|
// The caller's live view: the text as it streams and each tool, never the thinking.
|
|
242
243
|
const slot = this.#sessions.slot(key);
|
|
244
|
+
// Its time limit, not counting time waiting on the owner's cards, follows what it does to say why it fired.
|
|
245
|
+
const limits = turnLimits(this.#options, slot, request.channel, () =>
|
|
246
|
+
guard.abort(),
|
|
247
|
+
);
|
|
243
248
|
const progress = request.progress
|
|
244
249
|
? progressReporter(request.progress, {
|
|
245
250
|
refusalOf: (id) => slot.refusalOf(id),
|
|
@@ -247,6 +252,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
247
252
|
: undefined;
|
|
248
253
|
const unsubscribe = session.subscribe((event) => {
|
|
249
254
|
progress?.observe(event);
|
|
255
|
+
limits.observe(event);
|
|
250
256
|
if (event.type === "tool_execution_start") {
|
|
251
257
|
toolCalls.push(event.toolName);
|
|
252
258
|
interim?.toolStart(event.toolName);
|
|
@@ -275,10 +281,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
275
281
|
this.#sessions.drop(key, channelSession);
|
|
276
282
|
},
|
|
277
283
|
});
|
|
278
|
-
|
|
279
|
-
const budget = runtimeTurnBudget(this.#options, slot, request.channel, () =>
|
|
280
|
-
guard.abort(),
|
|
281
|
-
);
|
|
284
|
+
const { budget } = limits;
|
|
282
285
|
this.#budgets.set(key, budget);
|
|
283
286
|
const running = new SteerableRun(
|
|
284
287
|
session,
|
|
@@ -307,10 +310,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
307
310
|
} catch (error) {
|
|
308
311
|
if (running.stopped)
|
|
309
312
|
return { ok: false, error: new AgentRunError(STOPPED), stopped: true };
|
|
310
|
-
return
|
|
311
|
-
ok: false,
|
|
312
|
-
error: new AgentRunError(`prompt failed: ${String(error)}`),
|
|
313
|
-
};
|
|
313
|
+
return limits.failure() ?? promptFailed(error);
|
|
314
314
|
} finally {
|
|
315
315
|
this.#running.delete(key);
|
|
316
316
|
this.#budgets.delete(key);
|
|
@@ -335,7 +335,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
335
335
|
|
|
336
336
|
if (running.stopped)
|
|
337
337
|
return { ok: false, error: new AgentRunError(STOPPED), stopped: true };
|
|
338
|
-
return turnAnswer(turnMessages, running.steered);
|
|
338
|
+
return limits.failure() ?? turnAnswer(turnMessages, running.steered);
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
async steer(
|
|
@@ -99,6 +99,14 @@ export class PromptSlot {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* The turn's prompts as bound, an open card not counted as waiting; for a card the turn keeps
|
|
104
|
+
* working beside, such as the one its time limit posts.
|
|
105
|
+
*/
|
|
106
|
+
get cards(): Prompts | undefined {
|
|
107
|
+
return this.#prompts;
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
/** Time this turn has spent with at least one card open. */
|
|
103
111
|
waitedMs(now = Date.now()): number {
|
|
104
112
|
return this.#waited + (this.#open > 0 ? now - this.#openSince : 0);
|
|
@@ -89,17 +89,25 @@ export interface PiAgentRuntimeOptions {
|
|
|
89
89
|
/** The lowest tier that may use each tool; the default names the core's tools and leaves the rest to the owner. */
|
|
90
90
|
toolTiers?: ToolTiers;
|
|
91
91
|
/**
|
|
92
|
-
* A run that works longer, not counting time spent waiting on the owner's cards,
|
|
93
|
-
*
|
|
92
|
+
* A run that works longer, not counting time spent waiting on the owner's cards, asks the owner
|
|
93
|
+
* on a card whether to go on (see `turnTimeoutCardMs`), or, when it cannot show one, is aborted
|
|
94
|
+
* and reported as failed with the reason; default 10 minutes. Each message steered into the turn gives it this
|
|
94
95
|
* budget again from that moment, up to `turnMaxMs`.
|
|
95
96
|
*/
|
|
96
97
|
turnTimeoutMs?: number;
|
|
97
98
|
/**
|
|
98
99
|
* The most one turn may work in all, however often it is steered, not counting time spent
|
|
99
100
|
* waiting on the owner's cards; default three times `turnTimeoutMs`. Once it is reached the turn
|
|
100
|
-
* takes no more steering (the message waits for the next turn) and is aborted as on its
|
|
101
|
+
* takes no more steering (the message waits for the next turn) and asks or is aborted as on its
|
|
102
|
+
* timeout; the owner's Continue moves the cap to a full `turnTimeoutMs` from then.
|
|
101
103
|
*/
|
|
102
104
|
turnMaxMs?: number;
|
|
105
|
+
/**
|
|
106
|
+
* How long the card a turn posts when it reaches `turnTimeoutMs` or `turnMaxMs` waits for the
|
|
107
|
+
* owner's Continue or Cancel, in wall time, while the turn keeps working; default 5 minutes. With
|
|
108
|
+
* no answer by then the turn is aborted. A turn that cannot show a card is aborted at once.
|
|
109
|
+
*/
|
|
110
|
+
turnTimeoutCardMs?: number;
|
|
103
111
|
/**
|
|
104
112
|
* How long a turn that was aborted, by its timeout or a stop, gets to end before its session is
|
|
105
113
|
* disposed of and the turn is reported failed; default 15 seconds. A model that ignores the
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
/** What a turn was doing at one moment: tools running and for how long, or compacting, or neither (the model). */
|
|
4
|
+
export interface TurnActivity {
|
|
5
|
+
/** The tools running, the longest-running first, each with how long it has run. */
|
|
6
|
+
running: { tool: string; ms: number }[];
|
|
7
|
+
compacting: boolean;
|
|
8
|
+
/** Every tool called so far, by name, with how often, in the order first called. */
|
|
9
|
+
tools: [string, number][];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Follows a turn's events to say, when asked, what it is doing; never its inputs. */
|
|
13
|
+
export interface ActivityTracker {
|
|
14
|
+
observe(event: AgentSessionEvent): void;
|
|
15
|
+
now(at: number): TurnActivity;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function activityTracker(
|
|
19
|
+
clock: () => number = Date.now,
|
|
20
|
+
): ActivityTracker {
|
|
21
|
+
const running = new Map<string, { tool: string; since: number }>();
|
|
22
|
+
const tools = new Map<string, number>();
|
|
23
|
+
let compacting = false;
|
|
24
|
+
return {
|
|
25
|
+
observe(event) {
|
|
26
|
+
if (event.type === "tool_execution_start") {
|
|
27
|
+
running.set(event.toolCallId, { tool: event.toolName, since: clock() });
|
|
28
|
+
tools.set(event.toolName, (tools.get(event.toolName) ?? 0) + 1);
|
|
29
|
+
} else if (event.type === "tool_execution_end") {
|
|
30
|
+
running.delete(event.toolCallId);
|
|
31
|
+
} else if (event.type === "compaction_start") compacting = true;
|
|
32
|
+
else if (event.type === "compaction_end") compacting = false;
|
|
33
|
+
},
|
|
34
|
+
now: (at) => ({
|
|
35
|
+
running: [...running.values()]
|
|
36
|
+
.sort((a, b) => a.since - b.since)
|
|
37
|
+
.map(({ tool, since }) => ({ tool, ms: at - since })),
|
|
38
|
+
compacting,
|
|
39
|
+
tools: [...tools],
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TurnLimit,
|
|
3
|
+
TurnTimeout,
|
|
4
|
+
TurnTimeoutEnd,
|
|
5
|
+
} from "../domain/turn-timeout.ts";
|
|
6
|
+
import { messages } from "../i18n/index.ts";
|
|
7
|
+
import type { Approval } from "../interactions/prompts.ts";
|
|
1
8
|
import type { ChannelKey } from "../sessions.ts";
|
|
2
9
|
import type { PromptSlot } from "./prompt-slot.ts";
|
|
3
10
|
import type { PiAgentRuntimeOptions } from "./runtime-types.ts";
|
|
11
|
+
import type { ActivityTracker } from "./turn-activity.ts";
|
|
4
12
|
|
|
5
|
-
|
|
6
|
-
export type TurnLimit = "turn" | "turnMax";
|
|
13
|
+
export type { TurnLimit };
|
|
7
14
|
|
|
8
15
|
/** A turn's time limit, which a steered message renews up to a cap on the whole turn. */
|
|
9
16
|
export interface TurnBudget {
|
|
@@ -12,8 +19,15 @@ export interface TurnBudget {
|
|
|
12
19
|
* or undefined when the cap is reached and the turn takes no more steering.
|
|
13
20
|
*/
|
|
14
21
|
renew(): number | undefined;
|
|
15
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* After a limit fired, gives the turn its full budget again from now, as the owner's Continue
|
|
24
|
+
* does; with `raiseCap` the cap moves too, to a full budget from now.
|
|
25
|
+
*/
|
|
26
|
+
extend(raiseCap: boolean): void;
|
|
27
|
+
/** Whether the turn has worked up to its cap, or a limit fired and was not extended. */
|
|
16
28
|
capped(): boolean;
|
|
29
|
+
/** The turn's work so far, not counting time the slot spent waiting on cards. */
|
|
30
|
+
workedMs(): number;
|
|
17
31
|
cancel(): void;
|
|
18
32
|
}
|
|
19
33
|
|
|
@@ -43,13 +57,13 @@ const REARM_FLOOR_MS = 1_000;
|
|
|
43
57
|
/**
|
|
44
58
|
* Calls `expire` once the turn has worked `ms` since it started or since its last renewal, or
|
|
45
59
|
* `maxMs` in all, whichever comes first; time the slot spent waiting on the owner's cards counts
|
|
46
|
-
* towards neither. `expire` is told which limit fired.
|
|
60
|
+
* towards neither. `expire` is told which limit fired and its length.
|
|
47
61
|
*/
|
|
48
62
|
export function turnBudget(
|
|
49
63
|
ms: number,
|
|
50
64
|
maxMs: number,
|
|
51
65
|
slot: Pick<PromptSlot, "waitedMs">,
|
|
52
|
-
expire: (limit: TurnLimit) => void,
|
|
66
|
+
expire: (limit: TurnLimit, limitMs: number) => void,
|
|
53
67
|
clock: BudgetClock = REAL_CLOCK,
|
|
54
68
|
): TurnBudget {
|
|
55
69
|
const started = clock.now();
|
|
@@ -57,7 +71,8 @@ export function turnBudget(
|
|
|
57
71
|
const now = clock.now();
|
|
58
72
|
return now - started - slot.waitedMs(now);
|
|
59
73
|
};
|
|
60
|
-
let
|
|
74
|
+
let cap = maxMs;
|
|
75
|
+
let deadline = Math.min(ms, cap);
|
|
61
76
|
let cancelTimer: (() => void) | undefined;
|
|
62
77
|
let expired = false;
|
|
63
78
|
const arm = (delay: number) => {
|
|
@@ -65,47 +80,172 @@ export function turnBudget(
|
|
|
65
80
|
const left = deadline - worked();
|
|
66
81
|
if (left > 0) return arm(Math.max(left, REARM_FLOOR_MS));
|
|
67
82
|
expired = true;
|
|
68
|
-
|
|
83
|
+
if (deadline >= cap) expire("turnMax", cap);
|
|
84
|
+
else expire("turn", ms);
|
|
69
85
|
}, delay);
|
|
70
86
|
};
|
|
71
87
|
arm(deadline);
|
|
72
88
|
return {
|
|
73
89
|
renew() {
|
|
74
90
|
const now = worked();
|
|
75
|
-
if (expired || now >=
|
|
76
|
-
deadline = Math.min(now + ms,
|
|
91
|
+
if (expired || now >= cap) return undefined;
|
|
92
|
+
deadline = Math.min(now + ms, cap);
|
|
77
93
|
cancelTimer?.();
|
|
78
94
|
arm(deadline - now);
|
|
79
95
|
return deadline - now;
|
|
80
96
|
},
|
|
81
|
-
|
|
97
|
+
extend(raiseCap) {
|
|
98
|
+
const now = worked();
|
|
99
|
+
if (raiseCap) cap = Math.max(cap, now + ms);
|
|
100
|
+
deadline = Math.min(now + ms, cap);
|
|
101
|
+
expired = false;
|
|
102
|
+
cancelTimer?.();
|
|
103
|
+
arm(Math.max(deadline - now, REARM_FLOOR_MS));
|
|
104
|
+
},
|
|
105
|
+
capped: () => expired || worked() >= cap,
|
|
106
|
+
workedMs: worked,
|
|
82
107
|
cancel: () => cancelTimer?.(),
|
|
83
108
|
};
|
|
84
109
|
}
|
|
85
110
|
|
|
111
|
+
/** What the runtime's turn budget needs besides its options: what the turn is doing, and its clock. */
|
|
112
|
+
export interface TurnBudgetSeams {
|
|
113
|
+
activity: Pick<ActivityTracker, "now">;
|
|
114
|
+
clock?: BudgetClock;
|
|
115
|
+
}
|
|
116
|
+
|
|
86
117
|
/**
|
|
87
118
|
* The time limit of one turn of the built-in runtime, from its options: `turnTimeoutMs` (default
|
|
88
|
-
* 10 minutes) renewed by each steer up to `turnMaxMs` (default three times that).
|
|
89
|
-
*
|
|
119
|
+
* 10 minutes) renewed by each steer up to `turnMaxMs` (default three times that). When a limit
|
|
120
|
+
* fires on a turn that can show the owner a card, the turn keeps working and a card asks whether
|
|
121
|
+
* to go on: Continue extends the limit that fired by a full budget from then, Cancel aborts, and
|
|
122
|
+
* no answer within `turnTimeoutCardMs` (default 5 minutes, wall time) aborts. The card closes
|
|
123
|
+
* when the turn ends first. Without a card the turn is aborted at once. `abort` is told why.
|
|
90
124
|
*/
|
|
91
125
|
export function runtimeTurnBudget(
|
|
92
126
|
options: Pick<
|
|
93
127
|
PiAgentRuntimeOptions,
|
|
94
|
-
"turnTimeoutMs" | "turnMaxMs" | "logger"
|
|
128
|
+
"turnTimeoutMs" | "turnMaxMs" | "turnTimeoutCardMs" | "logger"
|
|
95
129
|
>,
|
|
96
|
-
slot: Pick<PromptSlot, "waitedMs">,
|
|
130
|
+
slot: Pick<PromptSlot, "waitedMs" | "cards">,
|
|
97
131
|
channel: ChannelKey,
|
|
98
|
-
abort: () => void,
|
|
132
|
+
abort: (timeout: TurnTimeout) => void,
|
|
133
|
+
seams: TurnBudgetSeams,
|
|
99
134
|
): TurnBudget {
|
|
100
135
|
const { logger, turnTimeoutMs = 10 * 60_000 } = options;
|
|
101
136
|
const turnMaxMs = options.turnMaxMs ?? 3 * turnTimeoutMs;
|
|
102
|
-
const
|
|
137
|
+
const graceMs = options.turnTimeoutCardMs ?? 5 * 60_000;
|
|
138
|
+
const clock = seams.clock ?? REAL_CLOCK;
|
|
139
|
+
/** Closes the open timeout card, if any, with the footer given. */
|
|
140
|
+
let closeCard: ((footer: string) => void) | undefined;
|
|
141
|
+
const reason = (
|
|
142
|
+
limit: TurnLimit,
|
|
143
|
+
limitMs: number,
|
|
144
|
+
end: TurnTimeoutEnd,
|
|
145
|
+
): TurnTimeout => ({
|
|
146
|
+
limit,
|
|
147
|
+
limitMs,
|
|
148
|
+
workedMs: budget.workedMs(),
|
|
149
|
+
...seams.activity.now(clock.now()),
|
|
150
|
+
graceMs,
|
|
151
|
+
end,
|
|
152
|
+
});
|
|
153
|
+
const stop = (timeout: TurnTimeout) => {
|
|
103
154
|
logger.warn(
|
|
104
|
-
{
|
|
155
|
+
{
|
|
156
|
+
channel,
|
|
157
|
+
limit: timeout.limit,
|
|
158
|
+
turnTimeoutMs,
|
|
159
|
+
turnMaxMs,
|
|
160
|
+
workedMs: timeout.workedMs,
|
|
161
|
+
running: timeout.running,
|
|
162
|
+
compacting: timeout.compacting,
|
|
163
|
+
tools: timeout.tools,
|
|
164
|
+
end: timeout.end,
|
|
165
|
+
},
|
|
105
166
|
"turn timed out; aborting",
|
|
106
167
|
);
|
|
107
|
-
abort();
|
|
108
|
-
}
|
|
168
|
+
abort(timeout);
|
|
169
|
+
};
|
|
170
|
+
const budget = turnBudget(
|
|
171
|
+
turnTimeoutMs,
|
|
172
|
+
turnMaxMs,
|
|
173
|
+
slot,
|
|
174
|
+
(limit, limitMs) => {
|
|
175
|
+
const prompts = slot.cards;
|
|
176
|
+
if (!prompts) return stop(reason(limit, limitMs, "timeout"));
|
|
177
|
+
const m = messages();
|
|
178
|
+
const asked = reason(limit, limitMs, "timeout");
|
|
179
|
+
logger.info(
|
|
180
|
+
{
|
|
181
|
+
channel,
|
|
182
|
+
limit,
|
|
183
|
+
workedMs: asked.workedMs,
|
|
184
|
+
running: asked.running,
|
|
185
|
+
},
|
|
186
|
+
"turn reached its time limit; asking the owner",
|
|
187
|
+
);
|
|
188
|
+
const controller = new AbortController();
|
|
189
|
+
let open = true;
|
|
190
|
+
const settle = () => {
|
|
191
|
+
if (!open) return false;
|
|
192
|
+
open = false;
|
|
193
|
+
closeCard = undefined;
|
|
194
|
+
cancelGrace();
|
|
195
|
+
return true;
|
|
196
|
+
};
|
|
197
|
+
const cancelGrace = clock.schedule(() => {
|
|
198
|
+
if (!settle()) return;
|
|
199
|
+
const timeout = reason(limit, limitMs, "unanswered");
|
|
200
|
+
controller.abort(m.turnTimeoutUnanswered(timeout));
|
|
201
|
+
stop(timeout);
|
|
202
|
+
}, graceMs);
|
|
203
|
+
closeCard = (footer) => {
|
|
204
|
+
if (settle()) controller.abort(footer);
|
|
205
|
+
};
|
|
206
|
+
const answered = (approval: Approval) => {
|
|
207
|
+
if (!settle()) return;
|
|
208
|
+
if (approval === "approved") {
|
|
209
|
+
budget.extend(limit === "turnMax");
|
|
210
|
+
logger.info(
|
|
211
|
+
{ channel, limit, workedMs: budget.workedMs() },
|
|
212
|
+
"owner continued the turn past its time limit",
|
|
213
|
+
);
|
|
214
|
+
} else
|
|
215
|
+
stop(
|
|
216
|
+
reason(
|
|
217
|
+
limit,
|
|
218
|
+
limitMs,
|
|
219
|
+
approval === "declined" ? "cancelled" : "timeout",
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
prompts
|
|
224
|
+
.confirm(
|
|
225
|
+
m.turnTimeoutTitle,
|
|
226
|
+
m.turnTimeoutQuestion(asked, turnTimeoutMs),
|
|
227
|
+
undefined,
|
|
228
|
+
"owner",
|
|
229
|
+
undefined,
|
|
230
|
+
undefined,
|
|
231
|
+
{
|
|
232
|
+
supersede: controller.signal,
|
|
233
|
+
wording: {
|
|
234
|
+
approve: m.turnTimeoutContinue,
|
|
235
|
+
decline: m.turnTimeoutCancel,
|
|
236
|
+
footer: m.turnTimeoutFooter,
|
|
237
|
+
approved: m.turnTimeoutContinued,
|
|
238
|
+
declined: m.turnTimeoutCancelled,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
)
|
|
242
|
+
.then(answered, (error: unknown) => {
|
|
243
|
+
logger.warn({ channel, err: error }, "timeout card failed");
|
|
244
|
+
answered("unavailable");
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
clock,
|
|
248
|
+
);
|
|
109
249
|
return {
|
|
110
250
|
...budget,
|
|
111
251
|
renew() {
|
|
@@ -117,5 +257,9 @@ export function runtimeTurnBudget(
|
|
|
117
257
|
);
|
|
118
258
|
return deadlineMs;
|
|
119
259
|
},
|
|
260
|
+
cancel() {
|
|
261
|
+
budget.cancel();
|
|
262
|
+
closeCard?.(messages().turnTimeoutFinished);
|
|
263
|
+
},
|
|
120
264
|
};
|
|
121
265
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { TurnResult } from "../domain/conversation.ts";
|
|
3
|
+
import { AgentRunError } from "../domain/errors.ts";
|
|
4
|
+
import { type TurnTimeout, TurnTimeoutError } from "../domain/turn-timeout.ts";
|
|
5
|
+
import { catalogFor } from "../i18n/index.ts";
|
|
6
|
+
import type { ChannelKey } from "../sessions.ts";
|
|
7
|
+
import type { PromptSlot } from "./prompt-slot.ts";
|
|
8
|
+
import { activityTracker } from "./turn-activity.ts";
|
|
9
|
+
import { runtimeTurnBudget, type TurnBudget } from "./turn-budget.ts";
|
|
10
|
+
|
|
11
|
+
/** One turn's time limit, with what it follows of the turn to say why it fired. */
|
|
12
|
+
export interface TurnLimits {
|
|
13
|
+
budget: TurnBudget;
|
|
14
|
+
/** Follows the turn's events: its tools and its compactions. */
|
|
15
|
+
observe(event: AgentSessionEvent): void;
|
|
16
|
+
/** The turn's failure when a limit aborted it, its reason as a `TurnTimeoutError`; else undefined. */
|
|
17
|
+
failure(): TurnResult | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The answer to a turn whose prompt threw, when no limit aborted it. */
|
|
21
|
+
export function promptFailed(error: unknown): TurnResult {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
error: new AgentRunError(`prompt failed: ${String(error)}`),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A timed-out turn's reason in English, for its error and the logs. */
|
|
29
|
+
export function timeoutMessage(timeout: TurnTimeout): string {
|
|
30
|
+
return catalogFor("en").turnTimedOutNotice(timeout);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The time limit of a turn of the built-in runtime; see `runtimeTurnBudget`. `abort` aborts the turn. */
|
|
34
|
+
export function turnLimits(
|
|
35
|
+
options: Parameters<typeof runtimeTurnBudget>[0],
|
|
36
|
+
slot: Pick<PromptSlot, "waitedMs" | "cards">,
|
|
37
|
+
channel: ChannelKey,
|
|
38
|
+
abort: () => void,
|
|
39
|
+
): TurnLimits {
|
|
40
|
+
const activity = activityTracker();
|
|
41
|
+
let timedOut: TurnTimeout | undefined;
|
|
42
|
+
const budget = runtimeTurnBudget(
|
|
43
|
+
options,
|
|
44
|
+
slot,
|
|
45
|
+
channel,
|
|
46
|
+
(timeout) => {
|
|
47
|
+
timedOut = timeout;
|
|
48
|
+
abort();
|
|
49
|
+
},
|
|
50
|
+
{ activity },
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
budget,
|
|
54
|
+
observe: (event) => activity.observe(event),
|
|
55
|
+
failure: () =>
|
|
56
|
+
timedOut && {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: new TurnTimeoutError(timedOut, timeoutMessage(timedOut)),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
package/src/core/services.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { Agent, AgentGroup } from "./agents/agent-rules.ts";
|
|
2
3
|
import type { AvatarMode } from "./agents/agent-tools.ts";
|
|
3
4
|
import type { TeamStatus } from "./agents/team-status.ts";
|
|
@@ -12,7 +13,7 @@ import type { PendingConfirmation } from "./domain/conversation.ts";
|
|
|
12
13
|
import type { HttpRoute } from "./http/listeners.ts";
|
|
13
14
|
import type { IdentityLinks } from "./identity/identity-links.ts";
|
|
14
15
|
import type { IdentityService } from "./identity/identity-service.ts";
|
|
15
|
-
import type { ThinkingSetting } from "./models.ts";
|
|
16
|
+
import type { ThinkingLevel, ThinkingSetting } from "./models.ts";
|
|
16
17
|
import type {
|
|
17
18
|
DelegationJob,
|
|
18
19
|
DelegationOutcome,
|
|
@@ -91,6 +92,12 @@ export const BACKGROUND_TURNS: ServiceKey<BackgroundTurns> =
|
|
|
91
92
|
export const DELEGATION: ServiceKey<Delegator> = serviceKey<Delegator>(
|
|
92
93
|
"roundtable.delegation",
|
|
93
94
|
);
|
|
95
|
+
/**
|
|
96
|
+
* Worker sessions on the host's models: a fresh, unsaved Pi session that has only the tools a
|
|
97
|
+
* plugin gives it. Provided by the `worker-sessions` plugin.
|
|
98
|
+
*/
|
|
99
|
+
export const WORKER_SESSIONS: ServiceKey<WorkerSessions> =
|
|
100
|
+
serviceKey<WorkerSessions>("roundtable.worker-sessions");
|
|
94
101
|
/**
|
|
95
102
|
* The remembered facts of each speaker. Provided by the `memory` plugin, an addon: read it with
|
|
96
103
|
* `find` when your plugin works without memory, and with `get` when it cannot.
|
|
@@ -267,6 +274,42 @@ export interface Delegator {
|
|
|
267
274
|
idle(): Promise<void>;
|
|
268
275
|
}
|
|
269
276
|
|
|
277
|
+
/** One task for a worker session. */
|
|
278
|
+
export interface WorkerSessionRequest {
|
|
279
|
+
/** What the worker is asked to do, as its one prompt. */
|
|
280
|
+
task: string;
|
|
281
|
+
/** Aborting it stops the worker; `run` then rejects. */
|
|
282
|
+
signal: AbortSignal;
|
|
283
|
+
/** Appended to the worker's system prompt: its role, its rules, and any instructions it should follow. */
|
|
284
|
+
prompt: string;
|
|
285
|
+
/** Extensions that register the worker's tools with `pi.registerTool`. */
|
|
286
|
+
extensions: readonly ExtensionFactory[];
|
|
287
|
+
/** The only tools active in the session. The session has no built-in tool, skill, context file or other extension. */
|
|
288
|
+
toolNames: readonly string[];
|
|
289
|
+
/** `<provider>/<id>`; default: the host's delegation model. */
|
|
290
|
+
model?: string;
|
|
291
|
+
/** Default: the host's delegation thinking level. */
|
|
292
|
+
thinking?: ThinkingLevel;
|
|
293
|
+
/** Why an aborted run stopped, as the rejection's message; default "the worker ran out of time". */
|
|
294
|
+
aborted?: string;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Runs one task in a fresh, unsaved Pi session on the host's model login and returns the worker's
|
|
299
|
+
* final answer. The session is disposed of either way. It has no access to the conversation, the
|
|
300
|
+
* owner's memory or the agents' tools: it can do only what `toolNames` lets it, and the tools a
|
|
301
|
+
* plugin registers for it are that plugin's to bound.
|
|
302
|
+
*/
|
|
303
|
+
export interface WorkerSessions {
|
|
304
|
+
/** The model and thinking level a request without its own uses: the host's `delegation` settings. */
|
|
305
|
+
readonly delegation: {
|
|
306
|
+
readonly model: string;
|
|
307
|
+
readonly thinking: ThinkingLevel;
|
|
308
|
+
};
|
|
309
|
+
/** Rejects with an `AgentRunError` when the worker ends without an answer. */
|
|
310
|
+
run(request: WorkerSessionRequest): Promise<string>;
|
|
311
|
+
}
|
|
312
|
+
|
|
270
313
|
/** Remembered facts, each speaker's own; every agent shares the memory of whoever is speaking. */
|
|
271
314
|
export interface MemoryStore {
|
|
272
315
|
/** One speaker's memory; it never reads or changes another's. */
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export type {
|
|
|
39
39
|
RoundtableConfig,
|
|
40
40
|
TierConfig,
|
|
41
41
|
} from "./core/config/config.ts";
|
|
42
|
+
export type { SkillsConfig } from "./core/config/skills-config.ts";
|
|
42
43
|
export type {
|
|
43
44
|
AttachmentPort,
|
|
44
45
|
AttachmentRefusalCode,
|
|
@@ -138,6 +139,12 @@ export type {
|
|
|
138
139
|
} from "./core/domain/interim.ts";
|
|
139
140
|
export type { TurnConversation, TurnRequest } from "./core/domain/ports.ts";
|
|
140
141
|
export type { HoldRefusal, TurnProgress } from "./core/domain/progress.ts";
|
|
142
|
+
export {
|
|
143
|
+
type TurnLimit,
|
|
144
|
+
type TurnTimeout,
|
|
145
|
+
type TurnTimeoutEnd,
|
|
146
|
+
TurnTimeoutError,
|
|
147
|
+
} from "./core/domain/turn-timeout.ts";
|
|
141
148
|
export type { DrainOptions } from "./core/drain.ts";
|
|
142
149
|
export {
|
|
143
150
|
HostStoppingError,
|
|
@@ -193,6 +200,7 @@ export type {
|
|
|
193
200
|
ApprovalDetails,
|
|
194
201
|
ApprovalFile,
|
|
195
202
|
ApprovalOptions,
|
|
203
|
+
ApprovalWording,
|
|
196
204
|
ApprovedBy,
|
|
197
205
|
AskOption,
|
|
198
206
|
AskSupport,
|
|
@@ -309,6 +317,8 @@ export type {
|
|
|
309
317
|
ScheduleStore,
|
|
310
318
|
SkillRegistry,
|
|
311
319
|
SpeakerMemory,
|
|
320
|
+
WorkerSessionRequest,
|
|
321
|
+
WorkerSessions,
|
|
312
322
|
} from "./core/services.ts";
|
|
313
323
|
export {
|
|
314
324
|
AGENTS,
|
|
@@ -322,6 +332,7 @@ export {
|
|
|
322
332
|
RUNTIME,
|
|
323
333
|
SCHEDULES,
|
|
324
334
|
SKILLS,
|
|
335
|
+
WORKER_SESSIONS,
|
|
325
336
|
} from "./core/services.ts";
|
|
326
337
|
export type {
|
|
327
338
|
AgentTurnScope,
|