pi-roundtable 0.22.5 → 0.23.0
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 +55 -1
- package/README.md +35 -9
- package/README.zh-TW.md +51 -13
- package/docs/migrating-0.8.md +2 -2
- package/docs/plugins.md +350 -28
- package/examples/discord-adapter.ts +9 -1
- package/examples/seeds.test.ts +2 -2
- package/examples/seeds.ts +1 -2
- package/package.json +6 -2
- package/src/characters/catalog.ts +1055 -0
- package/src/characters/characters.css +68 -0
- package/src/characters/colors.ts +108 -0
- package/src/characters/compose.ts +281 -0
- package/src/characters/css.ts +63 -0
- package/src/characters/index.ts +33 -0
- package/src/characters/parse.ts +196 -0
- package/src/characters/parts-face.ts +354 -0
- package/src/characters/parts-hair.ts +56 -0
- package/src/characters/parts-hats.ts +57 -0
- package/src/characters/parts-outfits.ts +81 -0
- package/src/characters/parts-props.ts +137 -0
- package/src/characters/spec.ts +224 -0
- package/src/characters/suggest.ts +101 -0
- package/src/cli/checks/characters.ts +23 -0
- package/src/cli/checks/images.ts +5 -3
- package/src/cli/doctor.ts +2 -0
- package/src/core/agents/agent-claim.ts +30 -11
- package/src/core/agents/agent-dashboard.ts +1 -1
- package/src/core/agents/agent-messages.ts +43 -15
- package/src/core/agents/agent-prompt.ts +2 -4
- package/src/core/agents/agent-rules.ts +42 -3
- package/src/core/agents/agent-schema.ts +37 -15
- package/src/core/agents/agent-store.ts +57 -13
- package/src/core/agents/agent-team-fixture.ts +90 -14
- package/src/core/agents/agent-team.ts +83 -50
- package/src/core/agents/agent-tools.ts +94 -41
- package/src/core/agents/avatar-studio.ts +40 -4
- package/src/core/agents/character-png.ts +40 -0
- package/src/core/agents/group-messages.ts +7 -4
- package/src/core/agents/group-turns.ts +27 -14
- package/src/core/agents/team-editing.ts +99 -58
- package/src/core/agents/team-keys.ts +177 -11
- package/src/core/agents/team-lifecycle.ts +151 -84
- package/src/core/agents/team-options.ts +13 -6
- package/src/core/agents/team-status.ts +31 -13
- package/src/core/agents/team-text.ts +13 -3
- package/src/core/agents/team-turn-types.ts +25 -7
- package/src/core/agents/team-turns.ts +143 -28
- package/src/core/attachments/attachment-fetcher.ts +162 -64
- package/src/core/attachments/prompt-block.ts +7 -1
- package/src/core/attachments/tool-attachment.ts +20 -1
- package/src/core/attachments/turn-attachments.ts +18 -2
- package/src/core/builtin/agent-server.ts +103 -40
- package/src/core/builtin/discord.ts +4 -17
- package/src/core/builtin/modules.ts +6 -5
- package/src/core/builtin/threads.ts +37 -0
- package/src/core/config/agent-team.ts +43 -0
- package/src/core/config/config.ts +50 -59
- package/src/core/config/limits.ts +93 -0
- package/src/core/config/ops.ts +34 -0
- package/src/core/config/schema.ts +26 -0
- package/src/core/contract/channels.ts +20 -0
- package/src/core/contract/surface.ts +77 -7
- package/src/core/define-roundtable.ts +70 -42
- package/src/core/discord/agent-commands.ts +108 -86
- package/src/core/discord/agent-panel.ts +11 -2
- package/src/core/discord/connection.ts +1 -1
- package/src/core/discord/discord-surface.ts +10 -1
- package/src/core/discord/dispatch-thread-host.ts +1 -1
- package/src/core/domain/conversation.ts +24 -0
- package/src/core/holds.ts +221 -15
- package/src/core/host-registration.ts +2 -1
- package/src/core/http/listeners.ts +63 -3
- package/src/core/i18n/agent-panel.ts +14 -21
- package/src/core/interactions/prompts.ts +12 -0
- package/src/core/modes/autonomy.ts +23 -0
- package/src/core/modes/conversation-modes.ts +213 -0
- package/src/core/modes/modes-plugin.ts +24 -0
- package/src/core/modules/delegation/delegator.ts +4 -4
- package/src/core/modules/host-shell/shell-arithmetic.ts +81 -0
- package/src/core/modules/host-shell/shell-lexer-types.ts +53 -0
- package/src/core/modules/host-shell/shell-lexer.ts +493 -0
- package/src/core/modules/host-shell/shell-paths.ts +85 -4
- package/src/core/modules/host-shell/shell-policy.ts +341 -332
- package/src/core/modules/host-shell/shell-programs.ts +298 -0
- package/src/core/modules/host-shell/shell-quoting.ts +237 -0
- package/src/core/modules/host-shell/shell-scripts.ts +114 -0
- package/src/core/modules/host-shell/shell-wrappers.ts +104 -0
- package/src/core/modules/schedules/precheck-tools.ts +17 -0
- package/src/core/modules/schedules/schedules.ts +9 -3
- package/src/core/plugin.ts +8 -1
- package/src/core/presence/direct-channels.ts +30 -0
- package/src/core/presentation/reply-body.ts +25 -0
- package/src/core/registry/contributions.ts +31 -7
- package/src/core/registry/providers.ts +1 -1
- package/src/core/routing/conversation-turns.ts +2 -15
- package/src/core/routing/surface-port.ts +5 -1
- package/src/core/runtime/extensions/approval-card.ts +1 -5
- package/src/core/runtime/extensions/approval-fields.ts +3 -7
- package/src/core/runtime/extensions/confirmation-gate-extension.ts +13 -2
- package/src/core/runtime/extensions/confirmation-gate.ts +41 -40
- package/src/core/runtime/extensions/gate-refusals.ts +23 -0
- package/src/core/runtime/extensions/held-calls.ts +16 -1
- package/src/core/runtime/extensions/hold-describer.ts +58 -8
- package/src/core/runtime/extensions/hold-lookups.ts +19 -19
- package/src/core/runtime/extensions/yolo.ts +105 -0
- package/src/core/runtime/pi-agent-runtime.ts +3 -5
- package/src/core/runtime/runtime-plugin.ts +42 -1
- package/src/core/runtime/runtime-types.ts +11 -0
- package/src/core/runtime/turn-budget.ts +32 -5
- package/src/core/runtime/turn-limits.ts +14 -3
- package/src/core/services.ts +61 -9
- package/src/core/shared/attachment-reader.ts +6 -0
- package/src/core/shared/unix-server.ts +10 -1
- package/src/core/testing/modules.ts +12 -8
- package/src/core/testing/surface-threads.ts +58 -0
- package/src/core/testing/thread-host.ts +17 -5
- package/src/core/threads/dispatch-threads.ts +299 -0
- package/src/core/threads/host-threads.ts +19 -0
- package/src/core/tool-set.snapshot.json +5 -0
- package/src/index.ts +27 -1
- package/src/kit/characters.ts +5 -0
- package/src/kit/index.ts +2 -0
- package/src/kit/shell.ts +1 -0
- package/src/kit/support.ts +1 -1
- package/src/kit/threads.ts +1 -1
- package/src/testing.ts +4 -0
- package/templates/agents.ts +1 -1
- package/src/core/discord/dispatch-threads.ts +0 -194
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
HeldCall,
|
|
3
3
|
PendingConfirmation,
|
|
4
4
|
} from "../../domain/conversation.ts";
|
|
5
|
+
import type { HoldRefusal } from "../../domain/progress.ts";
|
|
5
6
|
import { type OwnerIdentity, ownerWords } from "../../identity.ts";
|
|
6
7
|
import type {
|
|
7
8
|
ApprovalDetails,
|
|
@@ -181,3 +182,25 @@ function orList(items: readonly string[]): string {
|
|
|
181
182
|
if (items.length < 2 || last === undefined) return items.join("");
|
|
182
183
|
return `${items.slice(0, -1).join(", ")} or ${last}`;
|
|
183
184
|
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Decides for a held call whether the server approves it without a card, and says who approved
|
|
188
|
+
* it when it does; see `yoloApprover`. `card.speakerOnly` is true when the call's card is the
|
|
189
|
+
* speaker's alone. It must change nothing but its own record of the approval.
|
|
190
|
+
*/
|
|
191
|
+
export type AutoApprover = (
|
|
192
|
+
call: HeldCall,
|
|
193
|
+
card: { speakerOnly: boolean },
|
|
194
|
+
) => ApprovedBy | undefined;
|
|
195
|
+
|
|
196
|
+
/** What `ConfirmationGate.decide` answers for a call; an empty answer lets the call run. */
|
|
197
|
+
export interface GateDecision {
|
|
198
|
+
reason?: string;
|
|
199
|
+
approvedOnCard?: true;
|
|
200
|
+
/** Who approved the call, when it was held and a surface or a confirming message said who. */
|
|
201
|
+
approvedBy?: ApprovedBy;
|
|
202
|
+
/** The id of the card whose approval released the call, in this turn or answered late; absent for a call a message confirmed. */
|
|
203
|
+
approvedPrompt?: string;
|
|
204
|
+
/** Why a hold refused the call, for a surface that tells that from a failure; absent when it ran or the turn stopped. */
|
|
205
|
+
refused?: HoldRefusal;
|
|
206
|
+
}
|
|
@@ -28,6 +28,19 @@ export function sameCall(
|
|
|
28
28
|
return a.tool === b.tool && a.input === b.input;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Takes the first approval of `call` out of the lists, in order; undefined when none approves it. */
|
|
32
|
+
export function spendApproval<R extends { call: HeldCall }>(
|
|
33
|
+
lists: readonly R[][],
|
|
34
|
+
call: Pick<HeldCall, "tool" | "input">,
|
|
35
|
+
): R | undefined {
|
|
36
|
+
for (const list of lists) {
|
|
37
|
+
const at = list.findIndex((r) => sameCall(r.call, call));
|
|
38
|
+
const [spent] = at === -1 ? [] : list.splice(at, 1);
|
|
39
|
+
if (spent) return spent;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
/** JSON with object keys sorted at every level, so equal inputs compare equal. */
|
|
32
45
|
// pi-lens-ignore: no-unknown-parameters — tool input is untyped model JSON; this is where it is read
|
|
33
46
|
export function canonicalJson(value: unknown): string {
|
|
@@ -38,7 +51,9 @@ export function canonicalJson(value: unknown): string {
|
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
// pi-lens-ignore: no-unknown-parameters — tool input is untyped model JSON; this is where it is read
|
|
41
|
-
function isPlainObject(
|
|
54
|
+
export function isPlainObject(
|
|
55
|
+
value: unknown,
|
|
56
|
+
): value is Record<string, unknown> {
|
|
42
57
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
43
58
|
}
|
|
44
59
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { ToolTurn } from "../../define.ts";
|
|
2
|
+
import {
|
|
3
|
+
type ClassedHold,
|
|
4
|
+
type HoldCheck,
|
|
5
|
+
type HoldContext,
|
|
6
|
+
type HoldDescription,
|
|
7
|
+
isPromise,
|
|
8
|
+
} from "../../holds.ts";
|
|
2
9
|
import { messages } from "../../i18n/index.ts";
|
|
3
10
|
import {
|
|
4
11
|
awaited,
|
|
@@ -8,6 +15,27 @@ import {
|
|
|
8
15
|
joinApprovers,
|
|
9
16
|
} from "./hold-lookups.ts";
|
|
10
17
|
|
|
18
|
+
/** A held call's description with its classes inside, for the gate to record with the call. */
|
|
19
|
+
function withClasses(
|
|
20
|
+
held: ClassedHold | undefined,
|
|
21
|
+
): HoldDescription | undefined {
|
|
22
|
+
if (!held) return undefined;
|
|
23
|
+
const { description, classes } = held;
|
|
24
|
+
return typeof description === "string"
|
|
25
|
+
? { text: description, classes }
|
|
26
|
+
: { ...description, classes };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A description without the classes it names. */
|
|
30
|
+
function unclassed(
|
|
31
|
+
description: HoldDescription | undefined,
|
|
32
|
+
): HoldDescription | undefined {
|
|
33
|
+
if (typeof description !== "object" || description === null)
|
|
34
|
+
return description;
|
|
35
|
+
const { classes: _classes, ...rest } = description;
|
|
36
|
+
return rest;
|
|
37
|
+
}
|
|
38
|
+
|
|
11
39
|
/**
|
|
12
40
|
* Asks a conversation's holds to describe a call a turn makes, and hands each description on in
|
|
13
41
|
* the order the calls were made, whichever comes back first, so calls made together are held,
|
|
@@ -44,13 +72,7 @@ export class HoldDescriber {
|
|
|
44
72
|
if (turn.signal?.aborted) controller.abort();
|
|
45
73
|
let answer: ReturnType<NonNullable<HoldCheck["inTurn"]>>;
|
|
46
74
|
try {
|
|
47
|
-
answer = this.#
|
|
48
|
-
this.#holds,
|
|
49
|
-
tool,
|
|
50
|
-
input,
|
|
51
|
-
this.#context,
|
|
52
|
-
turn.toolTurn(controller.signal),
|
|
53
|
-
);
|
|
75
|
+
answer = this.#ask(tool, input, turn.toolTurn(controller.signal));
|
|
54
76
|
} catch {
|
|
55
77
|
answer = messages().holdGeneric(tool);
|
|
56
78
|
}
|
|
@@ -79,6 +101,34 @@ export class HoldDescriber {
|
|
|
79
101
|
);
|
|
80
102
|
}
|
|
81
103
|
|
|
104
|
+
/** The holds' answer for the call, with its classes in the description when the holds report them. */
|
|
105
|
+
#ask(
|
|
106
|
+
tool: string,
|
|
107
|
+
input: Record<string, unknown>,
|
|
108
|
+
toolTurn: ToolTurn,
|
|
109
|
+
): HoldDescription | undefined | Promise<HoldDescription | undefined> {
|
|
110
|
+
const classed = this.#holds.inTurnWithClasses;
|
|
111
|
+
if (!classed) {
|
|
112
|
+
// A check that does not vouch for its classes has none: yolo mode cards its calls.
|
|
113
|
+
const answer = this.#holds.inTurn?.call(
|
|
114
|
+
this.#holds,
|
|
115
|
+
tool,
|
|
116
|
+
input,
|
|
117
|
+
this.#context,
|
|
118
|
+
toolTurn,
|
|
119
|
+
);
|
|
120
|
+
return isPromise(answer) ? answer.then(unclassed) : unclassed(answer);
|
|
121
|
+
}
|
|
122
|
+
const answer = classed.call(
|
|
123
|
+
this.#holds,
|
|
124
|
+
tool,
|
|
125
|
+
input,
|
|
126
|
+
this.#context,
|
|
127
|
+
toolTurn,
|
|
128
|
+
);
|
|
129
|
+
return isPromise(answer) ? answer.then(withClasses) : withClasses(answer);
|
|
130
|
+
}
|
|
131
|
+
|
|
82
132
|
#inOrder<T, R>(
|
|
83
133
|
described: T | Promise<T>,
|
|
84
134
|
use: (described: T) => R,
|
|
@@ -34,6 +34,8 @@ export interface Judgment {
|
|
|
34
34
|
approvers?: readonly string[];
|
|
35
35
|
/** The card is the speaker's alone; see `HoldDescription`. */
|
|
36
36
|
speakerOnly?: true;
|
|
37
|
+
/** The hold classes of the call; see `HoldRule.holdClasses`. Absent when the holds gave none. */
|
|
38
|
+
classes?: readonly string[] | undefined;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export type Judged = Judgment | undefined | typeof STOPPED;
|
|
@@ -57,15 +59,19 @@ function isSignal(value: unknown): value is AbortSignal {
|
|
|
57
59
|
export function described(
|
|
58
60
|
answer: HoldDescription,
|
|
59
61
|
tool: string,
|
|
60
|
-
): Pick<Judgment, "action" | "copy" | "supersede" | "speakerOnly"> {
|
|
62
|
+
): Pick<Judgment, "action" | "copy" | "supersede" | "speakerOnly" | "classes"> {
|
|
61
63
|
if (typeof answer === "string") return { action: answer };
|
|
62
64
|
if (typeof answer !== "object" || answer === null)
|
|
63
65
|
return { action: messages().holdGeneric(tool) };
|
|
64
|
-
const { text, copy, supersede, approval } = answer;
|
|
66
|
+
const { text, copy, supersede, approval, classes } = answer;
|
|
65
67
|
if (typeof text !== "string") return { action: messages().holdGeneric(tool) };
|
|
68
|
+
const named = Array.isArray(classes)
|
|
69
|
+
? classes.filter((id): id is string => typeof id === "string" && id !== "")
|
|
70
|
+
: [];
|
|
66
71
|
const withdrawal = {
|
|
67
72
|
...(isSignal(supersede) ? { supersede } : {}),
|
|
68
73
|
...(approval === "speaker" ? { speakerOnly: true as const } : {}),
|
|
74
|
+
...(named.length > 0 ? { classes: named } : {}),
|
|
69
75
|
};
|
|
70
76
|
if (
|
|
71
77
|
!copy ||
|
|
@@ -135,7 +141,16 @@ export function joinApprovers(
|
|
|
135
141
|
copy,
|
|
136
142
|
supersede,
|
|
137
143
|
speakerOnly,
|
|
144
|
+
classes,
|
|
138
145
|
} = described(action, tool);
|
|
146
|
+
const joined = (approvers: readonly string[] | undefined): Judgment => ({
|
|
147
|
+
action: text,
|
|
148
|
+
...(copy ? { copy } : {}),
|
|
149
|
+
...(supersede ? { supersede } : {}),
|
|
150
|
+
...(speakerOnly ? { speakerOnly } : {}),
|
|
151
|
+
...(classes ? { classes } : {}),
|
|
152
|
+
...(approvers ? { approvers } : {}),
|
|
153
|
+
});
|
|
139
154
|
const controller = new AbortController();
|
|
140
155
|
if (turn.signal?.aborted) controller.abort();
|
|
141
156
|
let named:
|
|
@@ -155,25 +170,10 @@ export function joinApprovers(
|
|
|
155
170
|
}
|
|
156
171
|
if (!isPromise(named)) {
|
|
157
172
|
controller.abort();
|
|
158
|
-
return
|
|
159
|
-
action: text,
|
|
160
|
-
...(copy ? { copy } : {}),
|
|
161
|
-
...(supersede ? { supersede } : {}),
|
|
162
|
-
...(speakerOnly ? { speakerOnly } : {}),
|
|
163
|
-
...(named ? { approvers: named } : {}),
|
|
164
|
-
};
|
|
173
|
+
return joined(named);
|
|
165
174
|
}
|
|
166
175
|
return awaited(named, [], controller, turn.signal, from.timeoutMs).then(
|
|
167
|
-
(approvers) =>
|
|
168
|
-
if (approvers === STOPPED) return STOPPED;
|
|
169
|
-
return {
|
|
170
|
-
action: text,
|
|
171
|
-
...(copy ? { copy } : {}),
|
|
172
|
-
...(supersede ? { supersede } : {}),
|
|
173
|
-
...(speakerOnly ? { speakerOnly } : {}),
|
|
174
|
-
...(approvers ? { approvers } : {}),
|
|
175
|
-
};
|
|
176
|
-
},
|
|
176
|
+
(approvers) => (approvers === STOPPED ? STOPPED : joined(approvers)),
|
|
177
177
|
);
|
|
178
178
|
};
|
|
179
179
|
return isPromise(answer) ? answer.then(join) : join(answer);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { HeldCall } from "../../domain/conversation.ts";
|
|
2
|
+
import type { TurnRequest } from "../../domain/ports.ts";
|
|
3
|
+
import { SYSTEM_PRINCIPAL } from "../../identity/principal-store.ts";
|
|
4
|
+
import type { ApprovedBy } from "../../interactions/prompts.ts";
|
|
5
|
+
import { type Autonomy, DEFAULT_AUTONOMY } from "../../modes/autonomy.ts";
|
|
6
|
+
import type { ConversationModes } from "../../modes/conversation-modes.ts";
|
|
7
|
+
import { YOLO_CARDED_SHELL_CLASSES } from "../../modules/host-shell/shell-policy.ts";
|
|
8
|
+
import type { ChannelKey } from "../../sessions.ts";
|
|
9
|
+
import type { PiAgentRuntimeOptions } from "../runtime-types.ts";
|
|
10
|
+
import type { AutoApprover } from "./confirmation-gate.ts";
|
|
11
|
+
import type { SpeakerRef } from "./gate-refusals.ts";
|
|
12
|
+
|
|
13
|
+
/** What a turn's yolo approver needs to know. */
|
|
14
|
+
export interface YoloSeams {
|
|
15
|
+
/** The conversations' modes: read when a call is held, and told of each approval. */
|
|
16
|
+
modes: Pick<ConversationModes, "peek" | "autoApproved">;
|
|
17
|
+
/** The classes yolo may approve although it cards them by default, and those it never approves. */
|
|
18
|
+
autonomy: Pick<Autonomy, "yolo">;
|
|
19
|
+
/** The conversation the turn runs in, whose yolo mode decides. */
|
|
20
|
+
channel: ChannelKey;
|
|
21
|
+
/** Who spoke the turn: only an owner other than the host's own principal is acted for. */
|
|
22
|
+
speaker: SpeakerRef;
|
|
23
|
+
/** The clock, for the time of an approval. */
|
|
24
|
+
now?: () => Date;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The rule a class belongs to: the part before its first dot, or the whole of a rule's own name. */
|
|
28
|
+
function ruleOf(id: string): string {
|
|
29
|
+
const dot = id.indexOf(".");
|
|
30
|
+
return dot === -1 ? id : id.slice(0, dot);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether an id is a rule's bare name, which a call carries when its rule reports no classes for
|
|
35
|
+
* it: the call is unclassed. Rule names have no dot and every class id has one.
|
|
36
|
+
*/
|
|
37
|
+
function unclassed(id: string): boolean {
|
|
38
|
+
return !id.includes(".");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The auto-approver of a turn on a runtime built with `options.modes`; undefined without them. */
|
|
42
|
+
export function turnYolo(
|
|
43
|
+
options: Pick<PiAgentRuntimeOptions, "modes" | "autonomy">,
|
|
44
|
+
request: Pick<TurnRequest, "channel" | "speaker">,
|
|
45
|
+
): AutoApprover | undefined {
|
|
46
|
+
const { modes } = options;
|
|
47
|
+
return modes
|
|
48
|
+
? yoloApprover({
|
|
49
|
+
modes,
|
|
50
|
+
autonomy: options.autonomy ?? DEFAULT_AUTONOMY,
|
|
51
|
+
channel: request.channel,
|
|
52
|
+
speaker: request.speaker,
|
|
53
|
+
})
|
|
54
|
+
: undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The auto-approver of one turn in a yolo conversation: it answers for a held call, so that no
|
|
59
|
+
* card opens, only when all of these hold at the time of the call:
|
|
60
|
+
* - the conversation's yolo mode is on (read now, so a card opened before the toggle stays open);
|
|
61
|
+
* - the turn's speaker is an owner, and not the host's own principal, which the webhook and report
|
|
62
|
+
* turns speak as;
|
|
63
|
+
* - the call names no approvers and its card is not the speaker's alone;
|
|
64
|
+
* - the call has classes (a call without them is unknown, so a person decides), and none of them
|
|
65
|
+
* is carded: not `autonomy.yolo.card`, not one of its rules named there, not a class in
|
|
66
|
+
* `YOLO_CARDED_SHELL_CLASSES` that `autonomy.yolo.allow` leaves out, and not a rule's bare name
|
|
67
|
+
* (its call is unclassed) that `autonomy.yolo.allow` leaves out.
|
|
68
|
+
* It reports who approved as the speaker, marked `auto: "yolo"`, and tells the modes, which log it.
|
|
69
|
+
*/
|
|
70
|
+
export function yoloApprover(seams: YoloSeams): AutoApprover {
|
|
71
|
+
const { modes, autonomy, channel, speaker } = seams;
|
|
72
|
+
const allow = new Set(autonomy.yolo.allow);
|
|
73
|
+
const card = new Set(autonomy.yolo.card);
|
|
74
|
+
const carded = new Set(YOLO_CARDED_SHELL_CLASSES);
|
|
75
|
+
const approves = (id: string) => {
|
|
76
|
+
if (card.has(id) || card.has(ruleOf(id))) return false;
|
|
77
|
+
// A rule that reports classes has opted in; one that reports none is approved only by name.
|
|
78
|
+
if (unclassed(id)) return allow.has(id);
|
|
79
|
+
return !carded.has(id) || allow.has(id);
|
|
80
|
+
};
|
|
81
|
+
return (call: HeldCall, { speakerOnly }): ApprovedBy | undefined => {
|
|
82
|
+
if (!modes.peek(channel).yolo) return undefined;
|
|
83
|
+
if (speaker.tier !== "owner" || speaker.principalId === SYSTEM_PRINCIPAL)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (call.approverIds !== undefined || speakerOnly) return undefined;
|
|
86
|
+
const classes = call.holdClasses;
|
|
87
|
+
if (!classes || classes.length === 0 || !classes.every(approves))
|
|
88
|
+
return undefined;
|
|
89
|
+
modes.autoApproved({
|
|
90
|
+
channel,
|
|
91
|
+
tool: call.tool,
|
|
92
|
+
action: call.action,
|
|
93
|
+
classes: [...classes],
|
|
94
|
+
principalId: speaker.principalId,
|
|
95
|
+
at: (seams.now ?? (() => new Date()))(),
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
principalId: speaker.principalId,
|
|
99
|
+
speakerId: speaker.id,
|
|
100
|
+
name: speaker.name ?? speaker.id,
|
|
101
|
+
owner: true,
|
|
102
|
+
auto: "yolo",
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -31,6 +31,7 @@ import { ConversationSessions } from "./conversation-sessions.ts";
|
|
|
31
31
|
import { confirmedTurnText } from "./extensions/confirmation-gate.ts";
|
|
32
32
|
import { loadsMemory } from "./extensions/private-memory.ts";
|
|
33
33
|
import { missingToolsError } from "./extensions/self-compact-guard.ts";
|
|
34
|
+
import { turnYolo } from "./extensions/yolo.ts";
|
|
34
35
|
import { interimPoster } from "./interim-text.ts";
|
|
35
36
|
import { preflightTools } from "./preflight-tools.ts";
|
|
36
37
|
import { PromptSlot } from "./prompt-slot.ts";
|
|
@@ -207,11 +208,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
207
208
|
const missing = wanted.filter((name) => !registered.has(name));
|
|
208
209
|
if (missing.length > 0) {
|
|
209
210
|
logger.warn(
|
|
210
|
-
{
|
|
211
|
-
channel: request.channel,
|
|
212
|
-
selection: request.selection.id,
|
|
213
|
-
missing,
|
|
214
|
-
},
|
|
211
|
+
{ channel: request.channel, selection: request.selection.id, missing },
|
|
215
212
|
"selected tools missing; running without them",
|
|
216
213
|
);
|
|
217
214
|
}
|
|
@@ -223,6 +220,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
223
220
|
who,
|
|
224
221
|
request.speaker,
|
|
225
222
|
conversation.visibility === "private",
|
|
223
|
+
turnYolo(this.#options, request),
|
|
226
224
|
);
|
|
227
225
|
if (request.confirmed && pending && pending.calls.length > 0) {
|
|
228
226
|
logger.info(
|
|
@@ -17,8 +17,9 @@ import {
|
|
|
17
17
|
type ModelRef,
|
|
18
18
|
type ThinkingLevel,
|
|
19
19
|
} from "../models.ts";
|
|
20
|
+
import type { Autonomy } from "../modes/autonomy.ts";
|
|
20
21
|
import type { PluginContext, RoundtablePlugin } from "../plugin.ts";
|
|
21
|
-
import { CONVERSATIONS, IDENTITY, RUNTIME } from "../services.ts";
|
|
22
|
+
import { CONVERSATIONS, IDENTITY, MODES, RUNTIME } from "../services.ts";
|
|
22
23
|
import type { Speaker } from "../speakers.ts";
|
|
23
24
|
import {
|
|
24
25
|
BRIDGE_MEMORY_FIX,
|
|
@@ -28,6 +29,9 @@ import {
|
|
|
28
29
|
} from "./bridge-guard.ts";
|
|
29
30
|
import { PendingConfirmationStore } from "./pending-confirmation-store.ts";
|
|
30
31
|
import { PiAgentRuntime } from "./pi-agent-runtime.ts";
|
|
32
|
+
import type { PiAgentRuntimeOptions } from "./runtime-types.ts";
|
|
33
|
+
|
|
34
|
+
const NO_MODES = { yolo: false, longRunning: false } as const;
|
|
31
35
|
|
|
32
36
|
/** The name of the runtime plugin, as `serviceStarted` events and the migration ledger name it. */
|
|
33
37
|
export const RUNTIME_PLUGIN = "runtime";
|
|
@@ -82,6 +86,35 @@ export interface RuntimePluginOptions {
|
|
|
82
86
|
memory?: boolean;
|
|
83
87
|
/** Why the configuration lets several people speak in a shared conversation, as `configuredCrowd` says. */
|
|
84
88
|
crowd?: string;
|
|
89
|
+
/** A turn's budget, in ms; see `PiAgentRuntimeOptions.turnTimeoutMs`. */
|
|
90
|
+
turnTimeoutMs?: number;
|
|
91
|
+
/** The most one turn works in all, in ms; see `PiAgentRuntimeOptions.turnMaxMs`. */
|
|
92
|
+
turnMaxMs?: number;
|
|
93
|
+
/** How long the card at a turn's limit waits, in ms; see `PiAgentRuntimeOptions.turnTimeoutCardMs`. */
|
|
94
|
+
turnTimeoutCardMs?: number;
|
|
95
|
+
/** How far yolo and long-running reach; the modes themselves are the `MODES` service's. */
|
|
96
|
+
autonomy?: Autonomy;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The turn limits and autonomy the plugin's options set, as the runtime's options name them. */
|
|
100
|
+
export function turnPolicy(
|
|
101
|
+
options: RuntimePluginOptions,
|
|
102
|
+
): Pick<
|
|
103
|
+
PiAgentRuntimeOptions,
|
|
104
|
+
"turnTimeoutMs" | "turnMaxMs" | "turnTimeoutCardMs" | "autonomy"
|
|
105
|
+
> {
|
|
106
|
+
return {
|
|
107
|
+
...(options.turnTimeoutMs === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { turnTimeoutMs: options.turnTimeoutMs }),
|
|
110
|
+
...(options.turnMaxMs === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { turnMaxMs: options.turnMaxMs }),
|
|
113
|
+
...(options.turnTimeoutCardMs === undefined
|
|
114
|
+
? {}
|
|
115
|
+
: { turnTimeoutCardMs: options.turnTimeoutCardMs }),
|
|
116
|
+
...(options.autonomy ? { autonomy: options.autonomy } : {}),
|
|
117
|
+
};
|
|
85
118
|
}
|
|
86
119
|
|
|
87
120
|
/**
|
|
@@ -157,6 +190,14 @@ export function runtimePlugin(
|
|
|
157
190
|
},
|
|
158
191
|
principalOf: async (id) => services.find(IDENTITY)?.principal(id),
|
|
159
192
|
memory: options.memory === true,
|
|
193
|
+
// Read at every turn, after every plugin set up; a host without the modes has none on.
|
|
194
|
+
modes: {
|
|
195
|
+
peek: (channel) =>
|
|
196
|
+
services.find(MODES)?.peek(channel) ?? NO_MODES,
|
|
197
|
+
autoApproved: (entry) =>
|
|
198
|
+
services.find(MODES)?.autoApproved(entry),
|
|
199
|
+
},
|
|
200
|
+
...turnPolicy(options),
|
|
160
201
|
sessions: context.sessions,
|
|
161
202
|
agentDir: options.agentDir,
|
|
162
203
|
modelRuntime: options.modelRuntime,
|
|
@@ -14,6 +14,8 @@ import type { OwnerIdentity } from "../identity.ts";
|
|
|
14
14
|
import type { PromptScope, Prompts } from "../interactions/prompts.ts";
|
|
15
15
|
import type { Logger } from "../log.ts";
|
|
16
16
|
import type { EffortPolicy, ThinkingLevel, ThinkingPicker } from "../models.ts";
|
|
17
|
+
import type { Autonomy } from "../modes/autonomy.ts";
|
|
18
|
+
import type { ConversationModes } from "../modes/conversation-modes.ts";
|
|
17
19
|
import type { LinkedSessions } from "../plugin.ts";
|
|
18
20
|
import {
|
|
19
21
|
planOrder,
|
|
@@ -108,6 +110,15 @@ export interface PiAgentRuntimeOptions {
|
|
|
108
110
|
* no answer by then the turn is aborted. A turn that cannot show a card is aborted at once.
|
|
109
111
|
*/
|
|
110
112
|
turnTimeoutCardMs?: number;
|
|
113
|
+
/**
|
|
114
|
+
* The conversations' modes, read at each turn: a turn in a long-running conversation goes on
|
|
115
|
+
* past its time limit without a card, up to `autonomy.longRunningMaxMs` of work, and a turn in
|
|
116
|
+
* a yolo conversation has the held calls `autonomy.yolo` allows approved without a card
|
|
117
|
+
* (`autoApproved` is told of each). Without it no conversation has either mode.
|
|
118
|
+
*/
|
|
119
|
+
modes?: Pick<ConversationModes, "peek" | "autoApproved">;
|
|
120
|
+
/** How far the modes reach; default `DEFAULT_AUTONOMY`. */
|
|
121
|
+
autonomy?: Autonomy;
|
|
111
122
|
/**
|
|
112
123
|
* How long a turn that was aborted, by its timeout or a stop, gets to end before its session is
|
|
113
124
|
* disposed of and the turn is reported failed; default 15 seconds. A model that ignores the
|
|
@@ -21,9 +21,11 @@ export interface TurnBudget {
|
|
|
21
21
|
renew(): number | undefined;
|
|
22
22
|
/**
|
|
23
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.
|
|
24
|
+
* does; with `raiseCap` the cap moves too, to a full budget from now. With `ceilingMs`
|
|
25
|
+
* the limit never lands past that much work: a turn that reaches it fires there, as at its cap,
|
|
26
|
+
* whatever is left of its budget. A later `extend` without one lifts the ceiling.
|
|
25
27
|
*/
|
|
26
|
-
extend(raiseCap: boolean): void;
|
|
28
|
+
extend(raiseCap: boolean, ceilingMs?: number): void;
|
|
27
29
|
/** Whether the turn has worked up to its cap, or a limit fired and was not extended. */
|
|
28
30
|
capped(): boolean;
|
|
29
31
|
/** The turn's work so far, not counting time the slot spent waiting on cards. */
|
|
@@ -72,6 +74,8 @@ export function turnBudget(
|
|
|
72
74
|
return now - started - slot.waitedMs(now);
|
|
73
75
|
};
|
|
74
76
|
let cap = maxMs;
|
|
77
|
+
// The work time an extension was told not to pass; none until one is.
|
|
78
|
+
let ceiling = Number.POSITIVE_INFINITY;
|
|
75
79
|
let deadline = Math.min(ms, cap);
|
|
76
80
|
let cancelTimer: (() => void) | undefined;
|
|
77
81
|
let expired = false;
|
|
@@ -81,6 +85,7 @@ export function turnBudget(
|
|
|
81
85
|
if (left > 0) return arm(Math.max(left, REARM_FLOOR_MS));
|
|
82
86
|
expired = true;
|
|
83
87
|
if (deadline >= cap) expire("turnMax", cap);
|
|
88
|
+
else if (deadline >= ceiling) expire("turnMax", ceiling);
|
|
84
89
|
else expire("turn", ms);
|
|
85
90
|
}, delay);
|
|
86
91
|
};
|
|
@@ -89,15 +94,16 @@ export function turnBudget(
|
|
|
89
94
|
renew() {
|
|
90
95
|
const now = worked();
|
|
91
96
|
if (expired || now >= cap) return undefined;
|
|
92
|
-
deadline = Math.min(now + ms, cap);
|
|
97
|
+
deadline = Math.min(now + ms, cap, ceiling);
|
|
93
98
|
cancelTimer?.();
|
|
94
99
|
arm(deadline - now);
|
|
95
100
|
return deadline - now;
|
|
96
101
|
},
|
|
97
|
-
extend(raiseCap) {
|
|
102
|
+
extend(raiseCap, ceilingMs) {
|
|
98
103
|
const now = worked();
|
|
99
104
|
if (raiseCap) cap = Math.max(cap, now + ms);
|
|
100
|
-
|
|
105
|
+
ceiling = ceilingMs ?? Number.POSITIVE_INFINITY;
|
|
106
|
+
deadline = Math.min(now + ms, cap, ceiling);
|
|
101
107
|
expired = false;
|
|
102
108
|
cancelTimer?.();
|
|
103
109
|
arm(Math.max(deadline - now, REARM_FLOOR_MS));
|
|
@@ -112,6 +118,17 @@ export function turnBudget(
|
|
|
112
118
|
export interface TurnBudgetSeams {
|
|
113
119
|
activity: Pick<ActivityTracker, "now">;
|
|
114
120
|
clock?: BudgetClock;
|
|
121
|
+
/**
|
|
122
|
+
* Asked when a limit fires, with the turn's work so far: true goes on without a card, as the
|
|
123
|
+
* owner's Continue would (a long-running conversation, up to its own cap); false takes the
|
|
124
|
+
* limit's usual path, a card or an abort. It also answers for a turn that cannot show a card.
|
|
125
|
+
*/
|
|
126
|
+
keepGoing?: (workedMs: number) => boolean;
|
|
127
|
+
/**
|
|
128
|
+
* The work time, in ms, a `keepGoing` extension never carries the turn past: the limit then
|
|
129
|
+
* fires at it, as the turn's cap, so a long time limit cannot overshoot the long-running cap.
|
|
130
|
+
*/
|
|
131
|
+
ceilingMs?: number;
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
/**
|
|
@@ -121,6 +138,7 @@ export interface TurnBudgetSeams {
|
|
|
121
138
|
* to go on: Continue extends the limit that fired by a full budget from then, Cancel aborts, and
|
|
122
139
|
* no answer within `turnTimeoutCardMs` (default 5 minutes, wall time) aborts. The card closes
|
|
123
140
|
* when the turn ends first. Without a card the turn is aborted at once. `abort` is told why.
|
|
141
|
+
* When `seams.keepGoing` says so, the limit is extended server-side first and neither happens.
|
|
124
142
|
*/
|
|
125
143
|
export function runtimeTurnBudget(
|
|
126
144
|
options: Pick<
|
|
@@ -172,6 +190,14 @@ export function runtimeTurnBudget(
|
|
|
172
190
|
turnMaxMs,
|
|
173
191
|
slot,
|
|
174
192
|
(limit, limitMs) => {
|
|
193
|
+
if (seams.keepGoing?.(budget.workedMs())) {
|
|
194
|
+
budget.extend(limit === "turnMax", seams.ceilingMs);
|
|
195
|
+
logger.info(
|
|
196
|
+
{ channel, limit, workedMs: budget.workedMs() },
|
|
197
|
+
"long-running mode continued the turn",
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
175
201
|
const prompts = slot.cards;
|
|
176
202
|
if (!prompts) return stop(reason(limit, limitMs, "timeout"));
|
|
177
203
|
const m = messages();
|
|
@@ -230,6 +256,7 @@ export function runtimeTurnBudget(
|
|
|
230
256
|
undefined,
|
|
231
257
|
{
|
|
232
258
|
supersede: controller.signal,
|
|
259
|
+
purpose: "turn-timeout",
|
|
233
260
|
wording: {
|
|
234
261
|
approve: m.turnTimeoutContinue,
|
|
235
262
|
decline: m.turnTimeoutCancel,
|
|
@@ -3,8 +3,10 @@ import type { TurnResult } from "../domain/conversation.ts";
|
|
|
3
3
|
import { AgentRunError } from "../domain/errors.ts";
|
|
4
4
|
import { type TurnTimeout, TurnTimeoutError } from "../domain/turn-timeout.ts";
|
|
5
5
|
import { catalogFor } from "../i18n/index.ts";
|
|
6
|
+
import { DEFAULT_AUTONOMY } from "../modes/autonomy.ts";
|
|
6
7
|
import type { ChannelKey } from "../sessions.ts";
|
|
7
8
|
import type { PromptSlot } from "./prompt-slot.ts";
|
|
9
|
+
import type { PiAgentRuntimeOptions } from "./runtime-types.ts";
|
|
8
10
|
import { activityTracker } from "./turn-activity.ts";
|
|
9
11
|
import { runtimeTurnBudget, type TurnBudget } from "./turn-budget.ts";
|
|
10
12
|
|
|
@@ -30,13 +32,22 @@ export function timeoutMessage(timeout: TurnTimeout): string {
|
|
|
30
32
|
return catalogFor("en").turnTimedOutNotice(timeout);
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* The time limit of a turn of the built-in runtime; see `runtimeTurnBudget`. `abort` aborts the
|
|
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`.
|
|
39
|
+
*/
|
|
34
40
|
export function turnLimits(
|
|
35
|
-
options: Parameters<typeof runtimeTurnBudget>[0]
|
|
41
|
+
options: Parameters<typeof runtimeTurnBudget>[0] &
|
|
42
|
+
Pick<PiAgentRuntimeOptions, "modes" | "autonomy">,
|
|
36
43
|
slot: Pick<PromptSlot, "waitedMs" | "cards">,
|
|
37
44
|
channel: ChannelKey,
|
|
38
45
|
abort: () => void,
|
|
39
46
|
): TurnLimits {
|
|
47
|
+
const { longRunningMaxMs } = options.autonomy ?? DEFAULT_AUTONOMY;
|
|
48
|
+
const keepGoing = (workedMs: number) =>
|
|
49
|
+
workedMs < longRunningMaxMs &&
|
|
50
|
+
options.modes?.peek(channel).longRunning === true;
|
|
40
51
|
const activity = activityTracker();
|
|
41
52
|
let timedOut: TurnTimeout | undefined;
|
|
42
53
|
const budget = runtimeTurnBudget(
|
|
@@ -47,7 +58,7 @@ export function turnLimits(
|
|
|
47
58
|
timedOut = timeout;
|
|
48
59
|
abort();
|
|
49
60
|
},
|
|
50
|
-
{ activity },
|
|
61
|
+
{ activity, keepGoing, ceilingMs: longRunningMaxMs },
|
|
51
62
|
);
|
|
52
63
|
return {
|
|
53
64
|
budget,
|