@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/cli.js +3336 -3318
  3. package/dist/types/advisor/advise-tool.d.ts +12 -1
  4. package/dist/types/advisor/runtime.d.ts +41 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/update-cli.d.ts +4 -1
  7. package/dist/types/cli/usage-cli.d.ts +3 -0
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/config/api-key-resolver.d.ts +2 -2
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +8 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
  14. package/dist/types/eval/bridge-timeout.d.ts +9 -1
  15. package/dist/types/eval/js/context-manager.d.ts +5 -3
  16. package/dist/types/eval/js/process-entry.d.ts +6 -0
  17. package/dist/types/eval/js/worker-core.d.ts +15 -1
  18. package/dist/types/eval/py/spawn-options.d.ts +10 -0
  19. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  20. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
  24. package/dist/types/main.d.ts +1 -0
  25. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  26. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  27. package/dist/types/modes/orchestrate.d.ts +1 -1
  28. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  29. package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
  30. package/dist/types/modes/ultrathink.d.ts +1 -1
  31. package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
  32. package/dist/types/modes/workflow.d.ts +1 -1
  33. package/dist/types/session/agent-session.d.ts +6 -0
  34. package/dist/types/session/exit-diagnostics.d.ts +11 -0
  35. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
  36. package/dist/types/subprocess/worker-client.d.ts +6 -0
  37. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  38. package/dist/types/web/search/provider.d.ts +10 -3
  39. package/dist/types/web/search/providers/codex.d.ts +5 -4
  40. package/package.json +12 -12
  41. package/src/advisor/__tests__/advisor.test.ts +830 -42
  42. package/src/advisor/advise-tool.ts +17 -1
  43. package/src/advisor/runtime.ts +288 -67
  44. package/src/autolearn/controller.ts +15 -3
  45. package/src/cli/args.ts +12 -0
  46. package/src/cli/auth-broker-cli.ts +30 -11
  47. package/src/cli/auth-gateway-cli.ts +5 -1
  48. package/src/cli/dry-balance-cli.ts +14 -4
  49. package/src/cli/flag-tables.ts +21 -7
  50. package/src/cli/update-cli.ts +62 -11
  51. package/src/cli/usage-cli.ts +58 -5
  52. package/src/cli/usage-error.ts +7 -0
  53. package/src/cli.ts +23 -1
  54. package/src/commands/acp.ts +11 -2
  55. package/src/commands/launch.ts +12 -3
  56. package/src/commands/token.ts +3 -1
  57. package/src/config/api-key-resolver.ts +12 -3
  58. package/src/config/config-file.ts +30 -12
  59. package/src/config/model-registry.ts +7 -7
  60. package/src/config/model-resolver.ts +21 -7
  61. package/src/config/models-config.ts +1 -1
  62. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  63. package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
  64. package/src/eval/__tests__/js-context-manager.test.ts +158 -1
  65. package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
  66. package/src/eval/__tests__/process-entry-import.test.ts +27 -0
  67. package/src/eval/agent-bridge.ts +121 -116
  68. package/src/eval/bridge-timeout.ts +20 -2
  69. package/src/eval/executor-base.ts +85 -7
  70. package/src/eval/jl/kernel.ts +2 -1
  71. package/src/eval/js/context-manager.ts +109 -32
  72. package/src/eval/js/process-entry.ts +27 -0
  73. package/src/eval/js/shared/runtime.ts +1 -1
  74. package/src/eval/js/worker-core.ts +70 -9
  75. package/src/eval/js/worker-entry.ts +1 -1
  76. package/src/eval/py/kernel.ts +2 -1
  77. package/src/eval/py/spawn-options.ts +13 -0
  78. package/src/eval/py/tool-bridge.ts +13 -14
  79. package/src/eval/rb/kernel.ts +2 -1
  80. package/src/extensibility/custom-tools/types.ts +3 -0
  81. package/src/extensibility/extensions/runner.ts +3 -0
  82. package/src/extensibility/extensions/types.ts +3 -0
  83. package/src/extensibility/plugins/manager.ts +21 -0
  84. package/src/internal-urls/memory-protocol.ts +13 -9
  85. package/src/lsp/client.ts +7 -1
  86. package/src/main.ts +29 -0
  87. package/src/mcp/tool-bridge.ts +57 -6
  88. package/src/modes/components/chat-transcript-builder.ts +22 -1
  89. package/src/modes/components/status-line/component.ts +10 -1
  90. package/src/modes/components/transcript-container.ts +110 -7
  91. package/src/modes/controllers/command-controller.ts +12 -4
  92. package/src/modes/controllers/event-controller.ts +80 -15
  93. package/src/modes/controllers/selector-controller.ts +15 -3
  94. package/src/modes/magic-keyword-boundary.ts +23 -0
  95. package/src/modes/orchestrate.ts +6 -5
  96. package/src/modes/print-mode.ts +9 -0
  97. package/src/modes/rpc/host-tools.ts +15 -0
  98. package/src/modes/rpc/rpc-mode.ts +123 -48
  99. package/src/modes/ultrathink.ts +6 -5
  100. package/src/modes/utils/transcript-render-helpers.ts +54 -0
  101. package/src/modes/utils/ui-helpers.ts +27 -1
  102. package/src/modes/workflow.ts +6 -5
  103. package/src/prompts/advisor/system.md +1 -0
  104. package/src/sdk.ts +33 -3
  105. package/src/session/agent-session.ts +239 -17
  106. package/src/session/exit-diagnostics.ts +108 -0
  107. package/src/session/streaming-output.ts +40 -12
  108. package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
  109. package/src/slash-commands/helpers/logout.ts +23 -3
  110. package/src/slash-commands/helpers/usage-report.ts +14 -2
  111. package/src/subprocess/worker-client.ts +9 -2
  112. package/src/task/executor.ts +8 -0
  113. package/src/task/render.test.ts +36 -0
  114. package/src/task/render.ts +55 -43
  115. package/src/tools/bash-skill-urls.ts +4 -1
  116. package/src/tools/bash.ts +1 -0
  117. package/src/tools/write.ts +82 -9
  118. package/src/tools/yield.ts +29 -1
  119. package/src/web/search/index.ts +39 -22
  120. package/src/web/search/provider.ts +33 -16
  121. package/src/web/search/providers/codex.ts +68 -21
