pum-agent 0.2.13-beta.1 → 0.2.14-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pum-agent",
3
- "version": "0.2.13-beta.1",
3
+ "version": "0.2.14-beta.1",
4
4
  "description": "A compact terminal coding agent powered by pi and OpenTUI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/app.tsx CHANGED
@@ -1209,7 +1209,6 @@ export function App({
1209
1209
  : event.toolName.startsWith("message_cache_")
1210
1210
  ? messageCacheDetail(event.result)
1211
1211
  : undefined,
1212
- output: undefined,
1213
1212
  exitCode: bashResult.exitCode,
1214
1213
  });
1215
1214
  break;
@@ -0,0 +1,80 @@
1
+ import { rmSync, writeFileSync } from "node:fs";
2
+ import {
3
+ CURRENT_SESSION_VERSION,
4
+ SessionManager,
5
+ type SessionEntry,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import type { ForkOrigin, ForkSource } from "./types";
8
+
9
+ type ForkSessionSource = Pick<
10
+ SessionManager,
11
+ "getSessionId" | "getSessionFile" | "getLeafId" | "getBranch" | "buildContextEntries"
12
+ >;
13
+
14
+ export function captureForkSource(
15
+ sessionManager: ForkSessionSource,
16
+ sourceAgentId: string | null,
17
+ ): ForkSource {
18
+ const cutoffEntryId = sessionManager.getLeafId();
19
+ const entries = cutoffEntryId === null ? [] : sessionManager.getBranch(cutoffEntryId);
20
+ if (cutoffEntryId !== null && entries.at(-1)?.id !== cutoffEntryId) {
21
+ throw new Error(`Cannot fork: cutoff entry ${cutoffEntryId} is not on the active branch`);
22
+ }
23
+ return {
24
+ origin: {
25
+ sourceSessionId: sessionManager.getSessionId(),
26
+ cutoffEntryId,
27
+ sourceAgentId,
28
+ },
29
+ sourceSessionFile: sessionManager.getSessionFile(),
30
+ entries: structuredClone(entries) as SessionEntry[],
31
+ };
32
+ }
33
+
34
+ export function createForkedSession(
35
+ source: ForkSource,
36
+ targetCwd: string,
37
+ sessionDir: string,
38
+ ): SessionManager {
39
+ const cutoff = source.origin.cutoffEntryId;
40
+ if ((cutoff === null && source.entries.length !== 0)
41
+ || (cutoff !== null && source.entries.at(-1)?.id !== cutoff)) {
42
+ throw new Error("Cannot fork: captured active branch does not match the cutoff entry");
43
+ }
44
+
45
+ const target = SessionManager.create(targetCwd, sessionDir, {
46
+ parentSession: source.sourceSessionFile,
47
+ });
48
+ const sessionFile = target.getSessionFile();
49
+ const header = target.getHeader();
50
+ if (!sessionFile || !header) throw new Error("Cannot fork: target session could not be allocated");
51
+
52
+ const forkHeader = {
53
+ ...header,
54
+ version: CURRENT_SESSION_VERSION,
55
+ cwd: targetCwd,
56
+ parentSession: source.sourceSessionFile,
57
+ };
58
+ const content = [forkHeader, ...source.entries]
59
+ .map((entry) => JSON.stringify(entry))
60
+ .join("\n") + "\n";
61
+ try {
62
+ writeFileSync(sessionFile, content, { flag: "wx" });
63
+ return SessionManager.open(sessionFile, sessionDir, targetCwd);
64
+ } catch (error) {
65
+ rmSync(sessionFile, { force: true });
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ export function entriesAfterForkCutoff(
71
+ sessionManager: ForkSessionSource,
72
+ origin?: ForkOrigin,
73
+ ): SessionEntry[] {
74
+ if (!origin) return sessionManager.buildContextEntries();
75
+ const branch = sessionManager.getBranch();
76
+ if (origin.cutoffEntryId === null) return branch;
77
+ const cutoffIndex = branch.findIndex((entry) => entry.id === origin.cutoffEntryId);
78
+ if (cutoffIndex < 0) return [];
79
+ return branch.slice(cutoffIndex + 1);
80
+ }
@@ -9,7 +9,7 @@ import {
9
9
  type ModelRuntime,
10
10
  } from "@earendil-works/pi-coding-agent";
11
11
  import { Type } from "typebox";
12
- import { existsSync } from "node:fs";
12
+ import { existsSync, rmSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { randomUUID } from "node:crypto";
15
15
  import {
@@ -80,6 +80,7 @@ import {
80
80
  type AgentMessageData,
81
81
  type TriggerEventData,
82
82
  type AgentTranscript,
83
+ type ForkSource,
83
84
  type SpawnSubagentOptions,
84
85
  type SubagentManagerEvent,
85
86
  type SubagentRegistryEvent,
@@ -90,6 +91,7 @@ import {
90
91
  import type { SpawnPreviewManager, SpawnPreviewRequester } from "./spawn-preview";
91
92
  import { readonlySubagentExtension } from "./readonly";
92
93
  import type { SessionStatsManager } from "../session-stats";
94
+ import { captureForkSource, createForkedSession, entriesAfterForkCutoff } from "./fork-session";
93
95
 
94
96
  const MAX_RETAINED_AGENTS = 100;
95
97
  const MAX_MESSAGE_LENGTH = 12_000;
@@ -277,6 +279,12 @@ export function spawnSubagentParameters(readonlyAvailable: boolean) {
277
279
  task: Type.String({ description: "Complete task for the subagent" }),
278
280
  name: Type.Optional(Type.String({ description: "Optional worktree and agent name" })),
279
281
  preview: Type.Optional(Type.Boolean({ description: "Ask the user to approve before spawning" })),
282
+ context: Type.Optional(Type.Union([
283
+ Type.Literal("fresh"),
284
+ Type.Literal("fork"),
285
+ ], {
286
+ description: 'Conversation context mode. Defaults to "fresh". Use "fork" to inherit the immediate requester conversation before this assistant turn.',
287
+ })),
280
288
  ...(readonlyAvailable ? { readonly: readonlySpawnParameter() } : {}),
281
289
  }, { additionalProperties: false });
282
290
  }
@@ -443,7 +451,11 @@ export class SubagentManager {
443
451
  );
444
452
  restoredEntries.set(snapshot.id, childManager.getEntries());
445
453
  transcript = {
446
- lines: replayEntries(childManager.buildContextEntries(), snapshot.worktree.path, true),
454
+ lines: replayEntries(
455
+ entriesAfterForkCutoff(childManager, snapshot.forkOrigin),
456
+ snapshot.worktree.path,
457
+ true,
458
+ ),
447
459
  stream: null,
448
460
  pending: [],
449
461
  };
@@ -845,7 +857,6 @@ export class SubagentManager {
845
857
  : event.toolName.startsWith("message_cache_")
846
858
  ? messageCacheDetail(event.result)
847
859
  : undefined,
848
- output: undefined,
849
860
  exitCode: bashResult.exitCode,
850
861
  });
851
862
  break;
@@ -1050,7 +1061,14 @@ export class SubagentManager {
1050
1061
  thinkingLevel: parent.snapshot.thinkingLevel,
1051
1062
  readonly: readonlyRequested,
1052
1063
  parentAgentId: agentId,
1064
+ context: params.context ?? "fresh",
1053
1065
  };
1066
+ if (options.context === "fork") {
1067
+ options.forkSource = captureForkSource(
1068
+ ctx.sessionManager,
1069
+ agentId,
1070
+ );
1071
+ }
1054
1072
  if (params.preview) {
1055
1073
  const preview = await this.requestSpawnPreview({
1056
1074
  sessionId: ctx.sessionManager.getSessionId(),
@@ -1269,7 +1287,11 @@ export class SubagentManager {
1269
1287
  modelId: `${ctx.model.provider}/${ctx.model.id}`,
1270
1288
  thinkingLevel: ctx.thinkingLevel ?? "off",
1271
1289
  readonly: readonlyRequested,
1290
+ context: params.context ?? "fresh",
1272
1291
  };
1292
+ if (options.context === "fork") {
1293
+ options.forkSource = captureForkSource(ctx.sessionManager, null);
1294
+ }
1273
1295
  if (params.preview) {
1274
1296
  const preview = await this.requestSpawnPreview({
1275
1297
  sessionId: ctx.sessionManager.getSessionId(),
@@ -1421,6 +1443,11 @@ export class SubagentManager {
1421
1443
  if (options.readonly === true && this.sandboxModeSource() === "off") {
1422
1444
  throw new Error("Readonly subagents require the PUM Sandbox setting to be Auto or Require");
1423
1445
  }
1446
+ const context = options.context ?? "fresh";
1447
+ const forkSource = context === "fork"
1448
+ ? options.forkSource ?? this.captureSpawnerForkSource(options.parentAgentId ?? null)
1449
+ : undefined;
1450
+ let allocatedSessionFile: string | undefined;
1424
1451
  const record = await this.withWorktreeLock(async () => {
1425
1452
  if (this.activeCount() >= this.maxActiveSubagents) throw activeLimitError(this.maxActiveSubagents);
1426
1453
  if (this.records.size >= MAX_RETAINED_AGENTS) throw new Error(`At most ${MAX_RETAINED_AGENTS} subagents can be retained`);
@@ -1431,33 +1458,48 @@ export class SubagentManager {
1431
1458
  }
1432
1459
 
1433
1460
  const worktree = await createWorktree(this.mainCwd, options.name);
1434
- const id = randomUUID().slice(0, 8);
1435
- const now = Date.now();
1436
- const snapshot: SubagentSnapshot = {
1437
- id,
1438
- name: worktree.name,
1439
- task: options.task,
1440
- status: "starting",
1441
- worktree,
1442
- parentAgentId: options.parentAgentId ?? null,
1443
- modelId: options.modelId,
1444
- thinkingLevel: options.thinkingLevel,
1445
- readonly: options.readonly === true,
1446
- transcript: emptyTranscript(),
1447
- startedAt: now,
1448
- updatedAt: now,
1449
- usage: emptyAgentUsage(),
1450
- };
1451
- const created: RuntimeRecord = {
1452
- snapshot,
1453
- userInstructionNotices: new Map(),
1454
- activityGeneration: 0,
1455
- idleNotifiedGeneration: 0,
1456
- };
1457
- this.records.set(id, created);
1458
- this.persist({ event: "spawned", id, at: now, snapshot: snapshotMetadata(snapshot) });
1459
- this.emit();
1460
- return created;
1461
+ try {
1462
+ const id = randomUUID().slice(0, 8);
1463
+ const now = Date.now();
1464
+ const snapshot: SubagentSnapshot = {
1465
+ id,
1466
+ name: worktree.name,
1467
+ task: options.task,
1468
+ status: "starting",
1469
+ worktree,
1470
+ parentAgentId: options.parentAgentId ?? null,
1471
+ modelId: options.modelId,
1472
+ thinkingLevel: options.thinkingLevel,
1473
+ readonly: options.readonly === true,
1474
+ forkOrigin: forkSource?.origin,
1475
+ transcript: emptyTranscript(),
1476
+ startedAt: now,
1477
+ updatedAt: now,
1478
+ usage: emptyAgentUsage(),
1479
+ };
1480
+ if (forkSource) {
1481
+ const sessionDir = join(this.agentDir, "subagents", this.parentSessionId);
1482
+ allocatedSessionFile = createForkedSession(
1483
+ forkSource,
1484
+ worktree.path,
1485
+ sessionDir,
1486
+ ).getSessionFile();
1487
+ snapshot.sessionFile = allocatedSessionFile;
1488
+ }
1489
+ const created: RuntimeRecord = {
1490
+ snapshot,
1491
+ userInstructionNotices: new Map(),
1492
+ activityGeneration: 0,
1493
+ idleNotifiedGeneration: 0,
1494
+ };
1495
+ this.records.set(id, created);
1496
+ this.emit();
1497
+ return created;
1498
+ } catch (error) {
1499
+ if (allocatedSessionFile) rmSync(allocatedSessionFile, { force: true });
1500
+ await removeWorktree(this.mainCwd, worktree).catch(() => {});
1501
+ throw error;
1502
+ }
1461
1503
  });
1462
1504
 
1463
1505
  try {
@@ -1470,11 +1512,30 @@ export class SubagentManager {
1470
1512
  });
1471
1513
  return cloneSnapshot(record);
1472
1514
  } catch (error) {
1473
- this.updateStatus(record, "failed", String(error));
1515
+ if (context === "fork") {
1516
+ await record.dispose?.();
1517
+ this.records.delete(record.snapshot.id);
1518
+ if (record.snapshot.sessionFile) rmSync(record.snapshot.sessionFile, { force: true });
1519
+ await this.withWorktreeLock(() => removeWorktree(this.mainCwd, record.snapshot.worktree)).catch(() => {});
1520
+ this.persist({ event: "removed", id: record.snapshot.id, at: Date.now() });
1521
+ this.emit();
1522
+ } else {
1523
+ this.updateStatus(record, "failed", String(error));
1524
+ }
1474
1525
  throw error;
1475
1526
  }
1476
1527
  }
1477
1528
 
1529
+ private captureSpawnerForkSource(parentAgentId: string | null): ForkSource {
1530
+ if (parentAgentId === null) {
1531
+ if (!this.mainSessionManager) throw new Error("Cannot fork: the main source session is unavailable");
1532
+ return captureForkSource(this.mainSessionManager, null);
1533
+ }
1534
+ const parent = this.records.get(parentAgentId);
1535
+ if (!parent?.session) throw new Error("Cannot fork: the immediate parent session is unavailable");
1536
+ return captureForkSource(parent.session.sessionManager, parentAgentId);
1537
+ }
1538
+
1478
1539
  private async ensureRuntime(record: RuntimeRecord, retrySettlements = true): Promise<void> {
1479
1540
  if (record.session) {
1480
1541
  if (retrySettlements) await this.retrySettlementsForParent(record.snapshot.id);
@@ -2247,9 +2308,17 @@ export class SubagentManager {
2247
2308
  private formatAgentList(): string {
2248
2309
  const agents = this.getAgents();
2249
2310
  if (!agents.length) return "No subagents.";
2250
- return agents.map((agent) =>
2251
- `${agent.id} ${agent.name} ${agent.status}${agent.readonly ? " readonly" : ""}\n ${agent.worktree.branch}\n ${agent.worktree.path}`,
2252
- ).join("\n");
2311
+ return agents.map((agent) => {
2312
+ const origin = agent.forkOrigin;
2313
+ const source = origin?.sourceAgentId
2314
+ ? this.records.get(origin.sourceAgentId)?.snapshot.name ?? origin.sourceAgentId
2315
+ : "main";
2316
+ return `${agent.id} ${agent.name} ${agent.status}${agent.readonly ? " readonly" : ""}`
2317
+ + `\n ${agent.worktree.branch}\n ${agent.worktree.path}`
2318
+ + (origin
2319
+ ? `\n fork source: ${source} · session ${origin.sourceSessionId} · cutoff ${origin.cutoffEntryId ?? "root"}`
2320
+ : "");
2321
+ }).join("\n");
2253
2322
  }
2254
2323
 
2255
2324
  async stop(id: string, status: SubagentStatus = "stopped", persist = true): Promise<void> {
@@ -38,7 +38,11 @@ export function SpawnPreviewPopup({
38
38
  const geometry = spawnPreviewPopupGeometry(terminalWidth, terminalHeight);
39
39
  const scrollRef = useRef<ScrollBoxRenderable>(null);
40
40
  const noteHeight = geometry.compact ? 1 : Math.min(4, Math.max(1, terminalHeight - 8));
41
- const taskAreaHeight = Math.max(1, geometry.height - noteHeight - (geometry.compact ? 1 : 6));
41
+ const taskAreaHeight = Math.max(1, geometry.height - noteHeight - (geometry.compact ? 1 : 8));
42
+ const contextMode = request.options.context ?? "fresh";
43
+ const source = contextMode === "fork"
44
+ ? `${request.requester.name} · session ${request.requester.sessionId}`
45
+ : "none";
42
46
 
43
47
  useEffect(() => {
44
48
  inputRef.current?.setText("");
@@ -64,12 +68,15 @@ export function SpawnPreviewPopup({
64
68
  padding={geometry.compact ? 0 : 1}
65
69
  >
66
70
  {!geometry.compact ? (
67
- <text
68
- content={request.options.readonly ? "Child task · readonly" : "Child task"}
69
- fg={theme.accent}
70
- bg={theme.popupBg}
71
- style={{ height: 1, flexShrink: 0 }}
72
- />
71
+ <box style={{ flexDirection: "column", height: 3, flexShrink: 0 }}>
72
+ <text
73
+ content={request.options.readonly ? "Child task · readonly" : "Child task"}
74
+ fg={theme.accent}
75
+ bg={theme.popupBg}
76
+ />
77
+ <text content={`Context · ${contextMode}`} fg={theme.dim} bg={theme.popupBg} />
78
+ <text content={`Source · ${source}`} fg={theme.dim} bg={theme.popupBg} />
79
+ </box>
73
80
  ) : null}
74
81
  <scrollbox
75
82
  ref={scrollRef}
@@ -2,6 +2,7 @@ import type { ImageContent } from "@earendil-works/pi-ai";
2
2
  import type { Line, PendingLine } from "../transcript";
3
3
  import type { WorktreeRecord } from "../worktree";
4
4
  import type { AgentUsage } from "../agent-usage";
5
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
5
6
  import {
6
7
  EXTERNAL_TRIGGER_CUSTOM_TYPE,
7
8
  type ExternalTriggerEventData,
@@ -32,6 +33,21 @@ export type AgentTranscript = {
32
33
  pending: PendingLine[];
33
34
  };
34
35
 
36
+ export type SpawnContextMode = "fresh" | "fork";
37
+
38
+ export type ForkOrigin = {
39
+ sourceSessionId: string;
40
+ cutoffEntryId: string | null;
41
+ sourceAgentId: string | null;
42
+ };
43
+
44
+ /** Immutable runtime capture. Only origin is persisted in the subagent snapshot. */
45
+ export type ForkSource = {
46
+ origin: ForkOrigin;
47
+ sourceSessionFile?: string;
48
+ entries: readonly SessionEntry[];
49
+ };
50
+
35
51
  export type SubagentSnapshot = {
36
52
  id: string;
37
53
  name: string;
@@ -45,6 +61,8 @@ export type SubagentSnapshot = {
45
61
  thinkingLevel: string;
46
62
  /** True when the child must not mutate files or delegate filesystem mutation. Missing legacy values mean false. */
47
63
  readonly?: boolean;
64
+ /** Present only when this child inherited an exact requester conversation branch. */
65
+ forkOrigin?: ForkOrigin;
48
66
  transcript: AgentTranscript;
49
67
  summary?: string;
50
68
  startedAt: number;
@@ -121,6 +139,9 @@ export type SpawnSubagentOptions = {
121
139
  readonly?: boolean;
122
140
  createWorktree?: boolean;
123
141
  parentAgentId?: string | null;
142
+ context?: SpawnContextMode;
143
+ /** Runtime-only immutable branch capture. This value is not persisted. */
144
+ forkSource?: ForkSource;
124
145
  };
125
146
 
126
147
  export type RoutedPrompt = {
package/src/tool-line.ts CHANGED
@@ -9,7 +9,7 @@ export type ToolCall = {
9
9
  state: "running" | "ok" | "error" | "rejected";
10
10
  /** "+3 −1" for edits, or an error note. */
11
11
  detail?: string;
12
- /** Cumulative live output, or the final displayed Bash output line. */
12
+ /** Cumulative live output retained until the Bash output display period ends. */
13
13
  output?: string;
14
14
  /** Start time used to delay live Bash output without delaying the tool row. */
15
15
  startedAt?: number;
@@ -6,7 +6,7 @@ import {
6
6
  type SyntaxStyle,
7
7
  } from "@opentui/core";
8
8
  import type { MarkdownProps } from "@opentui/react";
9
- import { useEffect, useState } from "react";
9
+ import { useEffect, useRef, useState } from "react";
10
10
  import {
11
11
  useBlinkingText,
12
12
  useMarkdownCaret,
@@ -374,26 +374,62 @@ export function toolStateGlyph(state: ToolCall["state"]): string {
374
374
 
375
375
  const CHECK_MODE_HARD_BLOCK_PREFIX = "Check mode hard block:";
376
376
  const BASH_OUTPUT_DELAY_MS = 500;
377
+ const BASH_OUTPUT_MIN_VISIBLE_MS = 2_000;
378
+
379
+ function useBashOutputVisible(call: ToolCall): boolean {
380
+ const initiallyVisible = call.name === "bash"
381
+ && call.state === "running"
382
+ && Boolean(call.output)
383
+ && (call.startedAt === undefined || Date.now() - call.startedAt >= BASH_OUTPUT_DELAY_MS);
384
+ const [visible, setVisible] = useState(initiallyVisible);
385
+ const visibleSince = useRef<number | undefined>(initiallyVisible ? Date.now() : undefined);
377
386
 
378
- function useLiveBashOutputReady(call: ToolCall): boolean {
379
- const [ready, setReady] = useState(() => (
380
- call.startedAt === undefined || Date.now() - call.startedAt >= BASH_OUTPUT_DELAY_MS
381
- ));
382
387
  useEffect(() => {
383
- if (call.name !== "bash" || call.state !== "running" || call.startedAt === undefined) {
384
- setReady(call.startedAt === undefined);
388
+ if (call.name !== "bash" || !call.output) {
389
+ visibleSince.current = undefined;
390
+ setVisible(false);
385
391
  return;
386
392
  }
387
- const delay = BASH_OUTPUT_DELAY_MS - (Date.now() - call.startedAt);
388
- if (delay <= 0) {
389
- setReady(true);
393
+
394
+ const show = () => {
395
+ visibleSince.current ??= Date.now();
396
+ setVisible(true);
397
+ };
398
+
399
+ if (call.state === "running") {
400
+ if (call.startedAt === undefined) {
401
+ show();
402
+ return;
403
+ }
404
+ const delay = BASH_OUTPUT_DELAY_MS - (Date.now() - call.startedAt);
405
+ if (delay <= 0) {
406
+ show();
407
+ return;
408
+ }
409
+ visibleSince.current = undefined;
410
+ setVisible(false);
411
+ const timer = setTimeout(show, delay);
412
+ return () => clearTimeout(timer);
413
+ }
414
+
415
+ if (visibleSince.current === undefined) {
416
+ if (call.startedAt === undefined || Date.now() - call.startedAt < BASH_OUTPUT_DELAY_MS) {
417
+ setVisible(false);
418
+ return;
419
+ }
420
+ show();
421
+ }
422
+
423
+ const remaining = BASH_OUTPUT_MIN_VISIBLE_MS - (Date.now() - visibleSince.current!);
424
+ if (remaining <= 0) {
425
+ setVisible(false);
390
426
  return;
391
427
  }
392
- setReady(false);
393
- const timer = setTimeout(() => setReady(true), delay);
428
+ const timer = setTimeout(() => setVisible(false), remaining);
394
429
  return () => clearTimeout(timer);
395
- }, [call.id, call.name, call.state, call.startedAt]);
396
- return ready;
430
+ }, [call.id, call.name, call.output, call.startedAt, call.state]);
431
+
432
+ return visible;
397
433
  }
398
434
 
399
435
  function rejectedDetail(theme: Theme, detail: string): StyledText {
@@ -421,7 +457,7 @@ export function ToolLine({
421
457
  const spinner = useSpinner(call.state === "running");
422
458
  const failed = call.state === "error";
423
459
  const rejected = call.state === "rejected";
424
- const liveBashOutputReady = useLiveBashOutputReady(call);
460
+ const bashOutputVisible = useBashOutputVisible(call);
425
461
  const toolColor = failed ? theme.error : rejected ? theme.rejection : theme.tool;
426
462
  const argColor = failed ? theme.error : rejected ? theme.rejection : theme.toolArg;
427
463
  const detailColor = failed ? theme.error : rejected ? theme.rejection : theme.dim;
@@ -443,7 +479,7 @@ export function ToolLine({
443
479
  caretColor: failed ? theme.error : rejected ? theme.rejection : theme.accent,
444
480
  active: workingCaret,
445
481
  });
446
- const output = call.name === "bash" && call.state === "running" && call.output && liveBashOutputReady
482
+ const output = call.name === "bash" && call.output && bashOutputVisible
447
483
  ? bashOutputWindow(call.output)
448
484
  : null;
449
485
 
@@ -9,23 +9,26 @@ let currentStyle: WritingStyle = "none";
9
9
  * Practical ASD-STE100 guidance for model output. The controlled dictionary is
10
10
  * not embedded, so PUM does not claim that generated text is formally
11
11
  * certified or fully compliant with the standard.
12
+ *
13
+ * Kept deliberately concise: this text is part of the system prompt and is
14
+ * re-sent (cached) on every turn, so it costs tokens each turn. It states only
15
+ * the behavior-changing rules, not extra prose.
12
16
  */
13
17
  export const STE_SYSTEM_PROMPT = `## Writing style: Simplified Technical English (STE)
14
18
 
15
- Write your explanatory text with the principles of ASD-STE100 Simplified Technical English.
19
+ Write explanatory text with the principles of ASD-STE100 Simplified Technical English.
16
20
 
17
21
  - Keep the technical meaning accurate. Accuracy has priority over simplification.
18
22
  - Use simple and unambiguous words. Use one word for one meaning when possible.
19
- - Use necessary project terms, code identifiers, commands, paths, API names, and other technical nouns and verbs unchanged.
20
- - Use the active voice. For instructions, use the imperative form.
23
+ - Keep necessary project terms, code identifiers, commands, paths, API names, and other technical nouns and verbs unchanged.
24
+ - Use the active voice. Use the imperative form for instructions.
21
25
  - Give only one instruction in each sentence.
22
- - Keep procedural sentences to 20 words or fewer.
23
- - Keep descriptive sentences to 25 words or fewer.
26
+ - Keep procedural sentences to 20 words or fewer. Keep descriptive sentences to 25 words or fewer.
24
27
  - Use short paragraphs. Keep one topic in each paragraph.
25
28
  - Use vertical lists for complex information or multiple actions.
26
29
  - Do not use contractions. Do not omit necessary articles, subjects, or verbs.
27
30
  - Avoid ambiguous pronouns, idioms, slang, phrasal verbs, and unnecessary synonyms.
28
- - Repeat a noun when a pronoun could have more than one meaning.
31
+ - Repeat a noun when a pronoun could mean more than one thing.
29
32
  - Keep terminology and wording consistent.
30
33
  - Do not modify quoted text, source code, tool output, or user-supplied text to make it follow STE.
31
34
  - Do not state or imply that the output has formal ASD approval or certified STE compliance.