@oh-my-pi/pi-coding-agent 14.9.2 → 14.9.3

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +3 -3
  4. package/src/config/prompt-templates.ts +0 -5
  5. package/src/config/settings-schema.ts +38 -0
  6. package/src/eval/eval.lark +10 -31
  7. package/src/eval/index.ts +1 -0
  8. package/src/eval/parse.ts +156 -255
  9. package/src/eval/sniff.ts +28 -0
  10. package/src/export/html/template.css +38 -0
  11. package/src/export/html/template.generated.ts +1 -1
  12. package/src/export/html/template.js +209 -15
  13. package/src/hashline/constants.ts +20 -0
  14. package/src/hashline/grammar.lark +16 -23
  15. package/src/hashline/hash.ts +4 -34
  16. package/src/hashline/input.ts +16 -2
  17. package/src/hashline/parser.ts +12 -1
  18. package/src/internal-urls/agent-protocol.ts +1 -0
  19. package/src/internal-urls/artifact-protocol.ts +1 -0
  20. package/src/internal-urls/docs-index.generated.ts +2 -1
  21. package/src/internal-urls/jobs-protocol.ts +1 -0
  22. package/src/internal-urls/local-protocol.ts +1 -0
  23. package/src/internal-urls/mcp-protocol.ts +1 -0
  24. package/src/internal-urls/memory-protocol.ts +1 -0
  25. package/src/internal-urls/pi-protocol.ts +1 -0
  26. package/src/internal-urls/router.ts +2 -1
  27. package/src/internal-urls/rule-protocol.ts +1 -0
  28. package/src/internal-urls/skill-protocol.ts +1 -0
  29. package/src/internal-urls/types.ts +18 -2
  30. package/src/prompts/system/custom-system-prompt.md +0 -2
  31. package/src/prompts/system/now-prompt.md +7 -0
  32. package/src/prompts/system/project-prompt.md +2 -0
  33. package/src/prompts/system/subagent-system-prompt.md +18 -9
  34. package/src/prompts/system/subagent-user-prompt.md +1 -10
  35. package/src/prompts/system/system-prompt.md +154 -233
  36. package/src/prompts/tools/bash.md +0 -24
  37. package/src/prompts/tools/eval.md +26 -13
  38. package/src/session/agent-session.ts +49 -17
  39. package/src/system-prompt.ts +8 -9
  40. package/src/task/executor.ts +9 -5
  41. package/src/task/index.ts +38 -31
  42. package/src/tools/bash.ts +15 -41
  43. package/src/tools/eval.ts +13 -36
  44. package/src/tools/path-utils.ts +21 -1
  45. package/src/tools/read.ts +69 -27
  46. package/src/tools/search.ts +13 -1
  47. package/src/utils/file-display-mode.ts +11 -5
  48. package/src/task/template.ts +0 -47
  49. package/src/tools/bash-normalize.ts +0 -107
@@ -570,6 +570,7 @@ export class AgentSession {
570
570
  #agentId: string | undefined;
571
571
  #agentRegistry: AgentRegistry | undefined;
572
572
  #providerSessionId: string | undefined;
573
+ #isDisposed = false;
573
574
  // Extension system
574
575
  #extensionRunner: ExtensionRunner | undefined = undefined;
575
576
  #turnIndex = 0;
@@ -646,23 +647,32 @@ export class AgentSession {
646
647
  #hindsightSessionState: HindsightSessionState | undefined = undefined;
647
648
  readonly rawSseDebugBuffer: RawSseDebugBuffer;
648
649
 
649
- #startPowerAssertion(): void {
650
- if (process.platform !== "darwin") {
651
- return;
652
- }
650
+ #acquirePowerAssertion(): void {
651
+ if (process.platform !== "darwin") return;
652
+ if (this.#powerAssertion) return;
653
+ const idle = this.settings.get("power.preventIdleSleep");
654
+ const system = this.settings.get("power.preventSystemSleep");
655
+ const user = this.settings.get("power.declareUserActive");
656
+ const display = this.settings.get("power.preventDisplaySleep");
657
+ // All four off → user opted out; do nothing.
658
+ if (!idle && !system && !user && !display) return;
653
659
  try {
654
- this.#powerAssertion = MacOSPowerAssertion.start({ reason: "Oh My Pi agent session" });
660
+ this.#powerAssertion = MacOSPowerAssertion.start({
661
+ reason: "Oh My Pi agent session",
662
+ idle,
663
+ system,
664
+ user,
665
+ display,
666
+ });
655
667
  } catch (error) {
656
668
  logger.warn("Failed to acquire macOS power assertion", { error: String(error) });
657
669
  }
658
670
  }