@@ -136,6 +136,60 @@ export function assistantHasVisibleContent(message: AssistantAgentMessage): bool
136
136
  );
137
137
  }
138
138
 
139
+ /**
140
+ * Split mixed assistant turns into visible text before tool execution and
141
+ * visible text segments that must render immediately after the preceding tool.
142
+ * Cursor can return intro text, tool calls, progress text, and the final answer
143
+ * in one assistant message; keeping every text block in the leading assistant
144
+ * block buries post-tool text above tool results in the transcript.
145
+ */
146
+ export function splitAssistantMessageToolTimeline(message: AssistantAgentMessage): {
147
+ beforeTools: AssistantAgentMessage;
148
+ afterToolCalls: ReadonlyMap<string, AssistantAgentMessage>;
149
+ hasToolCalls: boolean;
150
+ } {
151
+ const beforeTools: AssistantAgentMessage["content"] = [];
152
+ const afterToolCalls = new Map<string, AssistantAgentMessage>();
153
+ let pendingAfterTool: AssistantAgentMessage["content"] = [];
154
+ let lastToolCallId: string | undefined;
155
+ let sawToolCall = false;
156
+
157
+ const displaySegment = (content: AssistantAgentMessage["content"]): AssistantAgentMessage => ({
158
+ ...message,
159
+ content,
160
+ stopReason: "stop",
161
+ errorMessage: undefined,
162
+ retryRecovery: undefined,
163
+ });
164
+
165
+ const flushPendingAfterTool = () => {
166
+ if (!lastToolCallId || pendingAfterTool.length === 0) return;
167
+ afterToolCalls.set(lastToolCallId, displaySegment(pendingAfterTool));
168
+ pendingAfterTool = [];
169
+ };
170
+
171
+ for (const content of message.content) {
172
+ if (content.type === "toolCall") {
173
+ flushPendingAfterTool();
174
+ sawToolCall = true;
175
+ lastToolCallId = content.id;
176
+ continue;
177
+ }
178
+ if (sawToolCall) {
179
+ pendingAfterTool.push(content);
180
+ } else {
181
+ beforeTools.push(content);
182
+ }
183
+ }
184
+ flushPendingAfterTool();
185
+
186
+ if (!sawToolCall) {
187
+ return { beforeTools: message, afterToolCalls, hasToolCalls: false };
188
+ }
189
+
190
+ return { beforeTools: displaySegment(beforeTools), afterToolCalls, hasToolCalls: true };
191
+ }
192
+
139
193
  /**
140
194
  * Normalize raw tool-call arguments to a plain record, collapsing non-object or
141
195
  * array values to an empty object.
@@ -57,6 +57,7 @@ import {
57
57
  buildIrcMessageCard,
58
58
  normalizeToolArgs,
59
59
  resolveAssistantErrorPresentation,
60
+ splitAssistantMessageToolTimeline,
60
61
  } from "./transcript-render-helpers";
61
62
 
62
63
  type TextBlock = { type: "text"; text: string };
@@ -251,7 +252,10 @@ export class UiHelpers {
251
252
  break;
252
253
  }
253
254
  case "assistant": {
254
- const assistantComponent = createAssistantMessageComponent(this.ctx, message);
255
+ const assistantComponent = createAssistantMessageComponent(
256
+ this.ctx,
257
+ splitAssistantMessageToolTimeline(message).beforeTools,
258
+ );
255
259
  this.ctx.chatContainer.addChild(assistantComponent);
256
260
  break;
257
261
  }
@@ -357,6 +361,7 @@ export class UiHelpers {
357
361
  if (message.role !== "toolResult") flushPendingUsage();
358
362
  // Assistant messages need special handling for tool calls
359
363
  if (message.role === "assistant") {
364
+ const timeline = splitAssistantMessageToolTimeline(message);
360
365
  this.ctx.addMessageToChat(message);
361
366
  const lastChild = this.ctx.chatContainer.children[this.ctx.chatContainer.children.length - 1];
362
367
  const assistantComponent = lastChild instanceof AssistantMessageComponent ? lastChild : undefined;
@@ -383,6 +388,11 @@ export class UiHelpers {
383
388
  const errorPresentation = resolveAssistantErrorPresentation(message, this.ctx.viewSession.retryAttempt);
384
389
  const hasErrorStop = errorPresentation.kind === "full";
385
390
  const errorMessage = hasErrorStop ? errorPresentation.text : null;
391
+ const appendAssistantSegment = (segment: AssistantMessage | undefined) => {
392
+ if (!segment || !assistantHasVisibleContent(segment)) return;
393
+ const component = createAssistantMessageComponent(this.ctx, segment);
394
+ this.ctx.chatContainer.addChild(component);
395
+ };
386
396
 
387
397
  // Render tool call components
388
398
  for (const content of message.content) {
@@ -390,6 +400,7 @@ export class UiHelpers {
390
400
  continue;
391
401
  }
392
402
  resolveWaitingPoll(content.name);
403
+ const afterToolSegment = timeline.afterToolCalls.get(content.id);
393
404
 
394
405
  if (
395
406
  content.name === "read" &&
@@ -410,6 +421,19 @@ export class UiHelpers {
410
421
  false,
411
422
  content.id,
412
423
  );
424
+ } else if (afterToolSegment) {
425
+ if (!readGroup) {
426
+ readGroup = new ReadToolGroupComponent({
427
+ showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
428
+ });
429
+ readGroup.setExpanded(this.ctx.toolOutputExpanded);
430
+ this.ctx.chatContainer.addChild(readGroup);
431
+ }
432
+ readGroup.updateArgs(content.arguments, content.id);
433
+ this.ctx.pendingTools.set(content.id, readGroup);
434
+ if (assistantComponent) {
435
+ readToolCallAssistantComponents.set(content.id, assistantComponent);
436
+ }
413
437
  } else {
414
438
  const normalizedArgs = normalizeToolArgs(content.arguments);
415
439
  readToolCallArgs.set(content.id, normalizedArgs);
@@ -417,6 +441,7 @@ export class UiHelpers {
417
441
  readToolCallAssistantComponents.set(content.id, assistantComponent);
418
442
  }
419
443
  }
444
+ appendAssistantSegment(afterToolSegment);
420
445
  continue;
421
446
  }
422
447
 
@@ -464,6 +489,7 @@ export class UiHelpers {
464
489
  } else {
465
490
  this.ctx.pendingTools.set(content.id, component);
466
491
  }
492
+ appendAssistantSegment(afterToolSegment);
467
493
  }
468
494
  // Dangling toolCalls (no result on the resolved path — failed or
469
495
  // retried turns, results on sibling branches) were stripped by the
@@ -1,6 +1,7 @@
1
1
  import { prompt } from "@oh-my-pi/pi-utils";
2
2
  import workflowNoticeTemplate from "../prompts/system/workflow-notice.md" with { type: "text" };
3
3
  import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
4
+ import { magicKeywordRegex } from "./magic-keyword-boundary";
4
5
  import { keywordInProse } from "./markdown-prose";
5
6
 
6
7
  /**
@@ -10,13 +11,13 @@ import { keywordInProse } from "./markdown-prose";
10
11
  * amber→green gradient ({@link highlightWorkflow}); submitting a message that
11
12
  * mentions it appends a hidden workflow notice that steers the model to author
12
13
  * a deterministic multi-subagent workflow through the active task schema.
13
- * Matching is whitespace-delimited and case-sensitive (lowercase only) —
14
+ * Matching is prose-delimited and case-sensitive (lowercase only) —
14
15
  * "workflowz" triggers, but "workflowzed", "Workflowz", and "workflowz.ts"
15
16
  * never do.
16
17
  */
