pi-roundtable 0.26.1 → 0.26.2
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 +33 -0
- package/docs/plugins.md +12 -7
- package/package.json +1 -1
- package/src/core/agents/agent-claim.ts +1 -0
- package/src/core/agents/agent-conversations.ts +94 -0
- package/src/core/agents/agent-messages.ts +247 -56
- package/src/core/agents/agent-outbox.ts +91 -0
- package/src/core/agents/agent-prompt.ts +40 -1
- package/src/core/agents/agent-schema.ts +6 -0
- package/src/core/agents/agent-store.ts +22 -3
- package/src/core/agents/agent-team-fixture.ts +81 -88
- package/src/core/agents/agent-team-seeds.ts +29 -0
- package/src/core/agents/agent-team.ts +17 -14
- package/src/core/agents/agent-tools.ts +34 -5
- package/src/core/agents/group-turns.ts +1 -1
- package/src/core/agents/message-target.ts +79 -0
- package/src/core/agents/team-editing.ts +30 -2
- package/src/core/agents/team-keys.ts +7 -0
- package/src/core/agents/team-options.ts +3 -0
- package/src/core/agents/team-text.ts +4 -0
- package/src/core/agents/team-turn-types.ts +28 -0
- package/src/core/agents/team-turns.ts +36 -25
- package/src/core/assets/prompts/shared-guest.md +1 -3
- package/src/core/assets/prompts/shared.md +1 -3
- package/src/core/builtin/agent-server.ts +5 -0
- package/src/core/builtin/modules.ts +6 -0
- package/src/core/contract/channels.ts +5 -0
- package/src/core/contract/surface.ts +5 -0
- package/src/core/discord/channel-context.ts +3 -1
- package/src/core/discord/embed-text.ts +45 -0
- package/src/core/discord/inbound-message.ts +7 -2
- package/src/core/domain/ports.ts +17 -0
- package/src/core/domain/turn-timeout.ts +2 -0
- package/src/core/i18n/turn-timeout.ts +8 -2
- package/src/core/modules/background/background-turns.ts +3 -0
- package/src/core/modules/schedules/precheck-catalog.ts +49 -0
- package/src/core/modules/schedules/schedule-store.ts +27 -3
- package/src/core/modules/schedules/schedule-tools.ts +43 -86
- package/src/core/modules/schedules/scheduled-turn-text.ts +31 -0
- package/src/core/modules/schedules/schedules.ts +26 -2
- package/src/core/modules/schedules/time-limit.ts +27 -0
- package/src/core/presentation/reply-body.ts +8 -0
- package/src/core/routing/conversation-turns.ts +21 -2
- package/src/core/runtime/extensions/ask-user.ts +1 -1
- package/src/core/runtime/extensions/confirmation-gate.ts +31 -27
- package/src/core/runtime/extensions/hold-lookups.ts +21 -0
- package/src/core/runtime/extensions/late-releases.ts +76 -0
- package/src/core/runtime/pi-agent-runtime.ts +21 -19
- package/src/core/runtime/turn-answer.ts +5 -1
- package/src/core/runtime/turn-budget.ts +1 -0
- package/src/core/runtime/turn-limits.ts +12 -2
- package/src/core/runtime/turn-tools.ts +26 -0
- package/src/core/shared/schedule-tools.ts +25 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { HeldCall } from "../../domain/conversation.ts";
|
|
2
|
+
import { SYSTEM_PRINCIPAL } from "../../identity/principal-store.ts";
|
|
3
|
+
import type { Release, SpeakerRef } from "./gate-refusals.ts";
|
|
4
|
+
import { sameCall } from "./held-calls.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How long a late approval's release waits for the call it was granted for. The turn that
|
|
8
|
+
* answers a late card re-makes the call at once, so an hour covers a slow model or a queue of
|
|
9
|
+
* turns and no more; a release nobody spent by then belongs to a call that is long gone.
|
|
10
|
+
*/
|
|
11
|
+
export const LATE_RELEASE_TTL_MS = 3_600_000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The releases that cards answered after their turn stopped waiting earned. A release lets the
|
|
15
|
+
* identical call run once, only while it is fresh and only in a turn spoken by the person who gave it
|
|
16
|
+
* (or by an owner), only in a turn a person's message started, and it is dropped when the identical call
|
|
17
|
+
* runs without a hold, since that run is the call the person approved.
|
|
18
|
+
*/
|
|
19
|
+
export class LateReleases {
|
|
20
|
+
#list: { release: Release; at: number }[] = [];
|
|
21
|
+
|
|
22
|
+
add(release: Release, now = Date.now()): void {
|
|
23
|
+
this.#prune(now);
|
|
24
|
+
this.#list.push({ release, at: now });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Takes the release of `call` that `speaker` may spend out of the list; undefined when none
|
|
29
|
+
* is fresh and theirs. A release whose approver is unknown is anyone's.
|
|
30
|
+
*/
|
|
31
|
+
spend(
|
|
32
|
+
call: Pick<HeldCall, "tool" | "input">,
|
|
33
|
+
speaker: SpeakerRef | undefined,
|
|
34
|
+
now = Date.now(),
|
|
35
|
+
): Release | undefined {
|
|
36
|
+
this.#prune(now);
|
|
37
|
+
const at = this.#list.findIndex(
|
|
38
|
+
({ release }) =>
|
|
39
|
+
sameCall(release.call, call) && maySpend(release, speaker),
|
|
40
|
+
);
|
|
41
|
+
if (at === -1) return undefined;
|
|
42
|
+
const [spent] = this.#list.splice(at, 1);
|
|
43
|
+
return spent?.release;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Drops every release of `call`, whoever gave it. */
|
|
47
|
+
drop(call: Pick<HeldCall, "tool" | "input">): void {
|
|
48
|
+
this.#list = this.#list.filter(
|
|
49
|
+
({ release }) => !sameCall(release.call, call),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Whether a fresh release of `call` waits, for whoever may spend it. */
|
|
54
|
+
has(call: Pick<HeldCall, "tool" | "input">, now = Date.now()): boolean {
|
|
55
|
+
this.#prune(now);
|
|
56
|
+
return this.#list.some(({ release }) => sameCall(release.call, call));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#prune(now: number): void {
|
|
60
|
+
this.#list = this.#list.filter(({ at }) => now - at <= LATE_RELEASE_TTL_MS);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether `speaker` may spend `release`: they gave it, or are an owner; any speaker, when its
|
|
66
|
+
* giver is unknown. The gate hands only the speaker of a turn a person's
|
|
67
|
+
* message started; every other turn (schedule, agent message, report, error report) has none and
|
|
68
|
+
* spends nothing, so a card is shown again there. The system principal never spends one.
|
|
69
|
+
*/
|
|
70
|
+
function maySpend(release: Release, speaker: SpeakerRef | undefined): boolean {
|
|
71
|
+
if (!speaker || speaker.principalId === SYSTEM_PRINCIPAL) return false;
|
|
72
|
+
if (!release.by) return true;
|
|
73
|
+
return (
|
|
74
|
+
speaker.tier === "owner" || speaker.principalId === release.by.principalId
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -21,10 +21,9 @@ import type { TurnRequest } from "../domain/ports.ts";
|
|
|
21
21
|
import { HostStoppingError } from "../errors.ts";
|
|
22
22
|
import { assistantName } from "../i18n/index.ts";
|
|
23
23
|
import { promptScopeOf } from "../interactions/prompts.ts";
|
|
24
|
-
import { MEMORY_TOOLS } from "../modules/memory/owner-memory.ts";
|
|
25
24
|
import { withoutReplyFiles } from "../reply-files.ts";
|
|
26
25
|
import type { TransientTask } from "../sessions.ts";
|
|
27
|
-
import { type ToolTiers,
|
|
26
|
+
import { type ToolTiers, toolTiers } from "../tool-tiers.ts";
|
|
28
27
|
import { AbortGuard } from "./abort-guard.ts";
|
|
29
28
|
import { bridgeTurnRefusal, chosenAgentModel } from "./bridge-guard.ts";
|
|
30
29
|
import { ConversationSessions } from "./conversation-sessions.ts";
|
|
@@ -52,6 +51,7 @@ import { transcriptOf, turnAnswer, unspokenTurn } from "./turn-answer.ts";
|
|
|
52
51
|
import type { TurnBudget } from "./turn-budget.ts";
|
|
53
52
|
import { promptFailed, turnLimits } from "./turn-limits.ts";
|
|
54
53
|
import { progressReporter } from "./turn-progress.ts";
|
|
54
|
+
import { turnTools } from "./turn-tools.ts";
|
|
55
55
|
import { wrapUpTurn } from "./turn-wrapup.ts";
|
|
56
56
|
import { workerReport } from "./worker-task.ts";
|
|
57
57
|
|
|
@@ -62,8 +62,6 @@ export {
|
|
|
62
62
|
sessionExtensions,
|
|
63
63
|
} from "./runtime-types.ts";
|
|
64
64
|
|
|
65
|
-
const MEMORY = new Set<string>(MEMORY_TOOLS);
|
|
66
|
-
|
|
67
65
|
/** AgentRuntime over the Pi SDK: one persistent session per channel, each with its own loader. */
|
|
68
66
|
const STOPPED = "stopped by the owner";
|
|
69
67
|
|
|
@@ -183,18 +181,15 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
183
181
|
request.agent !== undefined &&
|
|
184
182
|
this.#options.agents?.memory === "owners" &&
|
|
185
183
|
tier !== "owner";
|
|
186
|
-
const wanted =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
]),
|
|
194
|
-
],
|
|
195
|
-
tier,
|
|
184
|
+
const wanted = turnTools(
|
|
185
|
+
{
|
|
186
|
+
selected: this.#factory.toolsFor(request.selection),
|
|
187
|
+
held: request.confirmed && pending ? pending.calls : [],
|
|
188
|
+
tier,
|
|
189
|
+
withheld,
|
|
190
|
+
},
|
|
196
191
|
this.#tiers,
|
|
197
|
-
)
|
|
192
|
+
);
|
|
198
193
|
const markPrivateMemory = recordMemoryTurn(
|
|
199
194
|
session.sessionManager,
|
|
200
195
|
request.speaker.principalId,
|
|
@@ -221,6 +216,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
221
216
|
request.speaker,
|
|
222
217
|
conversation.visibility === "private",
|
|
223
218
|
turnYolo(this.#options, request),
|
|
219
|
+
request.byMessage === true,
|
|
224
220
|
);
|
|
225
221
|
if (request.confirmed && pending && pending.calls.length > 0) {
|
|
226
222
|
logger.info(
|
|
@@ -231,7 +227,6 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
231
227
|
"confirmed actions released",
|
|
232
228
|
);
|
|
233
229
|
}
|
|
234
|
-
|
|
235
230
|
// Collected as they end: a compaction during the turn shortens session.messages.
|
|
236
231
|
const turnMessages: TurnMessage[] = [];
|
|
237
232
|
const toolCalls: string[] = [];
|
|
@@ -240,8 +235,12 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
240
235
|
// The caller's live view: the text as it streams and each tool, never the thinking.
|
|
241
236
|
const slot = this.#sessions.slot(key);
|
|
242
237
|
// Its time limit, not counting time waiting on the owner's cards, follows what it does to say why it fired.
|
|
243
|
-
const limits = turnLimits(
|
|
244
|
-
|
|
238
|
+
const limits = turnLimits(
|
|
239
|
+
this.#options,
|
|
240
|
+
slot,
|
|
241
|
+
request.channel,
|
|
242
|
+
() => guard.abort(),
|
|
243
|
+
request.timeLimitMs,
|
|
245
244
|
);
|
|
246
245
|
const progress = request.progress
|
|
247
246
|
? progressReporter(request.progress, {
|
|
@@ -333,7 +332,10 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
333
332
|
|
|
334
333
|
if (running.stopped)
|
|
335
334
|
return { ok: false, error: new AgentRunError(STOPPED), stopped: true };
|
|
336
|
-
return
|
|
335
|
+
return (
|
|
336
|
+
limits.failure() ??
|
|
337
|
+
turnAnswer(turnMessages, running.steered, request.unattended !== true)
|
|
338
|
+
);
|
|
337
339
|
}
|
|
338
340
|
|
|
339
341
|
async steer(
|
|
@@ -77,11 +77,14 @@ export function thinkingOf(messages: readonly Message[]): string {
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* A finished turn's result from the messages it added. A steered run may have answered before
|
|
80
|
-
* the steer arrived, so every final answer of it is kept.
|
|
80
|
+
* the steer arrived, so every final answer of it is kept. A turn nobody waits on (`attended`
|
|
81
|
+
* false: a schedule's, an agent's message) may end with nothing to say, such as "send nothing if
|
|
82
|
+
* there is no progress"; that is an empty answer, not a failure. For an attended turn it is one.
|
|
81
83
|
*/
|
|
82
84
|
export function turnAnswer(
|
|
83
85
|
messages: readonly Message[],
|
|
84
86
|
steered: boolean,
|
|
87
|
+
attended: boolean,
|
|
85
88
|
): TurnResult {
|
|
86
89
|
const last = lastAssistant(messages);
|
|
87
90
|
if (!last.ok) return { ok: false, error: new AgentRunError(last.error) };
|
|
@@ -97,6 +100,7 @@ export function turnAnswer(
|
|
|
97
100
|
.join("\n\n")
|
|
98
101
|
: textOf(last.message.content)
|
|
99
102
|
).trim();
|
|
103
|
+
if (!text && !hasReplyFiles() && !attended) return { ok: true, text: "" };
|
|
100
104
|
if (!text && !hasReplyFiles())
|
|
101
105
|
return {
|
|
102
106
|
ok: false,
|
|
@@ -35,7 +35,8 @@ export function timeoutMessage(timeout: TurnTimeout): string {
|
|
|
35
35
|
/**
|
|
36
36
|
* The time limit of a turn of the built-in runtime; see `runtimeTurnBudget`. `abort` aborts the
|
|
37
37
|
* turn. In a long-running conversation (`options.modes`) a limit extends the turn instead of
|
|
38
|
-
* asking or aborting, until the turn has worked `options.autonomy.longRunningMaxMs`.
|
|
38
|
+
* asking or aborting, until the turn has worked `options.autonomy.longRunningMaxMs`. A
|
|
39
|
+
* `timeLimitMs` replaces the turn's limit and its cap, both held under that same ceiling.
|
|
39
40
|
*/
|
|
40
41
|
export function turnLimits(
|
|
41
42
|
options: Parameters<typeof runtimeTurnBudget>[0] &
|
|
@@ -43,15 +44,24 @@ export function turnLimits(
|
|
|
43
44
|
slot: Pick<PromptSlot, "waitedMs" | "cards">,
|
|
44
45
|
channel: ChannelKey,
|
|
45
46
|
abort: () => void,
|
|
47
|
+
timeLimitMs?: number,
|
|
46
48
|
): TurnLimits {
|
|
47
49
|
const { longRunningMaxMs } = options.autonomy ?? DEFAULT_AUTONOMY;
|
|
50
|
+
const limited =
|
|
51
|
+
timeLimitMs === undefined
|
|
52
|
+
? options
|
|
53
|
+
: {
|
|
54
|
+
...options,
|
|
55
|
+
turnTimeoutMs: Math.min(timeLimitMs, longRunningMaxMs),
|
|
56
|
+
turnMaxMs: Math.min(timeLimitMs, longRunningMaxMs),
|
|
57
|
+
};
|
|
48
58
|
const keepGoing = (workedMs: number) =>
|
|
49
59
|
workedMs < longRunningMaxMs &&
|
|
50
60
|
options.modes?.peek(channel).longRunning === true;
|
|
51
61
|
const activity = activityTracker();
|
|
52
62
|
let timedOut: TurnTimeout | undefined;
|
|
53
63
|
const budget = runtimeTurnBudget(
|
|
54
|
-
|
|
64
|
+
limited,
|
|
55
65
|
slot,
|
|
56
66
|
channel,
|
|
57
67
|
(timeout) => {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HeldCall } from "../domain/conversation.ts";
|
|
2
|
+
import { MEMORY_TOOLS } from "../modules/memory/owner-memory.ts";
|
|
3
|
+
import type { Tier } from "../speakers.ts";
|
|
4
|
+
import { type ToolTiers, toolsForTier } from "../tool-tiers.ts";
|
|
5
|
+
|
|
6
|
+
const MEMORY = new Set<string>(MEMORY_TOOLS);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The tools a turn is given: its selection, and the tools of the held calls an approving turn
|
|
10
|
+
* releases, limited to the speaker's tier; a speaker kept out of memory gets none of its tools.
|
|
11
|
+
*/
|
|
12
|
+
export function turnTools(
|
|
13
|
+
turn: {
|
|
14
|
+
selected: readonly string[];
|
|
15
|
+
held: readonly Pick<HeldCall, "tool">[];
|
|
16
|
+
tier: Tier;
|
|
17
|
+
withheld: boolean;
|
|
18
|
+
},
|
|
19
|
+
tiers: ToolTiers,
|
|
20
|
+
): string[] {
|
|
21
|
+
return toolsForTier(
|
|
22
|
+
[...new Set([...turn.selected, ...turn.held.map((call) => call.tool)])],
|
|
23
|
+
turn.tier,
|
|
24
|
+
tiers,
|
|
25
|
+
).filter((name) => !turn.withheld || !MEMORY.has(name));
|
|
26
|
+
}
|
|
@@ -13,6 +13,9 @@ export const SCHEDULE_TOOLS = Object.freeze([
|
|
|
13
13
|
] as const);
|
|
14
14
|
export type ScheduleToolName = (typeof SCHEDULE_TOOLS)[number];
|
|
15
15
|
|
|
16
|
+
/** The most minutes a schedule's own time limit may name: the four hours a long-running turn may work. */
|
|
17
|
+
export const MAX_SCHEDULE_TIME_LIMIT_MINUTES = 240;
|
|
18
|
+
|
|
16
19
|
export function isScheduleTool(value: string): value is ScheduleToolName {
|
|
17
20
|
return (SCHEDULE_TOOLS as readonly string[]).includes(value);
|
|
18
21
|
}
|
|
@@ -62,6 +65,8 @@ const timing = (zone: string) => ({
|
|
|
62
65
|
),
|
|
63
66
|
});
|
|
64
67
|
|
|
68
|
+
const TIME_LIMIT = `How long, in minutes, each run may work before it is stopped (at most ${MAX_SCHEDULE_TIME_LIMIT_MINUTES}); without it a run is stopped after the host's default of 10 minutes. Set it for a task that works longer.`;
|
|
69
|
+
|
|
65
70
|
export interface ScheduleToolSpec {
|
|
66
71
|
name: ScheduleToolName;
|
|
67
72
|
label: string;
|
|
@@ -118,6 +123,13 @@ export function scheduleToolSpecs(
|
|
|
118
123
|
"The name of a precheck the host runs first, from schedule_list; you are woken only when it finds something.",
|
|
119
124
|
}),
|
|
120
125
|
),
|
|
126
|
+
time_limit_minutes: Type.Optional(
|
|
127
|
+
Type.Integer({
|
|
128
|
+
minimum: 1,
|
|
129
|
+
maximum: MAX_SCHEDULE_TIME_LIMIT_MINUTES,
|
|
130
|
+
description: TIME_LIMIT,
|
|
131
|
+
}),
|
|
132
|
+
),
|
|
121
133
|
...script(false),
|
|
122
134
|
}),
|
|
123
135
|
},
|
|
@@ -133,7 +145,7 @@ export function scheduleToolSpecs(
|
|
|
133
145
|
name: "schedule_update",
|
|
134
146
|
label: "Update schedule",
|
|
135
147
|
description:
|
|
136
|
-
"Change one of this channel's schedules: its title, its prompt, its timing (timing fields replace the old timing), or its
|
|
148
|
+
"Change one of this channel's schedules: its title, its prompt, its timing (timing fields replace the old timing), its precheck, or its time limit. A run can use this to keep its own prompt current, such as adding what it already reported.",
|
|
137
149
|
parameters: Type.Object({
|
|
138
150
|
id: Type.Integer({ description: "Schedule id." }),
|
|
139
151
|
title: Type.Optional(Type.String()),
|
|
@@ -147,6 +159,18 @@ export function scheduleToolSpecs(
|
|
|
147
159
|
"A precheck's name from schedule_list to run first, or null to remove the schedule's precheck.",
|
|
148
160
|
}),
|
|
149
161
|
),
|
|
162
|
+
time_limit_minutes: Type.Optional(
|
|
163
|
+
Type.Union(
|
|
164
|
+
[
|
|
165
|
+
Type.Integer({
|
|
166
|
+
minimum: 1,
|
|
167
|
+
maximum: MAX_SCHEDULE_TIME_LIMIT_MINUTES,
|
|
168
|
+
}),
|
|
169
|
+
Type.Null(),
|
|
170
|
+
],
|
|
171
|
+
{ description: `${TIME_LIMIT} null returns it to the default.` },
|
|
172
|
+
),
|
|
173
|
+
),
|
|
150
174
|
...script(true),
|
|
151
175
|
}),
|
|
152
176
|
},
|