659
671
 
660
- #stopPowerAssertion(): void {
672
+ #releasePowerAssertion(): void {
661
673
  const assertion = this.#powerAssertion;
662
674
  this.#powerAssertion = undefined;
663
- if (!assertion) {
664
- return;
665
- }
675
+ if (!assertion) return;
666
676
  try {
667
677
  assertion.stop();
668
678
  } catch (error) {
@@ -670,11 +680,30 @@ export class AgentSession {
670
680
  }
671
681
  }
672
682
 
683
+ #beginInFlight(): void {
684
+ this.#promptInFlightCount++;
685
+ if (this.#promptInFlightCount === 1) {
686
+ this.#acquirePowerAssertion();
687
+ }
688
+ }
689
+
690
+ #endInFlight(): void {
691
+ this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
692
+ if (this.#promptInFlightCount === 0) {
693
+ this.#releasePowerAssertion();
694
+ }
695
+ }
696
+
697
+ #resetInFlight(): void {
698
+ this.#promptInFlightCount = 0;
699
+ this.#releasePowerAssertion();
700
+ }
701
+
673
702
  constructor(config: AgentSessionConfig) {
674
703
  this.agent = config.agent;
675
704
  this.sessionManager = config.sessionManager;
676
705
  this.settings = config.settings;
677
- this.#startPowerAssertion();
706
+ // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
678
707
  this.#asyncJobManager = config.asyncJobManager;
679
708
  this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
680
709
  this.#scopedModels = config.scopedModels ?? [];
@@ -2108,6 +2137,8 @@ export class AgentSession {
2108
2137
  * Call this when completely done with the session.
2109
2138
  */
2110
2139
  async dispose(): Promise<void> {
2140
+ this.#isDisposed = true;
2141
+ this.#pendingBackgroundExchanges = [];
2111
2142
  this.#evalExecutionDisposing = true;
2112
2143
  try {
2113
2144
  if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
@@ -2130,7 +2161,7 @@ export class AgentSession {
2130
2161
  );
2131
2162
  }
2132
2163
  await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
2133
- this.#stopPowerAssertion();
2164
+ this.#releasePowerAssertion();
2134
2165
  await this.sessionManager.close();
2135
2166
  this.#closeAllProviderSessions("dispose");
2136
2167
  const hindsightState = this.setHindsightSessionState(undefined);
@@ -3171,7 +3202,7 @@ export class AgentSession {
3171
3202
  skipPostPromptRecoveryWait?: boolean;
3172
3203
  },
3173
3204
  ): Promise<void> {
3174
- this.#promptInFlightCount++;
3205
+ this.#beginInFlight();
3175
3206
  const generation = this.#promptGeneration;
3176
3207
  try {
3177
3208
  // Flush any pending bash messages before the new prompt
@@ -3291,7 +3322,7 @@ export class AgentSession {
3291
3322
  await this.#waitForPostPromptRecovery();
3292
3323
  }
3293
3324
  } finally {
3294
- this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
3325
+ this.#endInFlight();
3295
3326
  }
3296
3327
  }
3297
3328
 
@@ -3877,7 +3908,7 @@ export class AgentSession {
3877
3908
  // Clear prompt-in-flight state: waitForIdle resolves when the agent loop's finally
3878
3909
  // block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
3879
3910
  // a subsequent prompt() can incorrectly observe the session as busy after an abort.
3880
- this.#promptInFlightCount = 0;
3911
+ this.#resetInFlight();
3881
3912
  // Safety net: if the agent loop aborted without producing an assistant
3882
3913
  // message (e.g. failed before the first stream), the in-flight yield was
3883
3914
  // never resolved or rejected by the normal message_end path. Reject it now
@@ -4699,7 +4730,7 @@ export class AgentSession {
4699
4730
  if (handoffSignal.aborted) {
4700
4731
  throw new Error("Handoff cancelled");
4701
4732
  }
4702
- this.#promptInFlightCount++;
4733
+ this.#beginInFlight();
4703
4734
  try {
4704
4735
  this.agent.setSystemPrompt(this.#baseSystemPrompt);
4705
4736
  await this.#promptAgentWithIdleRetry([
@@ -4711,7 +4742,7 @@ export class AgentSession {
4711
4742
  },
4712
4743
  ]);
4713
4744
  } finally {
4714
- this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
4745
+ this.#endInFlight();
4715
4746
  }
4716
4747
  await completionPromise;
4717
4748
 
@@ -6853,7 +6884,8 @@ export class AgentSession {
6853
6884
  if (this.#scheduledBackgroundExchangeFlush) return;
6854
6885
  this.#scheduledBackgroundExchangeFlush = true;
6855
6886
  const attempt = (): void => {
6856
- if (this.#pendingBackgroundExchanges.length === 0) {
6887
+ if (this.#pendingBackgroundExchanges.length === 0 || this.#isDisposed) {
6888
+ this.#pendingBackgroundExchanges = [];
6857
6889
  this.#scheduledBackgroundExchangeFlush = false;
6858
6890
  return;
6859
6891
  }
@@ -12,8 +12,10 @@ import type { SkillsSettings } from "./config/settings";
12
12
  import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
13
13
  import { loadSkills, type Skill } from "./extensibility/skills";
14
14
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
15
+ import nowPromptTemplate from "./prompts/system/now-prompt.md" with { type: "text" };
15
16
  import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
16
17
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
18
+ import { shortenPath } from "./tools/render-utils";
17
19
  import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
18
20
 
19
21
  interface AlwaysApplyRule {
@@ -503,7 +505,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
503
505
 
504
506
  const date = new Date().toISOString().slice(0, 10);
505
507
  const dateTime = date;
506
- const promptCwd = resolvedCwd.replace(/\\/g, "/");
508
+ const promptCwd = shortenPath(resolvedCwd.replace(/\\/g, "/"));
507
509
 
508
510
  // Build tool metadata for system prompt rendering
509
511
  // Priority: explicit list > tools map > defaults
@@ -541,7 +543,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
541
543
  const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
542
544
 
543
545
  const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
544
- const reportToolIssueToolName = toolPromptNames.get("report_tool_issue") ?? "report_tool_issue";
545
546
  const data = {
546
547
  systemPromptCustomization: effectiveSystemPromptCustomization,
547
548
  customPrompt: resolvedCustomPrompt,
@@ -568,18 +569,16 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
568
569
  eagerTasks,
569
570
  secretsEnabled,
570
571
  };
571
- let rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
572
-
573
- // When autoqa is active the report_tool_issue tool is in the tool set — nudge the agent.
574
- if (toolNames.includes("report_tool_issue")) {
575
- rendered += `\n\n<critical>\nThe \`${reportToolIssueToolName}\` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call \`${reportToolIssueToolName}\` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>`;
576
- }
577
-
572
+ const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
578
573
  const systemPrompt = [rendered];
579
574
  const projectPrompt = resolvedCustomPrompt ? "" : prompt.render(projectPromptTemplate, data).trim();
580
575
  if (projectPrompt) {
581
576
  systemPrompt.push(projectPrompt);
582
577
  }
578
+ const nowPrompt = prompt.render(nowPromptTemplate, data).trim();
579
+ if (nowPrompt) {
580
+ systemPrompt.push(nowPrompt);
581
+ }
583
582
 
584
583
  return { systemPrompt };
585
584
  }
@@ -139,6 +139,7 @@ export interface ExecutorOptions {
139
139
  agent: AgentDefinition;
140
140
  task: string;
141
141
  assignment?: string;
142
+ context?: string;
142
143
  description?: string;
143
144
  index: number;
144
145
  id: string;
@@ -994,17 +995,20 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
994
995
  skills: options.skills,
995
996
  promptTemplates: options.promptTemplates,
996
997
  workspaceTree: options.workspaceTree,
997
- systemPrompt: defaultPrompt => [
998
- prompt.render(subagentSystemPromptTemplate, {
999
- base: defaultPrompt.join("\n\n"),
998
+ systemPrompt: defaultPrompt => {
999
+ const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
1000
1000
  agent: agent.systemPrompt,
1001
+ context: options.context?.trim() ?? "",
1001
1002
  worktree: worktree ?? "",
1002
1003
  outputSchema: normalizedOutputSchema,
1003
1004
  contextFile: options.contextFile,
1004
1005
  ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
1005
1006
  ircSelfId: ircEnabled ? id : "",
1006
- }),
1007
- ],
1007
+ });
1008
+ return defaultPrompt.length === 0
1009
+ ? [subagentPrompt]
1010
+ : [...defaultPrompt.slice(0, -1), subagentPrompt, defaultPrompt[defaultPrompt.length - 1]];
1011
+ },
1008
1012
  sessionManager,
1009
1013
  hasUI: false,
1010
1014
  spawns: spawnsEnv,
package/src/task/index.ts CHANGED
@@ -23,6 +23,7 @@ import type { ToolSession } from "..";
23
23
  import { resolveAgentModelPatterns } from "../config/model-resolver";
24
24
  import type { Theme } from "../modes/theme/theme";
25
25
  import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
26
+ import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
26
27
  import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
27
28
  import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
28
29
  import { formatBytes, formatDuration } from "../tools/render-utils";
@@ -38,7 +39,6 @@ import { AgentOutputManager } from "./output-manager";
38
39
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
39
40
  import { renderResult, renderCall as renderTaskCall } from "./render";
40
41
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
41
- import { renderTemplate } from "./template";
42
42
  import {
43
43
  type AgentDefinition,
44
44
  type AgentProgress,
@@ -65,6 +65,12 @@ import {
65
65
  type WorktreeBaseline,
66
66
  } from "./worktree";
67
67
 
68
+ function renderSubagentUserPrompt(assignment: string, simpleMode: TaskSimpleMode): string {
69
+ return prompt.render(subagentUserPromptTemplate, {
70
+ assignment: assignment.trim(),
71
+ independentMode: simpleMode === "independent",
72
+ });
73
+ }
68
74
  function createUsageTotals(): Usage {
69
75
  return {
70
76
  input: 0,
@@ -282,21 +288,19 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
282
288
  const uniqueIds = await outputManager.allocateBatch(taskItems.map(t => t.id));
283
289
  const fallbackAgentSource =
284
290
  this.#discoveredAgents.find(agent => agent.name === params.agent)?.source ?? "bundled";
285
- const { contextEnabled } = getTaskSimpleModeCapabilities(simpleMode);
286
- const sharedContext = contextEnabled ? params.context : undefined;
287
- const renderedTasks = taskItems.map(taskItem => renderTemplate(sharedContext, taskItem, simpleMode));
288
291
  const progressByTaskId = new Map<string, AgentProgress>();
289
- for (let index = 0; index < renderedTasks.length; index++) {
290
- const renderedTask = renderedTasks[index];
291
- progressByTaskId.set(renderedTask.id, {
292
+ for (let index = 0; index < taskItems.length; index++) {
293
+ const taskItem = taskItems[index];
294
+ const assignment = taskItem.assignment.trim();
295
+ progressByTaskId.set(taskItem.id, {
292
296
  index,
293
- id: renderedTask.id,
297
+ id: taskItem.id,
294
298
  agent: params.agent,
295
299
  agentSource: fallbackAgentSource,
296
300
  status: "pending",
297
- task: renderedTask.task,
298
- assignment: renderedTask.assignment,
299
- description: renderedTask.description,
301
+ task: renderSubagentUserPrompt(assignment, simpleMode),
302
+ assignment,
303
+ description: taskItem.description,
300
304
  recentTools: [],
301
305
  recentOutput: [],
302
306
  toolCount: 0,
@@ -506,7 +510,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
506
510
  const { agent: agentName, context, schema: outputSchema } = params;
507
511
  const simpleMode = this.#getTaskSimpleMode();
508
512
  const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
509
- const sharedContext = contextEnabled ? context : undefined;
513
+ const sharedContext = contextEnabled ? context?.trim() : undefined;
510
514
  const isolationMode = this.session.settings.get("task.isolation.mode");
511
515
  const isolationRequested = "isolated" in params ? params.isolated === true : false;
512
516
  const isIsolated = isolationMode !== "none" && isolationRequested;
@@ -802,8 +806,6 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
802
806
  }
803
807
  const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: uniqueIds[i] }));
804
808
 
805
- // Build full prompts using shared context only when the current task mode allows it.
806
- const tasksWithContext = tasksWithUniqueIds.map(t => renderTemplate(sharedContext, t, simpleMode));
807
809
  const availableSkills = [...(this.session.skills ?? [])];
808
810
  const contextFiles = this.session.contextFiles?.filter(
809
811
  file => path.basename(file.path).toLowerCase() !== "agents.md",
@@ -811,34 +813,36 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
811
813
  const promptTemplates = this.session.promptTemplates;
812
814
 
813
815
  // Initialize progress for all tasks
814
- for (let i = 0; i < tasksWithContext.length; i++) {
815
- const t = tasksWithContext[i];
816
+ for (let i = 0; i < tasksWithUniqueIds.length; i++) {
817
+ const taskItem = tasksWithUniqueIds[i];
818
+ const assignment = taskItem.assignment.trim();
816
819
  progressMap.set(i, {
817
820
  index: i,
818
- id: t.id,
821
+ id: taskItem.id,
819
822
  agent: agentName,
820
823
  agentSource: agent.source,
821
824
  status: "pending",
822
- task: t.task,
823
- assignment: t.assignment,
825
+ task: renderSubagentUserPrompt(assignment, simpleMode),
826
+ assignment,
824
827
  recentTools: [],
825
828
  recentOutput: [],
826
829
  toolCount: 0,
827
830
  tokens: 0,
828
831
  durationMs: 0,
829
832
  modelOverride,
830
- description: t.description,
833
+ description: taskItem.description,
831
834
  });
832
835
  }
833
836
  emitProgress();
834
837
 
835
- const runTask = async (task: (typeof tasksWithContext)[number], index: number) => {
838
+ const runTask = async (task: (typeof tasksWithUniqueIds)[number], index: number) => {
836
839
  if (!isIsolated) {
837
840
  return runSubprocess({
838
841
  cwd: this.session.cwd,
839
842
  agent,
840
- task: task.task,
841
- assignment: task.assignment,
843
+ task: renderSubagentUserPrompt(task.assignment, simpleMode),
844
+ assignment: task.assignment.trim(),
845
+ context: sharedContext,
842
846
  description: task.description,
843
847
  index,
844
848
  id: task.id,
@@ -894,8 +898,9 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
894
898
  cwd: this.session.cwd,
895
899
  worktree: isolationDir,
896
900
  agent,
897
- task: task.task,
898
- assignment: task.assignment,
901
+ task: renderSubagentUserPrompt(task.assignment, simpleMode),
902
+ assignment: task.assignment.trim(),
903
+ context: sharedContext,
899
904
  description: task.description,
900
905
  index,
901
906
  id: task.id,
@@ -979,13 +984,14 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
979
984
  return result;
980
985
  } catch (err) {
981
986
  const message = err instanceof Error ? err.message : String(err);
987
+ const assignment = task.assignment.trim();
982
988
  return {
983
989
  index,
984
990
  id: task.id,
985
991
  agent: agent.name,
986
992
  agentSource: agent.source,
987
- task: task.task,
988
- assignment: task.assignment,
993
+ task: renderSubagentUserPrompt(assignment, simpleMode),
994
+ assignment,
989
995
  description: task.description,
990
996
  exitCode: 1,
991
997
  output: "",
@@ -1011,7 +1017,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
1011
1017
 
1012
1018
  // Execute in parallel with concurrency limit
1013
1019
  const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
1014
- tasksWithContext,
1020
+ tasksWithUniqueIds,
1015
1021
  maxConcurrency,
1016
1022
  runTask,
1017
1023
  signal,
@@ -1022,14 +1028,15 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
1022
1028
  if (result !== undefined) {
1023
1029
  return result;
1024
1030
  }
1025
- const task = tasksWithContext[index];
1031
+ const task = tasksWithUniqueIds[index];
1032
+ const assignment = task.assignment.trim();
1026
1033
  return {
1027
1034
  index,
1028
1035
  id: task.id,
1029
1036
  agent: agentName,
1030
1037
  agentSource: agent.source,
1031
- task: task.task,
1032
- assignment: task.assignment,
1038
+ task: renderSubagentUserPrompt(assignment, simpleMode),
1039
+ assignment,
1033
1040
  description: task.description,
1034
1041
  exitCode: 1,
1035
1042
  output: "",
package/src/tools/bash.ts CHANGED
@@ -16,7 +16,6 @@ import { getSixelLineMask } from "../utils/sixel";
16
16
  import type { ToolSession } from ".";
17
17
  import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
18
18
  import { checkBashInterception } from "./bash-interceptor";
19
- import { applyHeadTail } from "./bash-normalize";
20
19
  import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
21
20
  import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
22
21
  import { resolveToCwd } from "./path-utils";
@@ -50,8 +49,7 @@ const bashSchemaBase = Type.Object({
50
49
  ),
51
50
  timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 300 })),
52
51
  cwd: Type.Optional(Type.String({ description: "working directory", examples: ["src/", "/tmp"] })),
53
- head: Type.Optional(Type.Number({ description: "first n lines of output" })),
54
- tail: Type.Optional(Type.Number({ description: "last n lines of output" })),
52
+
55
53
  pty: Type.Optional(
56
54
  Type.Boolean({
57
55
  description: "run in pty mode",
@@ -75,8 +73,7 @@ export interface BashToolInput {
75
73
  env?: Record<string, string>;
76
74
  timeout?: number;
77
75
  cwd?: string;
78
- head?: number;
79
- tail?: number;
76
+
80
77
  async?: boolean;
81
78
  pty?: boolean;
82
79
  }
@@ -266,16 +263,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
266
263
  });
267
264
  }
268
265
 
269
- #formatResultOutput(result: BashResult | BashInteractiveResult, headLines?: number, tailLines?: number): string {
270
- let outputText = normalizeResultOutput(result);
271
- const headTailResult = applyHeadTail(outputText, headLines, tailLines);
272
- if (headTailResult.applied) {
273
- outputText = headTailResult.text;
274
- }
275
- if (!outputText) {
276
- outputText = "(no output)";
277
- }
278
- return outputText;
266
+ #formatResultOutput(result: BashResult | BashInteractiveResult): string {
267
+ const outputText = normalizeResultOutput(result);
268
+ return outputText || "(no output)";
279
269
  }
280
270
 
281
271
  #buildResultText(result: BashResult | BashInteractiveResult, timeoutSec: number, outputText: string): string {
@@ -297,11 +287,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
297
287
  #buildCompletedResult(
298
288
  result: BashResult | BashInteractiveResult,
299
289
  timeoutSec: number,
300
- headLines?: number,
301
- tailLines?: number,
302
290
  options: { requestedTimeoutSec?: number; notices?: string[] } = {},
303
291
  ): AgentToolResult<BashToolDetails> {
304
- const outputLines = [this.#formatResultOutput(result, headLines, tailLines)];
292
+ const outputLines = [this.#formatResultOutput(result)];
305
293
  const notices = options.notices?.filter(Boolean) ?? [];
306
294
  if (notices.length > 0) outputLines.push("", ...notices);
307
295
  const outputText = outputLines.join("\n");
@@ -356,8 +344,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
356
344
  timeoutSec: number;
357
345
  requestedTimeoutSec?: number;
358
346
  timeoutClampNotice?: string;
359
- headLines?: number;
360
- tailLines?: number;
347
+
361
348
  resolvedEnv?: Record<string, string>;
362
349
  onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
363
350
  startBackgrounded: boolean;
@@ -394,16 +381,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
394
381
  },
395
382
  onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
396
383
  });
397
- const finalResult = this.#buildCompletedResult(
398
- result,
399
- options.timeoutSec,
400
- options.headLines,
401
- options.tailLines,
402
- {
403
- requestedTimeoutSec: options.requestedTimeoutSec,
404
- notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
405
- },
406
- );
384
+ const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
385
+ requestedTimeoutSec: options.requestedTimeoutSec,
386
+ notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
387
+ });
407
388
  const finalText = this.#extractTextResult(finalResult);
408
389
  latestText = finalText;
409
390
  completion.resolve({ kind: "completed", result: finalResult });
@@ -481,8 +462,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
481
462
  env: rawEnv,
482
463
  timeout: rawTimeout = 300,
483
464
  cwd,
484
- head,
485
- tail,
465
+
486
466
  async: asyncRequested = false,
487
467
  pty = false,
488
468
  }: BashToolInput,
@@ -505,10 +485,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
505
485
  throw new ToolError("Async bash execution is disabled. Enable async.enabled to use async mode.");
506
486
  }
507
487
 
508
- // Only apply explicit head/tail params from tool input.
509
- const headLines = head;
510
- const tailLines = tail;
511
-
512
488
  // Check both the original command and the cwd-normalized command so
513
489
  // leading `cd ... &&` wrappers do not hide either shell-navigation rules
514
490
  // or the dedicated-tool command that follows the directory change.
@@ -583,8 +559,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
583
559
  timeoutSec,
584
560
  requestedTimeoutSec,
585
561
  timeoutClampNotice,
586
- headLines,
587
- tailLines,
562
+
588
563
  resolvedEnv,
589
564
  onUpdate,
590
565
  startBackgrounded: true,
@@ -605,8 +580,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
605
580
  timeoutSec,
606
581
  requestedTimeoutSec,
607
582
  timeoutClampNotice,
608
- headLines,
609
- tailLines,
583
+
610
584
  resolvedEnv,
611
585
  onUpdate,
612
586
  startBackgrounded,
@@ -675,7 +649,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
675
649
  if (isInteractiveResult(result) && result.timedOut) {
676
650
  throw new ToolError(normalizeResultOutput(result) || `Command timed out after ${timeoutSec} seconds`);
677
651
  }
678
- return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines, {
652
+ return this.#buildCompletedResult(result, timeoutSec, {
679
653
  requestedTimeoutSec,
680
654
  notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
681
655
  });
package/src/tools/eval.ts CHANGED
@@ -4,10 +4,10 @@ import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { Markdown, Text } from "@oh-my-pi/pi-tui";
5
5
  import { prompt } from "@oh-my-pi/pi-utils";
6
6
  import { type Static, Type } from "@sinclair/typebox";
7
- import { jsBackend, parseEvalInput, pythonBackend } from "../eval";
7
+ import { jsBackend, parseEvalInput, pythonBackend, sniffEvalLanguage } from "../eval";
8
8
  import type { ExecutorBackend } from "../eval/backend";
9
9
  import evalGrammar from "../eval/eval.lark" with { type: "text" };
10
- import type { ParsedEvalCell } from "../eval/parse";
10
+ import { ABORT_WARNING, type ParsedEvalCell } from "../eval/parse";
11
11
  import type { EvalCellResult, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
12
12
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
13
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
@@ -26,7 +26,7 @@ export const EVAL_DEFAULT_PREVIEW_LINES = 10;
26
26
 
27
27
  export const evalSchema = Type.Object({
28
28
  input: Type.String({
29
- description: "eval input as a sequence of `===== <info> =====` cell headers followed by code",
29
+ description: "eval input as a sequence of `*** Begin <LANG>` cell headers followed by code",
30
30
  }),
31
31
  });
32
32
  export type EvalToolParams = Static<typeof evalSchema>;
@@ -131,33 +131,6 @@ function timeoutSecondsFromMs(timeoutMs: number): number {
131
131
  return clampTimeout("eval", timeoutMs / 1000);
132
132
  }
133
133
 
134
- /**
135
- * Best-effort language sniff for cells with no explicit `language`.
136
- *
137
- * Order:
138
- * 1. Shebang on first line (`#!/usr/bin/env python`, `#!/usr/bin/env node`, etc.)
139
- * 2. Strong syntactic markers unique to one language. We bias false negatives over
140
- * false positives — anything ambiguous returns `undefined` and the caller falls
141
- * back to the default-backend rules.
142
- */
143
- function sniffLanguage(code: string): EvalLanguage | undefined {
144
- const stripped = code.replace(/^\s+/, "");
145
- if (stripped.startsWith("#!")) {
146
- const firstLine = stripped.split("\n", 1)[0]!.toLowerCase();
147
- if (/(\bpython\d?\b|\bipython\b)/.test(firstLine)) return "python";
148
- if (/(\bnode\b|\bbun\b|\bdeno\b|\bjavascript\b|\bjs\b)/.test(firstLine)) return "js";
149
- }
150
- const jsMarkers =
151
- /(^|\n)\s*(const|let|var|async\s+function|function\s*\*?\s*[\w$]*\s*\(|import\s+[^\n]+\sfrom\s|export\s+(default|const|let|function|class|async)|require\s*\(|console\.\w+\s*\(|=>|;\s*$)/m;
152
- const pyMarkers =
153
- /(^|\n)\s*(def\s+\w+\s*\(|from\s+[\w.]+\s+import|import\s+\w+(\s+as\s+\w+)?\s*$|class\s+\w+\s*[(:]|print\s*\(|elif\s+[^\n]*:|with\s+[^\n]+:\s*$|@[\w.]+\s*$)/m;
154
- const hasJs = jsMarkers.test(code);
155
- const hasPy = pyMarkers.test(code);
156
- if (hasJs && !hasPy) return "js";
157
- if (hasPy && !hasJs) return "python";
158
- return undefined;
159
- }
160
-
161
134
  async function resolveBackend(
162
135
  session: ToolSession,
163
136
  requested: EvalLanguage | undefined,
@@ -180,7 +153,7 @@ async function resolveBackend(
180
153
  return { backend: jsBackend, fallback: false };
181
154
  }
182
155
  // Auto-detect.
183
- const sniffed = sniffLanguage(code);
156
+ const sniffed = sniffEvalLanguage(code);
184
157
  if (sniffed === "python" && allowPy && (await pythonBackend.isAvailable(session))) {
185
158
  return { backend: pythonBackend, fallback: false };
186
159
  }
@@ -446,10 +419,11 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
446
419
  pushUpdate();
447
420
  const errorMsg = result.output || "Command aborted";
448
421
  const combinedOutput = cellOutputs.join("\n\n");
422
+ const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
449
423
  const outputText =
450
- cells.length > 1
424
+ (cells.length > 1
451
425
  ? `${combinedOutput}\n\nCell ${i + 1} aborted: ${errorMsg}`
452
- : combinedOutput || errorMsg;
426
+ : combinedOutput || errorMsg) + abortSuffix;
453
427
 
454
428
  const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
455
429
  const details: EvalToolDetails = {
@@ -473,12 +447,13 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
473
447
  cellResult.status = "error";
474
448
  pushUpdate();
475
449
  const combinedOutput = cellOutputs.join("\n\n");
450
+ const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
476
451
  const outputText =
477
- cells.length > 1
452
+ (cells.length > 1
478
453
  ? `${combinedOutput}\n\nCell ${i + 1} failed (exit code ${result.exitCode}). Earlier cells succeeded—their state persists. Fix only cell ${i + 1}.`
479
454
  : combinedOutput
480
455
  ? `${combinedOutput}\n\nCommand exited with code ${result.exitCode}`
481
- : `Command exited with code ${result.exitCode}`;
456
+ : `Command exited with code ${result.exitCode}`) + abortSuffix;
482
457
 
483
458
  const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
484
459
  const details: EvalToolDetails = {
@@ -503,8 +478,10 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
503
478
  }
504
479
 
505
480
  const combinedOutput = cellOutputs.join("\n\n");
481
+ const abortSuffix = parsedInput.aborted ? `\n\n${ABORT_WARNING}` : "";
506
482
  const outputText =
507
- combinedOutput || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)");
483
+ (combinedOutput || (jsonOutputs.length > 0 || images.length > 0 ? "(no text output)" : "(no output)")) +
484
+ abortSuffix;
508
485
  const summaryForMeta = await summarizeFinal(combinedOutput, finalizeOutput);
509
486
 
510
487
  const details: EvalToolDetails = {