@tshu-w/pi-agents 0.3.0 → 0.4.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 +84 -0
- package/bin/pd.mjs +17 -31
- package/docs/HOSTS.md +2 -2
- package/docs/PROGRAM.md +17 -28
- package/docs/SPEC.md +26 -37
- package/package.json +3 -2
- package/src/agents/agents.ts +126 -82
- package/src/agents/registry.ts +40 -7
- package/src/agents/tool.ts +31 -87
- package/src/index.ts +5 -3
- package/src/main-agent.md +5 -7
- package/src/main-agent.ts +47 -0
- package/src/output.ts +0 -18
- package/src/programs/loadout.ts +14 -19
- package/src/programs/programs.ts +39 -115
- package/src/programs/scope.ts +1 -1
- package/src/programs/tool.ts +16 -12
- package/src/roots/guard.mjs +5 -0
- package/src/roots/paths.mjs +1 -1
- package/src/ui/message.ts +41 -7
package/src/agents/agents.ts
CHANGED
|
@@ -15,9 +15,12 @@ import { randomUUID } from "node:crypto";
|
|
|
15
15
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { boundText } from "../output.ts";
|
|
18
19
|
import {
|
|
19
20
|
customMessage,
|
|
20
21
|
deliver,
|
|
22
|
+
messageParts,
|
|
23
|
+
type NotifiedResult,
|
|
21
24
|
addProgramSpend,
|
|
22
25
|
addSpend,
|
|
23
26
|
label,
|
|
@@ -36,10 +39,12 @@ import {
|
|
|
36
39
|
type Delivery,
|
|
37
40
|
type Sender,
|
|
38
41
|
type Entry,
|
|
42
|
+
type Notice,
|
|
39
43
|
type Spend,
|
|
40
44
|
type TreeMetadata,
|
|
41
45
|
} from "./registry.ts";
|
|
42
|
-
import {
|
|
46
|
+
import { releaseSession } from "../roots/guard.mjs";
|
|
47
|
+
import { defaultSessionDir, sessionDir as rootSessionDir } from "../roots/paths.mjs";
|
|
43
48
|
|
|
44
49
|
const AGENT_ENTRY = "pi-agents-agent";
|
|
45
50
|
const SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
@@ -64,8 +69,6 @@ export interface InputRecord {
|
|
|
64
69
|
result?: string;
|
|
65
70
|
/** The value submitted with `submit_result`, for an input with a schema. */
|
|
66
71
|
value?: unknown;
|
|
67
|
-
read: boolean;
|
|
68
|
-
notified: boolean;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
export interface AgentRecord {
|
|
@@ -90,15 +93,20 @@ function childSessionDirectory(cwd: string, currentCwd: string, currentSessionDi
|
|
|
90
93
|
return path.basename(current) === "subagents" ? current : path.join(current, "subagents");
|
|
91
94
|
}
|
|
92
95
|
|
|
93
|
-
function
|
|
96
|
+
function writePreparedSession(sessionManager: SessionManager): string {
|
|
94
97
|
const sessionFile = sessionManager.getSessionFile()!;
|
|
95
98
|
mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
96
99
|
// SessionManager flushes itself once it holds an assistant message (a forked context);
|
|
97
|
-
// otherwise write the prepared entries so the
|
|
100
|
+
// otherwise write the prepared entries so the Session can be reopened.
|
|
98
101
|
if (!existsSync(sessionFile)) {
|
|
99
102
|
const entries = [sessionManager.getHeader(), ...sessionManager.getEntries()];
|
|
100
103
|
writeFileSync(sessionFile, `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`, { flag: "wx" });
|
|
101
104
|
}
|
|
105
|
+
return sessionFile;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function persistPreparedSession(sessionManager: SessionManager, cwd: string): SessionManager {
|
|
109
|
+
const sessionFile = writePreparedSession(sessionManager);
|
|
102
110
|
return SessionManager.open(sessionFile, path.dirname(sessionFile), cwd);
|
|
103
111
|
}
|
|
104
112
|
|
|
@@ -147,7 +155,17 @@ export function steerQueued(agent: AgentSession["agent"] | undefined): boolean {
|
|
|
147
155
|
// Pi does not report steers queued by extensions, so a wait checks the queue this often.
|
|
148
156
|
export const STEER_POLL_MS = 250;
|
|
149
157
|
|
|
150
|
-
export const
|
|
158
|
+
export const RESULT_TAG = "agent-result";
|
|
159
|
+
|
|
160
|
+
/** How a wait ended: what it waited for ended, its timeout expired, another message ended it, or it had nothing to wait for. */
|
|
161
|
+
export type WaitEnd = "completed" | "timedOut" | "interrupted" | "nothing";
|
|
162
|
+
|
|
163
|
+
/** The text `wait` returns, naming what it still waits for as `still` does. */
|
|
164
|
+
export function waitText(end: WaitEnd, still: string): string {
|
|
165
|
+
if (end === "nothing") return "Nothing to wait for.";
|
|
166
|
+
if (end === "completed") return "Wait completed.";
|
|
167
|
+
return `${end === "timedOut" ? "Wait timed out." : "Wait interrupted by a new message."} ${still}`;
|
|
168
|
+
}
|
|
151
169
|
|
|
152
170
|
export interface Deferred {
|
|
153
171
|
promise: Promise<void>;
|
|
@@ -205,6 +223,7 @@ interface Input {
|
|
|
205
223
|
user?: boolean;
|
|
206
224
|
/** The Agent that sent it. */
|
|
207
225
|
sender?: Sender;
|
|
226
|
+
notice?: Notice;
|
|
208
227
|
record?: InputRecord;
|
|
209
228
|
permit?: symbol;
|
|
210
229
|
schema?: unknown;
|
|
@@ -214,7 +233,7 @@ interface Input {
|
|
|
214
233
|
silent?: boolean;
|
|
215
234
|
}
|
|
216
235
|
|
|
217
|
-
type Message = Pick<Input, "text" | "user" | "sender">;
|
|
236
|
+
type Message = Pick<Input, "text" | "user" | "sender" | "notice">;
|
|
218
237
|
|
|
219
238
|
interface Turn {
|
|
220
239
|
inputs: Input[];
|
|
@@ -245,7 +264,6 @@ interface Owned {
|
|
|
245
264
|
session?: AgentSession;
|
|
246
265
|
loading?: Promise<AgentSession>;
|
|
247
266
|
prepared?: Prepared;
|
|
248
|
-
waiters: number;
|
|
249
267
|
/** Its turn was stopped: messages from other Agents become writes until its owner's or user's next input. */
|
|
250
268
|
stopped?: boolean;
|
|
251
269
|
}
|
|
@@ -259,23 +277,47 @@ export interface SpawnRequest {
|
|
|
259
277
|
thinkingLevel?: ThinkingLevel;
|
|
260
278
|
}
|
|
261
279
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
280
|
+
/** A notification a wait holds; every wait for its Agent or Program holds it, and the first to end delivers it. */
|
|
281
|
+
interface HeldNotice {
|
|
282
|
+
text: string;
|
|
283
|
+
result: NotifiedResult;
|
|
284
|
+
images?: NonNullable<Notice["images"]>;
|
|
285
|
+
delivered?: boolean;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** A wait in progress: the IDs of the Agents or Programs it waits for, and their notifications it holds. */
|
|
289
|
+
export interface Held {
|
|
290
|
+
ids: Set<string>;
|
|
291
|
+
notices: HeldNotice[];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Has the waits in progress for `id` hold a notification; returns whether any does. */
|
|
295
|
+
export function hold(waits: Held[], id: string, notice: HeldNotice): boolean {
|
|
296
|
+
const holding = waits.filter(({ ids }) => ids.has(id));
|
|
297
|
+
for (const wait of holding) wait.notices.push(notice);
|
|
298
|
+
return holding.length > 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The notifications a wait holds that no other wait has delivered, as one notification, or undefined if none. */
|
|
302
|
+
export function release(wait: Held): { text: string; notice: Notice } | undefined {
|
|
303
|
+
const notices = wait.notices.filter(({ delivered }) => !delivered);
|
|
304
|
+
if (notices.length === 0) return undefined;
|
|
305
|
+
for (const notice of notices) notice.delivered = true;
|
|
306
|
+
const images = notices.flatMap((notice) => notice.images ?? []);
|
|
307
|
+
return { text: notices.map(({ text }) => text).join("\n\n"), notice: { results: notices.map(({ result }) => result), ...(images.length ? { images } : {}) } };
|
|
266
308
|
}
|
|
267
309
|
|
|
268
310
|
export interface WaitOutcome {
|
|
269
|
-
|
|
311
|
+
end: WaitEnd;
|
|
270
312
|
pending: AgentRecord[];
|
|
271
|
-
/** A steer ended the wait. */
|
|
272
|
-
interrupted?: boolean;
|
|
273
313
|
}
|
|
274
314
|
|
|
275
315
|
/** The Agents owned by one loaded Agent. */
|
|
276
316
|
export class Agents {
|
|
277
317
|
/** Waits in progress that keep this Agent's turn `waiting`. */
|
|
278
318
|
waiting = 0;
|
|
319
|
+
/** The notifications held by the waits in progress. */
|
|
320
|
+
private held: Held[] = [];
|
|
279
321
|
private agents = new Map<string, Owned>();
|
|
280
322
|
private change = deferred();
|
|
281
323
|
private closing = false;
|
|
@@ -409,7 +451,6 @@ export class Agents {
|
|
|
409
451
|
},
|
|
410
452
|
queue: [],
|
|
411
453
|
prepared,
|
|
412
|
-
waiters: 0,
|
|
413
454
|
};
|
|
414
455
|
this.agents.set(agent.record.id, agent);
|
|
415
456
|
return agent;
|
|
@@ -450,31 +491,30 @@ export class Agents {
|
|
|
450
491
|
input.submitted = { value: args.value };
|
|
451
492
|
}
|
|
452
493
|
|
|
453
|
-
accept(id: string, text: string, delivery: Delivery, options: { fromOwner: boolean;
|
|
494
|
+
accept(id: string, text: string, delivery: Delivery, options: { fromOwner: boolean; notice?: Notice; sender?: Sender }): { queued: boolean } {
|
|
454
495
|
const agent = this.agent(id);
|
|
455
496
|
if (agent.stopped && !options.fromOwner) delivery = "write";
|
|
456
497
|
if (delivery === "write") {
|
|
457
|
-
this.write(agent, text, options.sender);
|
|
498
|
+
this.write(agent, text, options.sender, options.notice);
|
|
458
499
|
return { queued: false };
|
|
459
500
|
}
|
|
460
501
|
// Notifications keep their owner informed even at the input limit.
|
|
461
|
-
const permit = options.
|
|
462
|
-
const input: Input = { text, sender: options.sender, permit, record: options.fromOwner && !options.
|
|
502
|
+
const permit = options.notice ? undefined : this.reserve();
|
|
503
|
+
const input: Input = { text, sender: options.sender, notice: options.notice, permit, record: options.fromOwner && !options.notice ? this.newRecord(agent) : undefined };
|
|
463
504
|
return { queued: this.add(agent, input, delivery) };
|
|
464
505
|
}
|
|
465
506
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const suspended =
|
|
476
|
-
|
|
477
|
-
let returned = false;
|
|
507
|
+
/**
|
|
508
|
+
* Waits until `targets`, or the Agents with pending inputs, have none; a steer ends it early. Their notifications
|
|
509
|
+
* are held until it ends, then delivered as one.
|
|
510
|
+
*/
|
|
511
|
+
async wait(targets: Owned[] | undefined, timeoutSeconds: number, signal?: AbortSignal, steered?: () => boolean): Promise<WaitOutcome> {
|
|
512
|
+
const unique = [...new Set(targets ?? [...this.agents.values()])].filter((agent) => this.pending(agent));
|
|
513
|
+
if (unique.length === 0) return { end: "nothing", pending: [] };
|
|
514
|
+
const held: Held = { ids: new Set(unique.map((agent) => agent.record.id)), notices: [] };
|
|
515
|
+
this.held.push(held);
|
|
516
|
+
const suspended = this.self.ownerId !== undefined && scheduler.suspend(this.self.rootId, this.self.id);
|
|
517
|
+
this.markWaiting(1);
|
|
478
518
|
let interrupted = false;
|
|
479
519
|
try {
|
|
480
520
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
@@ -489,42 +529,17 @@ export class Agents {
|
|
|
489
529
|
await waitForChange(this.change.promise, steered ? Math.min(remaining, STEER_POLL_MS) : remaining, signal);
|
|
490
530
|
}
|
|
491
531
|
signal?.throwIfAborted();
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
for (const input of replay.get(agent)!) results.push({ agent: agent.record, input, history: true });
|
|
495
|
-
for (const input of agent.record.inputs) {
|
|
496
|
-
if (ended(input) && !input.read) results.push({ agent: agent.record, input, history: false });
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
returned = true;
|
|
500
|
-
return { results, pending: unique.filter((agent) => this.pending(agent)).map((agent) => agent.record), ...(interrupted && { interrupted }) };
|
|
532
|
+
const pending = unique.filter((agent) => this.pending(agent)).map((agent) => agent.record);
|
|
533
|
+
return { end: pending.length === 0 ? "completed" : interrupted ? "interrupted" : "timedOut", pending };
|
|
501
534
|
} finally {
|
|
502
|
-
|
|
535
|
+
this.held.splice(this.held.indexOf(held), 1);
|
|
536
|
+
const released = this.closing ? undefined : release(held);
|
|
537
|
+
if (released) this.notify(released.text, released.notice);
|
|
538
|
+
this.markWaiting(-1);
|
|
503
539
|
if (suspended) await scheduler.resume(this.self.rootId, this.self.id, this.limits.maxConcurrent, signal);
|
|
504
|
-
for (const agent of unique) {
|
|
505
|
-
agent.waiters -= 1;
|
|
506
|
-
let notified = false;
|
|
507
|
-
for (const input of agent.record.inputs) {
|
|
508
|
-
if (!ended(input) || input.notified) continue;
|
|
509
|
-
if (returned) input.notified = notified = true;
|
|
510
|
-
else this.notifyLater(agent, input);
|
|
511
|
-
}
|
|
512
|
-
if (notified) this.persist(agent);
|
|
513
|
-
}
|
|
514
540
|
}
|
|
515
541
|
}
|
|
516
542
|
|
|
517
|
-
markRead(results: WaitResult[]): void {
|
|
518
|
-
const changed = new Set<string>();
|
|
519
|
-
for (const { agent, input, history } of results) {
|
|
520
|
-
if (history || input.read) continue;
|
|
521
|
-
input.read = true;
|
|
522
|
-
changed.add(agent.id);
|
|
523
|
-
}
|
|
524
|
-
for (const id of changed) this.persist(this.agents.get(id)!);
|
|
525
|
-
if (changed.size > 0) treeChanged();
|
|
526
|
-
}
|
|
527
|
-
|
|
528
543
|
/**
|
|
529
544
|
* Stops the current turn and withdraws queued inputs; returns whether there was anything to stop.
|
|
530
545
|
* A turn that has not stopped by `deadline` is abandoned: its inputs end as aborted, and this throws.
|
|
@@ -578,14 +593,13 @@ export class Agents {
|
|
|
578
593
|
records.set(record.id, record);
|
|
579
594
|
}
|
|
580
595
|
for (const record of records.values()) {
|
|
581
|
-
const agent: Owned = { record, queue: []
|
|
596
|
+
const agent: Owned = { record, queue: [] };
|
|
582
597
|
this.agents.set(record.id, agent);
|
|
583
598
|
if (!record.inputs.some((input) => !ended(input))) continue;
|
|
584
599
|
for (const input of record.inputs) {
|
|
585
600
|
if (ended(input)) continue;
|
|
586
601
|
input.state = "aborted";
|
|
587
602
|
input.result = "The process exited before this input ended.";
|
|
588
|
-
input.notified = true;
|
|
589
603
|
}
|
|
590
604
|
this.persist(agent);
|
|
591
605
|
}
|
|
@@ -621,8 +635,8 @@ export class Agents {
|
|
|
621
635
|
if (errors.length > 0) throw new Error(errors.map((error) => error instanceof Error ? error.message : String(error)).join("\n"));
|
|
622
636
|
}
|
|
623
637
|
|
|
624
|
-
private write(agent: Owned, text: string, from?: Sender): void {
|
|
625
|
-
const message = customMessage(text, "write", { sender: from });
|
|
638
|
+
private write(agent: Owned, text: string, from?: Sender, notice?: Notice): void {
|
|
639
|
+
const message = customMessage(text, "write", { sender: from, notice });
|
|
626
640
|
const session = agent.session;
|
|
627
641
|
if (session?.isStreaming) {
|
|
628
642
|
session.agent.steer({ role: "custom", ...message, timestamp: Date.now() });
|
|
@@ -664,9 +678,9 @@ export class Agents {
|
|
|
664
678
|
this.changed();
|
|
665
679
|
if (!turn.started) return;
|
|
666
680
|
if (agent.session?.isStreaming) {
|
|
667
|
-
agent.session.agent.steer({ role: "custom", ...customMessage(input.text, "steer", { user: input.user, sender: input.sender }), timestamp: Date.now() });
|
|
681
|
+
agent.session.agent.steer({ role: "custom", ...customMessage(input.text, "steer", { user: input.user, sender: input.sender, notice: input.notice }), timestamp: Date.now() });
|
|
668
682
|
} else {
|
|
669
|
-
turn.pending.push({ text: input.text, user: input.user, sender: input.sender });
|
|
683
|
+
turn.pending.push({ text: input.text, user: input.user, sender: input.sender, notice: input.notice });
|
|
670
684
|
}
|
|
671
685
|
}
|
|
672
686
|
|
|
@@ -692,7 +706,7 @@ export class Agents {
|
|
|
692
706
|
for (const input of turn.inputs) if (input.record) input.record.state = "running";
|
|
693
707
|
this.persist(agent);
|
|
694
708
|
this.changed();
|
|
695
|
-
let batch: Message[] = turn.inputs.map(({ text, user, sender }) => ({ text, user, sender }));
|
|
709
|
+
let batch: Message[] = turn.inputs.map(({ text, user, sender, notice }) => ({ text, user, sender, notice }));
|
|
696
710
|
while (batch.length > 0 && !turn.abort.signal.aborted) {
|
|
697
711
|
for (const { text, ...rest } of batch.slice(0, -1)) await session.sendCustomMessage(customMessage(text, "steer", rest), { triggerTurn: false });
|
|
698
712
|
const { text, ...rest } = batch.at(-1)!;
|
|
@@ -704,7 +718,7 @@ export class Agents {
|
|
|
704
718
|
}
|
|
705
719
|
batch = [
|
|
706
720
|
...turn.pending.splice(0),
|
|
707
|
-
...leftover.flatMap((
|
|
721
|
+
...leftover.flatMap((message) => message.details.delivery === "write" ? [] : [messageParts(message)]),
|
|
708
722
|
];
|
|
709
723
|
}
|
|
710
724
|
const last = lastAnswer(session, before);
|
|
@@ -744,10 +758,9 @@ export class Agents {
|
|
|
744
758
|
}
|
|
745
759
|
record.state = outcome;
|
|
746
760
|
record.result = result;
|
|
747
|
-
if (input.silent) record.notified = true;
|
|
748
761
|
this.persist(agent);
|
|
749
762
|
input.done?.resolve();
|
|
750
|
-
if (
|
|
763
|
+
if (!input.silent) this.notifyLater(agent, record);
|
|
751
764
|
}
|
|
752
765
|
|
|
753
766
|
private notifyLater(agent: Owned, record: InputRecord): void {
|
|
@@ -756,10 +769,12 @@ export class Agents {
|
|
|
756
769
|
const release = trackTreeWork(this.self.rootId);
|
|
757
770
|
queueMicrotask(() => {
|
|
758
771
|
try {
|
|
759
|
-
if (this.closing
|
|
760
|
-
record.
|
|
761
|
-
this.
|
|
762
|
-
|
|
772
|
+
if (this.closing) return;
|
|
773
|
+
const { kept, notice } = boundText(record.result ?? "", "pi-agents-notification");
|
|
774
|
+
const id = shortId(agent.record.id, this.ids());
|
|
775
|
+
const text = `<${RESULT_TAG} name="${agent.record.name}" id="${id}" status="${record.state}">\n${kept}\n</${RESULT_TAG}>${notice ? `\n\n${notice}` : ""}`;
|
|
776
|
+
const result = { name: agent.record.name, id, status: record.state, body: kept, ...(notice && { notice }) };
|
|
777
|
+
if (!hold(this.held, agent.record.id, { text, result })) this.notify(text, { results: [result] });
|
|
763
778
|
} finally {
|
|
764
779
|
release();
|
|
765
780
|
}
|
|
@@ -767,9 +782,9 @@ export class Agents {
|
|
|
767
782
|
}
|
|
768
783
|
|
|
769
784
|
/** Delivers a notification to this Agent as a steer. */
|
|
770
|
-
notify(text: string): void {
|
|
785
|
+
notify(text: string, notice: Notice): void {
|
|
771
786
|
try {
|
|
772
|
-
deliver(undefined, { id: this.self.id, name: this.self.name(), ownerId: this.self.ownerId, cwd: "", state: "running" }, "steer", text);
|
|
787
|
+
deliver(undefined, { id: this.self.id, name: this.self.name(), ownerId: this.self.ownerId, cwd: "", state: "running" }, "steer", text, notice);
|
|
773
788
|
} catch (error) {
|
|
774
789
|
console.warn(`[pi-agents] notification failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
775
790
|
}
|
|
@@ -808,7 +823,7 @@ export class Agents {
|
|
|
808
823
|
}
|
|
809
824
|
|
|
810
825
|
private newRecord(agent: Owned): InputRecord {
|
|
811
|
-
const record: InputRecord = { id: `${agent.record.id}:${agent.record.inputs.length + 1}`, state: "queued"
|
|
826
|
+
const record: InputRecord = { id: `${agent.record.id}:${agent.record.inputs.length + 1}`, state: "queued" };
|
|
812
827
|
agent.record.inputs.push(record);
|
|
813
828
|
return record;
|
|
814
829
|
}
|
|
@@ -837,7 +852,31 @@ export class Agents {
|
|
|
837
852
|
return this.ctx;
|
|
838
853
|
}
|
|
839
854
|
|
|
840
|
-
|
|
855
|
+
/** Creates the Session of a root Agent the caller does not own; a message to it starts it. */
|
|
856
|
+
disown(request: Omit<SpawnRequest, "message" | "context">): { id: string; name: string } {
|
|
857
|
+
const ctx = this.requireContext();
|
|
858
|
+
const name = request.name.trim();
|
|
859
|
+
const env = ctx.sessionManager.getBranch().filter((entry) => entry.type === "custom" && entry.customType === "env-state").pop();
|
|
860
|
+
if (env?.type === "custom" && (env.data as { environment?: string | null }).environment) throw new Error("An Agent in a pi-env environment cannot disown an Agent.");
|
|
861
|
+
const { model, thinkingLevel } = this.resolveModel(request);
|
|
862
|
+
const cwd = path.resolve(ctx.cwd, request.cwd ?? ".");
|
|
863
|
+
// Where Pi keeps the cwd's Sessions without `--session-dir`, which the daemon searches.
|
|
864
|
+
const configured = path.resolve(rootSessionDir());
|
|
865
|
+
const sessionDir = configured === path.resolve(this.agentDir, "sessions") ? defaultSessionDir(cwd, this.agentDir) : configured;
|
|
866
|
+
const sessionManager = SessionManager.create(cwd, sessionDir);
|
|
867
|
+
sessionManager.appendSessionInfo(name);
|
|
868
|
+
sessionManager.appendModelChange(model.provider, model.id);
|
|
869
|
+
sessionManager.appendThinkingLevelChange(thinkingLevel);
|
|
870
|
+
try {
|
|
871
|
+
writePreparedSession(sessionManager);
|
|
872
|
+
} finally {
|
|
873
|
+
// The Session runs in a process of its own.
|
|
874
|
+
releaseSession(sessionManager);
|
|
875
|
+
}
|
|
876
|
+
return { id: sessionManager.getSessionId(), name };
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
private resolveModel(request: Pick<SpawnRequest, "model" | "thinkingLevel">): Pick<Prepared, "model" | "thinkingLevel"> {
|
|
841
880
|
const ctx = this.requireContext();
|
|
842
881
|
let model = ctx.model;
|
|
843
882
|
if (request.model !== undefined) {
|
|
@@ -848,7 +887,12 @@ export class Agents {
|
|
|
848
887
|
if (!model) throw new Error(`Unknown model "${request.model}". Use provider/modelId.`);
|
|
849
888
|
}
|
|
850
889
|
if (!model) throw new Error("Cannot spawn an Agent without a model.");
|
|
851
|
-
|
|
890
|
+
return { model, thinkingLevel: request.thinkingLevel ?? this.pi.getThinkingLevel() };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private prepare(request: Omit<SpawnRequest, "message">, name: string, id?: string): Prepared {
|
|
894
|
+
const ctx = this.requireContext();
|
|
895
|
+
const { model, thinkingLevel } = this.resolveModel(request);
|
|
852
896
|
// In a pi-env execution environment, the Agent runs in the same environment and `cwd` is a path there.
|
|
853
897
|
const envEntry = ctx.sessionManager.getBranch().filter((entry) => entry.type === "custom" && entry.customType === "env-state").pop();
|
|
854
898
|
const envState = envEntry?.type === "custom" ? structuredClone(envEntry.data) as { environment?: string | null } : undefined;
|
package/src/agents/registry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The Agents loaded in this process, shared across trees: who sees whom, how targets resolve, and how messages are addressed. */
|
|
2
|
-
import type { Usage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ImageContent, Usage } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { Programs } from "../programs/programs.ts";
|
|
5
5
|
import type { Agents } from "./agents.ts";
|
|
@@ -34,7 +34,7 @@ export interface AgentNode {
|
|
|
34
34
|
/** An Agent's background Programs, which the task viewer lists. */
|
|
35
35
|
programs?: Programs;
|
|
36
36
|
/** Delivers a message to this node; used only for root Agents. */
|
|
37
|
-
receive(text: string, delivery: Delivery, messageId?: string, sender?: Sender): void;
|
|
37
|
+
receive(text: string, delivery: Delivery, messageId?: string, sender?: Sender, notice?: Notice): void;
|
|
38
38
|
/** A root Agent's turn was stopped: messages from other Agents become writes until the user's next input. */
|
|
39
39
|
stopped?: boolean;
|
|
40
40
|
}
|
|
@@ -252,11 +252,30 @@ export function resolveIn(entries: Entry[], target: string): Entry {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/** Sends a message from one Agent to another visible Agent. */
|
|
255
|
+
/** A result a notification carries, as it shows: a Program has no name, and may have a wall time. */
|
|
256
|
+
export interface NotifiedResult {
|
|
257
|
+
name?: string;
|
|
258
|
+
id: string;
|
|
259
|
+
status: string;
|
|
260
|
+
seconds?: number;
|
|
261
|
+
/** The text kept within Pi's output limits, and the notice naming the file with the rest. */
|
|
262
|
+
body: string;
|
|
263
|
+
notice?: string;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** What a notification carries: its results, and their images. */
|
|
267
|
+
export interface Notice {
|
|
268
|
+
results: NotifiedResult[];
|
|
269
|
+
images?: ImageContent[];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Delivers a message from `from`, or with `notice` a notification. */
|
|
255
273
|
export function deliver(
|
|
256
274
|
from: { id: string; name?: string } | undefined,
|
|
257
275
|
target: Entry,
|
|
258
276
|
delivery: Delivery,
|
|
259
277
|
body: string,
|
|
278
|
+
notice?: Notice,
|
|
260
279
|
): { queued: boolean } {
|
|
261
280
|
const fromOwner = from !== undefined && target.ownerId === from.id;
|
|
262
281
|
const ids = recipientIds(target);
|
|
@@ -265,12 +284,12 @@ export function deliver(
|
|
|
265
284
|
if (target.ownerId === undefined) {
|
|
266
285
|
const node = shared.nodes.get(target.id);
|
|
267
286
|
if (!node) throw new Error(`${label(target, ids)} is offline.`);
|
|
268
|
-
node.receive(text, delivery, undefined, by);
|
|
287
|
+
node.receive(text, delivery, undefined, by, notice);
|
|
269
288
|
return { queued: false };
|
|
270
289
|
}
|
|
271
290
|
const owner = shared.nodes.get(target.ownerId);
|
|
272
291
|
if (!owner) throw new Error(`${label(target, ids)} is offline.`);
|
|
273
|
-
return owner.agents.accept(target.id, text, delivery, { fromOwner,
|
|
292
|
+
return owner.agents.accept(target.id, text, delivery, { fromOwner, notice, sender: by });
|
|
274
293
|
}
|
|
275
294
|
|
|
276
295
|
export const MESSAGE_TAG = "agent-message";
|
|
@@ -307,10 +326,24 @@ export function messageBody(text: string, sender: Sender): string | undefined {
|
|
|
307
326
|
|
|
308
327
|
/**
|
|
309
328
|
* `messageId` marks a message from a root Agent in another process, so it is added once; `user`
|
|
310
|
-
* marks the user's input from the task viewer,
|
|
329
|
+
* marks the user's input from the task viewer, `sender` the Agent that sent it, and `notice` a notification.
|
|
311
330
|
*/
|
|
312
|
-
export function customMessage(text: string, delivery: Delivery, { messageId, user, sender }: { messageId?: string; user?: boolean; sender?: Sender } = {}) {
|
|
313
|
-
return {
|
|
331
|
+
export function customMessage(text: string, delivery: Delivery, { messageId, user, sender, notice }: { messageId?: string; user?: boolean; sender?: Sender; notice?: Notice } = {}) {
|
|
332
|
+
return {
|
|
333
|
+
customType: MESSAGE_TYPE,
|
|
334
|
+
content: notice?.images?.length ? [{ type: "text" as const, text }, ...notice.images] : text,
|
|
335
|
+
display: true,
|
|
336
|
+
details: { delivery, ...(messageId ? { messageId } : {}), ...(user ? { user } : {}), ...(sender ? { sender } : {}), ...(notice ? { results: notice.results } : {}) },
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** The text, sender, and notice of a message `customMessage` made. */
|
|
341
|
+
export function messageParts({ content, details }: { content: string | Array<{ type: string }>; details: { user?: boolean; sender?: Sender; results?: NotifiedResult[] } }) {
|
|
342
|
+
const parts = typeof content === "string" ? [{ type: "text", text: content }] : content;
|
|
343
|
+
const text = parts.flatMap((part) => part.type === "text" ? [(part as { text: string }).text] : []).join("\n");
|
|
344
|
+
const images = parts.filter((part) => part.type === "image") as ImageContent[];
|
|
345
|
+
const notice = details.results && { results: details.results, ...(images.length ? { images } : {}) };
|
|
346
|
+
return { text, user: details.user, sender: details.sender, ...(notice ? { notice } : {}) };
|
|
314
347
|
}
|
|
315
348
|
|
|
316
349
|
export function receivedMessageIds(ctx: ExtensionContext): string[] {
|