17
18
 
18
- // Detection: lowercase keyword flanked by whitespace or a string edge. Non-global so `.test` stays stateless.
19
- const WORKFLOW_WORD = /(?<!\S)workflowz(?!\S)/;
19
+ // Detection: lowercase keyword flanked by prose punctuation, whitespace, or a string edge.
20
+ const WORKFLOW_WORD = magicKeywordRegex("workflowz");
20
21
 
21
22
  /** WORKFLOW_NOTICE is the default hidden notice for sessions with batched task calls enabled. */
22
23
  export const WORKFLOW_NOTICE: string = renderWorkflowNotice({ taskBatch: true });
@@ -28,7 +29,7 @@ export function renderWorkflowNotice({ taskBatch }: { taskBatch: boolean }): str
28
29
 
29
30
  /**
30
31
  * Whether `text` contains the standalone keyword "workflowz"
31
- * (lowercase, whitespace-delimited) in prose — never inside a code block, inline
32
+ * (lowercase, prose-delimited) in prose — never inside a code block, inline
32
33
  * code span, or XML/HTML section.
33
34
  */
34
35
  export function containsWorkflow(text: string): boolean {
@@ -42,7 +43,7 @@ export function containsWorkflow(text: string): boolean {
42
43
  */
43
44
  export const highlightWorkflow: KeywordHighlighter = createGradientHighlighter({
44
45
  probe: /workflowz/,
45
- highlight: /(?<!\S)workflowz(?!\S)/g,
46
+ highlight: magicKeywordRegex("workflowz", "g"),
46
47
  stops: 14,
47
48
  hue: t => 30 + t * 120,
48
49
  });
@@ -28,6 +28,7 @@ Keep exploration lean:
28
28
  - NEVER restate information the agent already has, including errors they have seen.
29
29
  - Examples: type errors, LSP diagnostics, failed builds, failing tests, lint.
30
30
  - NEVER repeat advice you already gave, and NEVER send the same advice twice; give the agent room to act on prior advice before raising the same theme again.
31
+ - When an update heading is tagged `[in progress — more steps follow]`, the agent is mid-turn and has not finished yet. Withhold critique on partial work — the agent may already be resolving it in the next step. Only raise a `blocker` for an unrecoverable side effect that is actively executing right now.
31
32
  - NEVER nitpick about things user stated they are okay with. You are the advocate for the user.
32
33
  - You are user-aligned: treat the user's word as truth, their frustration as justified, their stated requirements as binding.
33
34
  </communication>
package/src/sdk.ts CHANGED
@@ -112,6 +112,7 @@ import {
112
112
  import { AgentSession, type PlanYolo, type Prewalk } from "./session/agent-session";
113
113
  import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
114
114
  import type { AuthStorage } from "./session/auth-storage";
115
+ import { createInterruptedTurnAbortMessage } from "./session/exit-diagnostics";
115
116
  import {
116
117
  type CustomMessage,
117
118
  convertToLlm,
@@ -864,6 +865,7 @@ function createCustomToolContext(ctx: ExtensionContext): CustomToolContext {
864
865
  isIdle: ctx.isIdle,
865
866
  hasQueuedMessages: ctx.hasPendingMessages,
866
867
  abort: ctx.abort,
868
+ localProtocolOptions: ctx.localProtocolOptions,
867
869
  };
868
870
  }
869
871
 
@@ -1271,11 +1273,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1271
1273
  }
1272
1274
  const secretsEnabled = obfuscator?.hasSecrets() === true;
1273
1275
 
1274
- // Check if session has existing data to restore
1275
- const existingSession = logger.time("loadSessionContext", () =>
1276
+ // An abnormal process exit after a non-terminal message tail is durable
1277
+ // evidence that the old process can no longer finish that turn. Preserve the
1278
+ // partial transcript and append one terminal aborted assistant record before
1279
+ // rebuilding runtime context. The helper is idempotent once that record exists.
1280
+ let existingBranch = logger.time("getSessionBranch", () => sessionManager.getBranch());
1281
+ const interruptedTurnAbort = createInterruptedTurnAbortMessage(existingBranch);
1282
+ if (interruptedTurnAbort) {
1283
+ sessionManager.appendMessage(interruptedTurnAbort);
1284
+ existingBranch = logger.time("getRecoveredSessionBranch", () => sessionManager.getBranch());
1285
+ }
1286
+ let existingSession = logger.time("loadSessionContext", () =>
1276
1287
  deobfuscateSessionContext(sessionManager.buildSessionContext(), obfuscator),
1277
1288
  );
1278
- const existingBranch = logger.time("getSessionBranch", () => sessionManager.getBranch());
1279
1289
  const hasExistingSession = existingBranch.length > 0;
1280
1290
  const hasThinkingEntry = existingBranch.some(entry => entry.type === "thinking_level_change");
1281
1291
  const hasServiceTierEntry = existingBranch.some(entry => entry.type === "service_tier_change");
@@ -2166,6 +2176,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2166
2176
  }
2167
2177
  }
2168
2178
 
2179
+ // A first-turn user tail has no assistant metadata to copy. Once startup
2180
+ // has selected its final model, use that model to terminate the
2181
+ // interrupted turn before the live agent consumes the restored context.
2182
+ if (model) {
2183
+ const selectedModelAbort = createInterruptedTurnAbortMessage(existingBranch, {
2184
+ api: model.api,
2185
+ provider: model.provider,
2186
+ model: model.id,
2187
+ });
2188
+ if (selectedModelAbort) {
2189
+ sessionManager.appendMessage(selectedModelAbort);
2190
+ existingBranch = logger.time("getRecoveredUserTailBranch", () => sessionManager.getBranch());
2191
+ existingSession = logger.time("loadRecoveredUserTailContext", () =>
2192
+ deobfuscateSessionContext(sessionManager.buildSessionContext(), obfuscator),
2193
+ );
2194
+ }
2195
+ }
2196
+
2169
2197
  // Discover custom commands (TypeScript slash commands)
2170
2198
  const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
2171
2199
  ? { commands: [], errors: [] }
@@ -2191,6 +2219,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2191
2219
  modelRegistry,
2192
2220
  () => (hasSession ? createSessionMemoryRuntimeContext(session, agentDir, cwd) : undefined),
2193
2221
  settings,
2222
+ localProtocolOptions,
2194
2223
  );
2195
2224
 
2196
2225
  credentialDisabledTarget = extensionRunner;
@@ -2209,6 +2238,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2209
2238
  session.abort({ reason: USER_INTERRUPT_LABEL });
2210
2239
  },
2211
2240
  settings,
2241
+ localProtocolOptions,
2212
2242
  autoApprove: options.autoApprove ?? false,
2213
2243
  });
2214
2244
  const toolContextStore = new ToolContextStore(getSessionContext);