pi-roundtable 0.7.15 → 0.7.17
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 +36 -0
- package/docs/plugins.md +94 -4
- package/package.json +2 -1
- package/src/core/agents/agent-ports.ts +9 -0
- package/src/core/agents/agent-prompt.ts +3 -1
- package/src/core/agents/agent-team.ts +2 -0
- package/src/core/agents/team-options.ts +2 -0
- package/src/core/agents/team-turn-types.ts +1 -1
- package/src/core/agents/team-turns.ts +24 -0
- package/src/core/builtin/agent-server.ts +37 -0
- package/src/core/config/config.ts +30 -3
- package/src/core/contract/runtime.ts +5 -0
- package/src/core/contract/surface.ts +8 -0
- package/src/core/define-roundtable.ts +3 -0
- package/src/core/discord/agent-discord.ts +33 -0
- package/src/core/discord/discord-surface.ts +20 -0
- package/src/core/domain/interim.ts +16 -0
- package/src/core/domain/ports.ts +6 -0
- package/src/core/holds.ts +2 -0
- package/src/core/modules/host-shell/git-push.ts +112 -0
- package/src/core/modules/host-shell/shell-paths.ts +160 -0
- package/src/core/modules/host-shell/shell-policy.ts +111 -48
- package/src/core/routing/conversation-turns.ts +3 -0
- package/src/core/routing/surface-port.ts +1 -0
- package/src/core/runtime/conversation-sessions.ts +7 -2
- package/src/core/runtime/extensions/ask-user.ts +1 -1
- package/src/core/runtime/extensions/confirmation-gate.ts +6 -7
- package/src/core/runtime/interim-text.ts +207 -0
- package/src/core/runtime/pi-agent-runtime.ts +13 -2
- package/src/core/runtime/prompt-slot.ts +13 -2
- package/src/core/runtime/runtime-types.ts +8 -0
- package/src/core/runtime/session-factory.ts +16 -0
- package/src/index.ts +5 -0
- package/src/kit/index.ts +20 -0
- package/src/kit/jev.ts +357 -0
- package/src/kit/shell.ts +2 -0
|
@@ -82,6 +82,8 @@ export function conversationTurns(
|
|
|
82
82
|
const hideStop = surfaces.showStop(channel);
|
|
83
83
|
const turn = { kind, channel, speaker };
|
|
84
84
|
events.turnStarted(turn);
|
|
85
|
+
// A claim that posts its own reply formats its own text, so only the default reply posts as it goes.
|
|
86
|
+
const interim = input.reply ? undefined : surfaces.interim(channel);
|
|
85
87
|
let result: TurnResult;
|
|
86
88
|
try {
|
|
87
89
|
result = await settleTurn(
|
|
@@ -102,6 +104,7 @@ export function conversationTurns(
|
|
|
102
104
|
...(input.confirmed ? { confirmed: true } : {}),
|
|
103
105
|
...(input.steerable ? { steerable: true } : {}),
|
|
104
106
|
...(input.interactive ? { interactive: true } : {}),
|
|
107
|
+
...(interim ? { interim } : {}),
|
|
105
108
|
}),
|
|
106
109
|
),
|
|
107
110
|
"conversation turn",
|
|
@@ -41,5 +41,6 @@ export function surfacePort(linked: () => readonly ChatSurface[]): SurfacePort {
|
|
|
41
41
|
unreact: async (channel, messageId, emoji) =>
|
|
42
42
|
of(channel)?.unreact?.(channel, messageId, emoji),
|
|
43
43
|
prompts: (channel, speaker) => of(channel)?.prompts?.(channel, speaker),
|
|
44
|
+
interim: (channel) => of(channel)?.interim?.(channel),
|
|
44
45
|
};
|
|
45
46
|
}
|
|
@@ -63,12 +63,17 @@ export class ConversationSessions {
|
|
|
63
63
|
async gate(key: ChannelKey, agent: boolean): Promise<ConfirmationGate> {
|
|
64
64
|
let gate = this.#gates.get(key);
|
|
65
65
|
if (!gate) {
|
|
66
|
-
const
|
|
66
|
+
const agents = agent ? this.#options.agents : undefined;
|
|
67
67
|
gate = new ConfirmationGate(
|
|
68
68
|
this.#factory.link().holds,
|
|
69
69
|
this.#options.owner,
|
|
70
70
|
await this.#options.confirmations.load(key),
|
|
71
|
-
|
|
71
|
+
agents
|
|
72
|
+
? {
|
|
73
|
+
workspace: agents.workDir,
|
|
74
|
+
...(agents.scratchDir ? { scratchDir: agents.scratchDir } : {}),
|
|
75
|
+
}
|
|
76
|
+
: {},
|
|
72
77
|
this.#tiers,
|
|
73
78
|
);
|
|
74
79
|
this.#gates.set(key, gate);
|
|
@@ -37,7 +37,7 @@ export function askUserExtension(
|
|
|
37
37
|
pi.registerTool({
|
|
38
38
|
name: ASK_USER_TOOL,
|
|
39
39
|
label: `Ask ${o.name}`,
|
|
40
|
-
description: `Ask ${o.name} a question on a card with buttons in this channel and wait for ${o.his} answer, within this turn. Use it when you need ${o.his} decision to continue, instead of ending your turn with a question. Offer options when the choices are known; set multi when several may apply and allow_other to let ${o.him} write ${o.his} own answer. ${o.He} has 30 minutes; after that the result says there was no answer.`,
|
|
40
|
+
description: `Ask ${o.name} a question on a card with buttons in this channel and wait for ${o.his} answer, within this turn. Use it when you need ${o.his} decision to continue, instead of ending your turn with a question. The card should read on its own, so put the context ${o.he} needs in the question too. Offer options when the choices are known; set multi when several may apply and allow_other to let ${o.him} write ${o.his} own answer. ${o.He} has 30 minutes; after that the result says there was no answer.`,
|
|
41
41
|
parameters: Type.Object(
|
|
42
42
|
{
|
|
43
43
|
question: Type.String({ minLength: 1, maxLength: 1500 }),
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
PendingConfirmation,
|
|
5
5
|
} from "../../domain/conversation.ts";
|
|
6
6
|
import type { OwnerPrompts } from "../../domain/owner-prompts.ts";
|
|
7
|
-
import { type HoldCheck, higherTier } from "../../holds.ts";
|
|
7
|
+
import { type HoldCheck, type HoldContext, higherTier } from "../../holds.ts";
|
|
8
8
|
import { messages } from "../../i18n/index.ts";
|
|
9
9
|
import { type OwnerIdentity, ownerWords } from "../../identity.ts";
|
|
10
10
|
import type { ToolTiers } from "../../tool-tiers.ts";
|
|
@@ -60,18 +60,18 @@ export class ConfirmationGate {
|
|
|
60
60
|
readonly #owner: OwnerIdentity;
|
|
61
61
|
/** Whom the running turn's held actions and refusals speak of: the turn's speaker. */
|
|
62
62
|
#addressee: OwnerIdentity;
|
|
63
|
-
readonly #
|
|
63
|
+
readonly #context: HoldContext;
|
|
64
64
|
readonly #tiers: ToolTiers | undefined;
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* `holds` decides which calls wait for the owner. With a workspace, the session has a shell,
|
|
68
|
-
* and its writes outside the workspace are held too.
|
|
68
|
+
* and its writes outside the workspace and the context's scratch dir are held too.
|
|
69
69
|
*/
|
|
70
70
|
constructor(
|
|
71
71
|
holds: HoldCheck,
|
|
72
72
|
owner: OwnerIdentity,
|
|
73
73
|
pending?: PendingConfirmation,
|
|
74
|
-
|
|
74
|
+
context: HoldContext = {},
|
|
75
75
|
tiers?: ToolTiers,
|
|
76
76
|
) {
|
|
77
77
|
this.#tiers = tiers;
|
|
@@ -79,7 +79,7 @@ export class ConfirmationGate {
|
|
|
79
79
|
this.#owner = owner;
|
|
80
80
|
this.#addressee = owner;
|
|
81
81
|
this.#pending = pending;
|
|
82
|
-
this.#
|
|
82
|
+
this.#context = context;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/** Held actions awaiting the owner's answer; expired ones count as none. */
|
|
@@ -156,8 +156,7 @@ export class ConfirmationGate {
|
|
|
156
156
|
|
|
157
157
|
/** The call when it needs the owner's confirmation and no approval releases it. */
|
|
158
158
|
#needing(tool: string, input: Record<string, unknown>): HeldCall | undefined {
|
|
159
|
-
const context =
|
|
160
|
-
this.#workspace === undefined ? {} : { workspace: this.#workspace };
|
|
159
|
+
const context = this.#context;
|
|
161
160
|
const action = this.#holds(tool, input, context);
|
|
162
161
|
if (!action) return undefined;
|
|
163
162
|
const minTier = this.#holds.approvalTier?.(tool, input, context);
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { InterimMessage, InterimPosts } from "../domain/interim.ts";
|
|
2
|
+
import type { TurnRequest } from "../domain/ports.ts";
|
|
3
|
+
import type { Logger } from "../log.ts";
|
|
4
|
+
import {
|
|
5
|
+
DISCORD_MESSAGE_LIMIT,
|
|
6
|
+
splitReply,
|
|
7
|
+
} from "../presentation/reply-splitter.ts";
|
|
8
|
+
import { textOf } from "../shared/session-messages.ts";
|
|
9
|
+
import type { PiAgentRuntimeOptions } from "./runtime-types.ts";
|
|
10
|
+
|
|
11
|
+
/** An intermediate text this long or longer is posted as an ordinary message. */
|
|
12
|
+
export const PRIMARY_CHARS = 400;
|
|
13
|
+
/** The progress message is edited at most this often; its last state always lands. */
|
|
14
|
+
export const PROGRESS_EDIT_MS = 1_500;
|
|
15
|
+
|
|
16
|
+
const STRUCTURE = /^\s{0,3}(#{1,6}\s|```|~~~|[-*+]\s|\d+[.)]\s|\|.*\|)/m;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether an intermediate text is content the owner must read, such as a proposal or findings:
|
|
20
|
+
* long, or written with a Markdown heading, list, table, or code fence. Shorter narration
|
|
21
|
+
* between tool calls goes to the progress message instead.
|
|
22
|
+
*/
|
|
23
|
+
export function isPrimaryText(
|
|
24
|
+
text: string,
|
|
25
|
+
primaryChars = PRIMARY_CHARS,
|
|
26
|
+
): boolean {
|
|
27
|
+
return text.length >= primaryChars || STRUCTURE.test(text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A run's progress message: its narration lines, then the tools it called, all in Discord's
|
|
32
|
+
* small text and kept inside one message by dropping the oldest lines behind a "…" marker.
|
|
33
|
+
*/
|
|
34
|
+
export function progressText(
|
|
35
|
+
lines: readonly string[],
|
|
36
|
+
tools: ReadonlyMap<string, number>,
|
|
37
|
+
): string {
|
|
38
|
+
const small = (line: string) => `-# ${line}`;
|
|
39
|
+
const toolLine =
|
|
40
|
+
tools.size > 0
|
|
41
|
+
? small(
|
|
42
|
+
[...tools]
|
|
43
|
+
.map(([name, count]) => (count > 1 ? `${name} ×${count}` : name))
|
|
44
|
+
.join(" · "),
|
|
45
|
+
).slice(0, DISCORD_MESSAGE_LIMIT)
|
|
46
|
+
: undefined;
|
|
47
|
+
const body = lines
|
|
48
|
+
.flatMap((text) => text.split("\n"))
|
|
49
|
+
.map((line) => line.trim())
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.map(small);
|
|
52
|
+
const tail = toolLine ? [toolLine] : [];
|
|
53
|
+
const fits = (kept: string[]) =>
|
|
54
|
+
[...kept, ...tail].join("\n").length <= DISCORD_MESSAGE_LIMIT;
|
|
55
|
+
if (fits(body)) return [...body, ...tail].join("\n");
|
|
56
|
+
const marker = small("…");
|
|
57
|
+
let kept = body;
|
|
58
|
+
while (kept.length > 0 && !fits([marker, ...kept])) kept = kept.slice(1);
|
|
59
|
+
if (kept.length === 0 && body.length > 0) {
|
|
60
|
+
// One line alone is too long: keep its end.
|
|
61
|
+
const room =
|
|
62
|
+
DISCORD_MESSAGE_LIMIT - [marker, ...tail].join("\n").length - 4;
|
|
63
|
+
const last = body.at(-1) ?? "";
|
|
64
|
+
kept = room > 0 ? [small(`…${last.slice(-room)}`)] : [];
|
|
65
|
+
return [...kept, ...tail].join("\n");
|
|
66
|
+
}
|
|
67
|
+
return [marker, ...kept, ...tail].join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface Run {
|
|
71
|
+
lines: string[];
|
|
72
|
+
tools: Map<string, number>;
|
|
73
|
+
message?: InterimMessage;
|
|
74
|
+
shown?: string;
|
|
75
|
+
lastAt: number;
|
|
76
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface InterimPosterOptions {
|
|
80
|
+
logger: Logger;
|
|
81
|
+
channel: string;
|
|
82
|
+
primaryChars?: number;
|
|
83
|
+
editMs?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Posts a turn's intermediate text as it goes. A tool-calling assistant message whose text is
|
|
88
|
+
* primary is posted as ordinary messages; shorter text joins the run's progress message with
|
|
89
|
+
* the tools called, edited in place. A primary post or a card starts a new run. Posts go out
|
|
90
|
+
* in order, and a failed one is logged, never thrown.
|
|
91
|
+
*/
|
|
92
|
+
export class InterimPoster {
|
|
93
|
+
readonly #posts: InterimPosts;
|
|
94
|
+
readonly #options: InterimPosterOptions;
|
|
95
|
+
#queue: Promise<void> = Promise.resolve();
|
|
96
|
+
#run: Run = newRun();
|
|
97
|
+
|
|
98
|
+
constructor(posts: InterimPosts, options: InterimPosterOptions) {
|
|
99
|
+
this.#posts = posts;
|
|
100
|
+
this.#options = options;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A finished message of the turn; only a tool-calling assistant message is posted. */
|
|
104
|
+
messageEnd(message: {
|
|
105
|
+
role: string;
|
|
106
|
+
content?: unknown;
|
|
107
|
+
stopReason?: string;
|
|
108
|
+
}): void {
|
|
109
|
+
if (message.role !== "assistant" || message.stopReason !== "toolUse")
|
|
110
|
+
return;
|
|
111
|
+
const text = textOf(message.content).trim();
|
|
112
|
+
if (!text) return;
|
|
113
|
+
if (isPrimaryText(text, this.#options.primaryChars)) {
|
|
114
|
+
this.#endRun();
|
|
115
|
+
for (const chunk of splitReply(text))
|
|
116
|
+
this.#enqueue(async () => {
|
|
117
|
+
await this.#posts.post(chunk);
|
|
118
|
+
}, "interim text not posted");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.#run.lines.push(text);
|
|
122
|
+
this.#changed();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A tool the turn started, counted on the run's tool line. */
|
|
126
|
+
toolStart(name: string): void {
|
|
127
|
+
const { tools } = this.#run;
|
|
128
|
+
tools.set(name, (tools.get(name) ?? 0) + 1);
|
|
129
|
+
this.#changed();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Brings every post up to date and waits for them, then starts a new run: before a card, so
|
|
134
|
+
* the text written before it shows above it, and when the turn ends, before its final reply.
|
|
135
|
+
*/
|
|
136
|
+
async flush(): Promise<void> {
|
|
137
|
+
this.#endRun();
|
|
138
|
+
await this.#queue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#endRun(): void {
|
|
142
|
+
const run = this.#run;
|
|
143
|
+
this.#run = newRun();
|
|
144
|
+
clearTimeout(run.timer);
|
|
145
|
+
run.timer = undefined;
|
|
146
|
+
this.#render(run);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** The run changed: shown now, or once the edit interval has passed. */
|
|
150
|
+
#changed(): void {
|
|
151
|
+
const run = this.#run;
|
|
152
|
+
if (run.timer) return;
|
|
153
|
+
const wait =
|
|
154
|
+
run.lastAt + (this.#options.editMs ?? PROGRESS_EDIT_MS) - now();
|
|
155
|
+
if (wait <= 0) {
|
|
156
|
+
this.#render(run);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
run.timer = setTimeout(() => {
|
|
160
|
+
run.timer = undefined;
|
|
161
|
+
this.#render(run);
|
|
162
|
+
}, wait);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#render(run: Run): void {
|
|
166
|
+
if (run.lines.length === 0 && run.tools.size === 0) return;
|
|
167
|
+
run.lastAt = now();
|
|
168
|
+
const text = progressText(run.lines, run.tools);
|
|
169
|
+
this.#enqueue(async () => {
|
|
170
|
+
if (text === run.shown) return;
|
|
171
|
+
if (run.message) await run.message.edit(text);
|
|
172
|
+
else run.message = await this.#posts.post(text);
|
|
173
|
+
run.shown = text;
|
|
174
|
+
}, "progress message not posted");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#enqueue(step: () => Promise<void>, failure: string): void {
|
|
178
|
+
const { logger, channel } = this.#options;
|
|
179
|
+
this.#queue = this.#queue.then(step).catch((error: unknown) => {
|
|
180
|
+
logger.warn({ channel, err: error }, failure);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The turn's poster, when its caller gave a place for interim posts and the host left them on. */
|
|
186
|
+
export function interimPoster(
|
|
187
|
+
request: Pick<TurnRequest, "interim" | "channel">,
|
|
188
|
+
options: Pick<
|
|
189
|
+
PiAgentRuntimeOptions,
|
|
190
|
+
"interimText" | "interimPrimaryChars" | "logger"
|
|
191
|
+
>,
|
|
192
|
+
): InterimPoster | undefined {
|
|
193
|
+
if (!request.interim || options.interimText === "off") return undefined;
|
|
194
|
+
return new InterimPoster(request.interim, {
|
|
195
|
+
logger: options.logger,
|
|
196
|
+
channel: request.channel,
|
|
197
|
+
...(options.interimPrimaryChars
|
|
198
|
+
? { primaryChars: options.interimPrimaryChars }
|
|
199
|
+
: {}),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function newRun(): Run {
|
|
204
|
+
return { lines: [], tools: new Map(), lastAt: 0 };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const now = () => Date.now();
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
confirmedTurnText,
|
|
37
37
|
} from "./extensions/confirmation-gate.ts";
|
|
38
38
|
import { COMPACT_TOOL } from "./extensions/self-compact-guard.ts";
|
|
39
|
+
import { interimPoster } from "./interim-text.ts";
|
|
39
40
|
import { PromptSlot, workTimeout } from "./prompt-slot.ts";
|
|
40
41
|
import {
|
|
41
42
|
type PiAgentRuntimeOptions,
|
|
@@ -181,9 +182,17 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
181
182
|
// Collected as they end: a compaction during the turn shortens session.messages.
|
|
182
183
|
const turnMessages: TurnMessage[] = [];
|
|
183
184
|
const toolCalls: string[] = [];
|
|
185
|
+
// Text written before the final answer is posted as the turn goes; the final reply stays the caller's.
|
|
186
|
+
const interim = interimPoster(request, this.#options);
|
|
184
187
|
const unsubscribe = session.subscribe((event) => {
|
|
185
|
-
if (event.type === "tool_execution_start")
|
|
186
|
-
|
|
188
|
+
if (event.type === "tool_execution_start") {
|
|
189
|
+
toolCalls.push(event.toolName);
|
|
190
|
+
interim?.toolStart(event.toolName);
|
|
191
|
+
}
|
|
192
|
+
if (event.type === "message_end") {
|
|
193
|
+
turnMessages.push(event.message);
|
|
194
|
+
interim?.messageEnd(event.message);
|
|
195
|
+
}
|
|
187
196
|
});
|
|
188
197
|
const slot = this.#sessions.slot(key);
|
|
189
198
|
slot.bind(
|
|
@@ -191,6 +200,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
191
200
|
? this.#options.prompts?.(request.channel, request.speaker)
|
|
192
201
|
: undefined,
|
|
193
202
|
request.agent?.name ?? assistantName(),
|
|
203
|
+
interim ? () => interim.flush() : undefined,
|
|
194
204
|
);
|
|
195
205
|
// Time spent waiting on the owner's cards does not count towards the timeout.
|
|
196
206
|
const cancelTimeout = workTimeout(turnTimeoutMs, slot, () => {
|
|
@@ -237,6 +247,7 @@ export class PiAgentRuntime implements AgentRuntime {
|
|
|
237
247
|
cancelTimeout();
|
|
238
248
|
slot.unbind();
|
|
239
249
|
unsubscribe();
|
|
250
|
+
await interim?.flush();
|
|
240
251
|
gate.endTurn();
|
|
241
252
|
const usage = session.getContextUsage();
|
|
242
253
|
if (usage)
|
|
@@ -10,14 +10,23 @@ import { assistantName } from "../i18n/index.ts";
|
|
|
10
10
|
*/
|
|
11
11
|
export class PromptSlot {
|
|
12
12
|
#prompts: OwnerPrompts | undefined;
|
|
13
|
+
#beforeCard: (() => Promise<void>) | undefined;
|
|
13
14
|
#asker = assistantName();
|
|
14
15
|
#open = 0;
|
|
15
16
|
#openSince = 0;
|
|
16
17
|
#waited = 0;
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Binds the turn's prompts; `asker` names who asks, the assistant or an agent. `beforeCard`
|
|
21
|
+
* runs before each card opens, such as posting the text the turn wrote before it.
|
|
22
|
+
*/
|
|
23
|
+
bind(
|
|
24
|
+
prompts: OwnerPrompts | undefined,
|
|
25
|
+
asker: string,
|
|
26
|
+
beforeCard?: () => Promise<void>,
|
|
27
|
+
): void {
|
|
20
28
|
this.#prompts = prompts;
|
|
29
|
+
this.#beforeCard = beforeCard;
|
|
21
30
|
this.#asker = asker;
|
|
22
31
|
this.#open = 0;
|
|
23
32
|
this.#waited = 0;
|
|
@@ -25,6 +34,7 @@ export class PromptSlot {
|
|
|
25
34
|
|
|
26
35
|
unbind(): void {
|
|
27
36
|
this.#prompts = undefined;
|
|
37
|
+
this.#beforeCard = undefined;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
get asker(): string {
|
|
@@ -49,6 +59,7 @@ export class PromptSlot {
|
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
async #waiting<T>(ask: () => Promise<T>): Promise<T> {
|
|
62
|
+
await this.#beforeCard?.();
|
|
52
63
|
if (this.#open++ === 0) this.#openSince = Date.now();
|
|
53
64
|
try {
|
|
54
65
|
return await ask();
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type { AgentSessions, LoadedSkill } from "../contract/runtime.ts";
|
|
8
8
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
9
|
+
import type { InterimTextMode } from "../domain/interim.ts";
|
|
9
10
|
import type { OwnerPrompts } from "../domain/owner-prompts.ts";
|
|
10
11
|
import type { OwnerIdentity } from "../identity.ts";
|
|
11
12
|
import type { Logger } from "../log.ts";
|
|
@@ -66,6 +67,13 @@ export interface PiAgentRuntimeOptions {
|
|
|
66
67
|
toolTiers?: ToolTiers;
|
|
67
68
|
/** A run that takes longer, not counting time spent waiting on the owner's cards, is aborted and reported as failed. */
|
|
68
69
|
turnTimeoutMs?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Whether a turn given a place for interim posts shows the text it writes before its final
|
|
72
|
+
* answer as it goes; default "on". "off" posts only the final reply.
|
|
73
|
+
*/
|
|
74
|
+
interimText?: InterimTextMode;
|
|
75
|
+
/** An intermediate text this long or longer is posted as an ordinary message; default 400. */
|
|
76
|
+
interimPrimaryChars?: number;
|
|
69
77
|
/** How long a new session waits for its MCP tools to register. */
|
|
70
78
|
mcpConnectTimeoutMs?: number;
|
|
71
79
|
}
|
|
@@ -3,9 +3,11 @@ import { join } from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
type AgentSession,
|
|
5
5
|
createAgentSession,
|
|
6
|
+
createBashToolDefinition,
|
|
6
7
|
DefaultResourceLoader,
|
|
7
8
|
type ModelRuntime,
|
|
8
9
|
type SessionManager,
|
|
10
|
+
type ToolDefinition,
|
|
9
11
|
} from "@earendil-works/pi-coding-agent";
|
|
10
12
|
import { channelSegment } from "../attachments/attachment-dir.ts";
|
|
11
13
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
@@ -279,6 +281,10 @@ export class SessionFactory {
|
|
|
279
281
|
const { session } = await createAgentSession({
|
|
280
282
|
cwd,
|
|
281
283
|
agentDir,
|
|
284
|
+
// Pi's bash under its own name, run with TMPDIR at the agents' scratch dir.
|
|
285
|
+
...(agents?.scratchDir
|
|
286
|
+
? { customTools: [scratchBash(cwd, agents.scratchDir)] }
|
|
287
|
+
: {}),
|
|
282
288
|
thinkingLevel: thinking,
|
|
283
289
|
modelRuntime: this.#modelRuntime,
|
|
284
290
|
resourceLoader,
|
|
@@ -305,3 +311,13 @@ export class SessionFactory {
|
|
|
305
311
|
return Object.assign(state, { session });
|
|
306
312
|
}
|
|
307
313
|
}
|
|
314
|
+
|
|
315
|
+
/** Pi's bash tool with TMPDIR set to `scratchDir`, so `mktemp` and tools write there. */
|
|
316
|
+
export function scratchBash(cwd: string, scratchDir: string): ToolDefinition {
|
|
317
|
+
return createBashToolDefinition(cwd, {
|
|
318
|
+
spawnHook: (context) => ({
|
|
319
|
+
...context,
|
|
320
|
+
env: { ...context.env, TMPDIR: scratchDir },
|
|
321
|
+
}),
|
|
322
|
+
}) as unknown as ToolDefinition;
|
|
323
|
+
}
|
package/src/index.ts
CHANGED
package/src/kit/index.ts
CHANGED
|
@@ -44,6 +44,24 @@ export {
|
|
|
44
44
|
thinkingLabel,
|
|
45
45
|
} from "./domain.ts";
|
|
46
46
|
export { holdChain } from "./holds.ts";
|
|
47
|
+
export type {
|
|
48
|
+
JevCompactInput,
|
|
49
|
+
JevCompactOptions,
|
|
50
|
+
JevCompactOutcome,
|
|
51
|
+
JevCompactor,
|
|
52
|
+
JevCompactRequest,
|
|
53
|
+
JevExtensionOptions,
|
|
54
|
+
JevSkipReason,
|
|
55
|
+
} from "./jev.ts";
|
|
56
|
+
export {
|
|
57
|
+
isRuleLoad,
|
|
58
|
+
JEV_COMPACTION_ENGINE,
|
|
59
|
+
JEV_GOAL,
|
|
60
|
+
JEV_PREVIOUS_SUMMARY_LIMIT_TOKENS,
|
|
61
|
+
jevCompact,
|
|
62
|
+
jevCompactionExtension,
|
|
63
|
+
jevCompactor,
|
|
64
|
+
} from "./jev.ts";
|
|
47
65
|
export type {
|
|
48
66
|
EffortBrief,
|
|
49
67
|
EffortJudgeOptions,
|
|
@@ -79,8 +97,10 @@ export {
|
|
|
79
97
|
} from "./presentation.ts";
|
|
80
98
|
export { packageDir, serveUnix } from "./process.ts";
|
|
81
99
|
export {
|
|
100
|
+
type PushPolicy,
|
|
82
101
|
SHELL_TOOLS,
|
|
83
102
|
shellHoldRule,
|
|
103
|
+
shellHoldRuleFor,
|
|
84
104
|
} from "./shell.ts";
|
|
85
105
|
export {
|
|
86
106
|
checkRepoName,
|