@omercnet/paseo-omp 0.4.0-next.114.1 → 0.4.0-next.120.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.
@@ -1,7 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import type {
3
3
  ProviderConfigState,
4
- ProviderContent,
5
4
  ProviderError,
6
5
  ProviderEvent,
7
6
  ProviderInput,
@@ -10,7 +9,6 @@ import type {
10
9
  ProviderToolCallDetail,
11
10
  ProviderUsage,
12
11
  } from "@getpaseo/plugin/server/provider";
13
- import { getForgeDefinitionOrNeutral } from "@getpaseo/protocol/forge-manifest";
14
12
  import type {
15
13
  OmpOperationalFailure,
16
14
  OmpOperationalFailureReporter,
@@ -30,13 +28,12 @@ import {
30
28
  withCommittedOmpSelection,
31
29
  } from "./config-normalization";
32
30
  import { OmpHostToolsBridge, type OmpMcpConnector, validateOmpHostToolConfig } from "./host-tools";
33
- import { isOmpImageMimeType, isValidImagePayload, OmpImageMaterializer } from "./image";
31
+ import { isOmpImageMimeType, OmpImageMaterializer } from "./image";
34
32
  import type {
35
33
  OmpAvailableCommand,
36
34
  OmpBranchResult,
37
35
  OmpCompactionResult,
38
36
  OmpExtensionUiResponse,
39
- OmpImage,
40
37
  OmpMessage,
41
38
  OmpModel,
42
39
  OmpRpcEvent,
@@ -44,11 +41,19 @@ import type {
44
41
  OmpRuntimeSession,
45
42
  OmpSessionState,
46
43
  OmpSessionStats,
47
- OmpStartOptions,
48
44
  OmpToolApprovalCancel,
49
45
  OmpToolApprovalRequest,
50
46
  } from "./omp-rpc";
51
- import { buildOmpSpawnRequest, OmpRpcRequestRejectedError } from "./omp-rpc";
47
+ import { OmpRpcRequestRejectedError } from "./omp-rpc";
48
+ import { buildOmpSpawnRequest, type OmpStartOptions } from "./omp-rpc-environment";
49
+ import {
50
+ inlinePromptFrameBytes,
51
+ isSafeCommandName,
52
+ MAX_PROMPT_TEXT_LENGTH,
53
+ type OmpPromptPayload,
54
+ promptPayload,
55
+ slashCommandName,
56
+ } from "./prompt-payload";
52
57
  import {
53
58
  BoundedStringSet,
54
59
  boundedJsonBytes,
@@ -94,9 +99,6 @@ const USAGE_REFRESH_MS = 100;
94
99
  const FINAL_USAGE_WAIT_MS = 250;
95
100
  const COMPACTION_MAX_WAIT_MS = 5 * 60_000;
96
101
  const AGENT_END_SETTLE_MS = 5_000;
97
- const MAX_PROMPT_PARTS = 64;
98
- const MAX_PROMPT_TEXT_LENGTH = 1024 * 1024;
99
- const RPC_REQUEST_ID_BYTES = 36;
100
102
  const MAX_AGENT_END_CORRELATION_MESSAGES = 512;
101
103
  const MAX_TRACKED_ENTRY_IDS = 1_024;
102
104
  const MAX_UNCLAIMED_BRANCH_ENTRIES = 1_024;
@@ -407,240 +409,6 @@ async function settleSessionCleanup(promises: readonly Promise<void>[]): Promise
407
409
  throw new AggregateError(failures, "OMP session initialization cleanup failed");
408
410
  }
409
411
  }
410
- function isSafeCommandName(name: string): boolean {
411
- return /^[A-Za-z0-9][A-Za-z0-9._-]*(?::[A-Za-z0-9][A-Za-z0-9._-]*)*$/u.test(name);
412
- }
413
-
414
- type OmpPromptPayload = { text: string; images: OmpImage[]; commandName?: string };
415
-
416
- const REVIEW_LINE_MARKERS = { add: "+", remove: "-", context: " " } as const;
417
-
418
- function renderPromptAttachmentAsText(part: Exclude<ProviderContent, { type: "image" }>): string {
419
- switch (part.type) {
420
- case "forge_change_request": {
421
- return renderChangeRequestAttachment({
422
- forge: part.forge ?? "github",
423
- number: part.number,
424
- title: part.title,
425
- url: part.url,
426
- body: part.body,
427
- projectPath: part.projectPath,
428
- baseRefName: part.baseRefName,
429
- headRefName: part.headRefName,
430
- });
431
- }
432
- case "github_pr": {
433
- return renderChangeRequestAttachment({
434
- forge: "github",
435
- number: part.number,
436
- title: part.title,
437
- url: part.url,
438
- body: part.body,
439
- baseRefName: part.baseRefName,
440
- headRefName: part.headRefName,
441
- });
442
- }
443
- case "forge_issue": {
444
- return renderIssueAttachment({
445
- forge: part.forge ?? "github",
446
- number: part.number,
447
- title: part.title,
448
- url: part.url,
449
- body: part.body,
450
- projectPath: part.projectPath,
451
- });
452
- }
453
- case "github_issue": {
454
- return renderIssueAttachment({
455
- forge: "github",
456
- number: part.number,
457
- title: part.title,
458
- url: part.url,
459
- body: part.body,
460
- });
461
- }
462
- case "text": {
463
- return part.text;
464
- }
465
- case "review": {
466
- const lines = [`Paseo review attachment (${part.mode})`, `CWD: ${part.cwd}`];
467
- if (part.baseRef) {
468
- lines.push(`Base: ${part.baseRef}`);
469
- }
470
- part.comments.forEach((comment, index) => {
471
- lines.push(
472
- "",
473
- `Comment ${index + 1}: ${comment.filePath}:${comment.side}:${comment.lineNumber}`,
474
- comment.body,
475
- comment.context.hunkHeader,
476
- );
477
- const target = comment.context.targetLine;
478
- for (const line of comment.context.lines) {
479
- const isTarget =
480
- line.oldLineNumber === target.oldLineNumber &&
481
- line.newLineNumber === target.newLineNumber &&
482
- line.type === target.type &&
483
- line.content === target.content;
484
- const prefix = isTarget ? "> " : " ";
485
- const oldLn = padLineNumber(line.oldLineNumber);
486
- const newLn = padLineNumber(line.newLineNumber);
487
- lines.push(`${prefix}${oldLn} ${newLn} ${REVIEW_LINE_MARKERS[line.type]}${line.content}`);
488
- }
489
- });
490
- return lines.join("\n");
491
- }
492
- case "uploaded_file": {
493
- return [
494
- `Uploaded file: ${part.fileName}`,
495
- `Path: ${part.path}`,
496
- `MIME: ${part.mimeType}`,
497
- `Size: ${part.size} bytes`,
498
- ].join("\n");
499
- }
500
- default:
501
- throw new Error("unreachable");
502
- }
503
- }
504
-
505
- function renderChangeRequestAttachment(input: {
506
- forge: string;
507
- number: number;
508
- title: string;
509
- url: string;
510
- body?: string | null;
511
- projectPath?: string;
512
- baseRefName?: string | null;
513
- headRefName?: string | null;
514
- }): string {
515
- const lines = [
516
- `${formatForgeLabel(input.forge)} ${formatChangeRequestAbbrev(input.forge)} ${formatChangeRequestNumber(input.forge, input.number)}: ${input.title}`,
517
- input.url,
518
- ];
519
- if (input.projectPath) {
520
- lines.push(`Project: ${input.projectPath}`);
521
- }
522
- if (input.baseRefName) {
523
- lines.push(`Base: ${input.baseRefName}`);
524
- }
525
- if (input.headRefName) {
526
- lines.push(`Head: ${input.headRefName}`);
527
- }
528
- if (input.body) {
529
- lines.push("", input.body);
530
- }
531
- return lines.join("\n");
532
- }
533
-
534
- function renderIssueAttachment(input: {
535
- forge: string;
536
- number: number;
537
- title: string;
538
- url: string;
539
- body?: string | null;
540
- projectPath?: string;
541
- }): string {
542
- const lines = [
543
- `${formatForgeLabel(input.forge)} Issue ${formatIssueNumber(input.forge, input.number)}: ${input.title}`,
544
- input.url,
545
- ];
546
- if (input.projectPath) {
547
- lines.push(`Project: ${input.projectPath}`);
548
- }
549
- if (input.body) {
550
- lines.push("", input.body);
551
- }
552
- return lines.join("\n");
553
- }
554
-
555
- function formatForgeLabel(forge: string): string {
556
- return getForgeDefinitionOrNeutral(forge).displayName;
557
- }
558
-
559
- function formatChangeRequestAbbrev(forge: string): string {
560
- return getForgeDefinitionOrNeutral(forge).changeRequestAbbrev;
561
- }
562
-
563
- function formatChangeRequestNumber(forge: string, number: number): string {
564
- return `${getForgeDefinitionOrNeutral(forge).changeRequestNumberPrefix}${number}`;
565
- }
566
-
567
- function formatIssueNumber(forge: string, number: number): string {
568
- return `${getForgeDefinitionOrNeutral(forge).issueNumberPrefix}${number}`;
569
- }
570
-
571
- function padLineNumber(lineNumber: number | null): string {
572
- return (lineNumber?.toString() ?? "-").padStart(2);
573
- }
574
-
575
- function promptPayload(input: SessionPromptInput): OmpPromptPayload {
576
- if (input.prompt.outputSchema !== undefined || input.prompt.clearPendingPermissions) {
577
- throw new OmpPublicError("OMP does not support structured output or permission controls");
578
- }
579
- if (input.prompt.input.type === "command") {
580
- const name = input.prompt.input.name.trim();
581
- if (!isSafeCommandName(name)) throw new OmpPublicError("Invalid OMP command name");
582
- const argumentsText = input.prompt.input.arguments.trim();
583
- const text = `/${name}${argumentsText ? ` ${argumentsText}` : ""}`;
584
- if (utf8Bytes(text) > MAX_PROMPT_TEXT_LENGTH)
585
- throw new OmpPublicError("OMP command is too large");
586
- return { text, images: [], commandName: name };
587
- }
588
- if (input.prompt.input.content.length > MAX_PROMPT_PARTS) {
589
- throw new OmpPublicError("OMP prompt has too many content parts");
590
- }
591
- const parts: string[] = [];
592
- const images: OmpImage[] = [];
593
- let length = 0;
594
- const appendText = (text: string) => {
595
- length += utf8Bytes(text) + (parts.length > 0 ? 2 : 0);
596
- if (length > MAX_PROMPT_TEXT_LENGTH) throw new OmpPublicError("OMP prompt is too large");
597
- parts.push(text);
598
- };
599
- for (const part of input.prompt.input.content) {
600
- if (part.type === "text") {
601
- appendText(part.text);
602
- continue;
603
- }
604
- if (part.type === "image") {
605
- if (!isValidImagePayload(part.data, part.mimeType, 8 * 1024 * 1024)) {
606
- throw new OmpPublicError("OMP prompt image is invalid");
607
- }
608
- images.push({ type: "image", data: part.data, mimeType: part.mimeType });
609
- continue;
610
- }
611
- appendText(renderPromptAttachmentAsText(part));
612
- }
613
- const text = parts.join("\n\n").trim();
614
- if (!text && images.length === 0) throw new OmpPublicError("OMP prompt cannot be empty");
615
- return { text, images };
616
- }
617
-
618
- function inlinePromptFrameBytes(payload: OmpPromptPayload, delivery: "prompt" | "steer"): number {
619
- const images = payload.images.map(({ type, mimeType }) => ({ type, data: "", mimeType }));
620
- const frame = {
621
- type: delivery,
622
- message: payload.text,
623
- ...(images.length > 0 ? { images } : {}),
624
- ...(delivery === "prompt" ? { id: "" } : {}),
625
- };
626
- const requestIdBytes = delivery === "prompt" ? RPC_REQUEST_ID_BYTES : 0;
627
- return (
628
- utf8Bytes(JSON.stringify(frame)) +
629
- requestIdBytes +
630
- payload.images.reduce((total, image) => total + image.data.length, 0) +
631
- 1
632
- );
633
- }
634
-
635
- function slashCommandName(text: string): string | undefined {
636
- if (!text.startsWith("/")) return undefined;
637
- const body = text.slice(1);
638
- if (!body) return undefined;
639
- const firstWhitespace = body.search(/\s/u);
640
- const name = firstWhitespace === -1 ? body : body.slice(0, firstWhitespace);
641
- return name || undefined;
642
- }
643
-
644
412
  function nativeEntryId(message: OmpMessage): string | undefined {
645
413
  return message.entryId;
646
414
  }
@@ -1685,7 +1453,7 @@ export class OmpProviderSession {
1685
1453
  });
1686
1454
  }
1687
1455
  }
1688
- messages ??= await waitForReplay(this.runtime.getMessages(), replay.signal);
1456
+ messages ??= await waitForReplay(this.runtime.getMessages(replay.signal), replay.signal);
1689
1457
  this.quarantineBranchEntries();
1690
1458
  for (const message of messages) {
1691
1459
  replay.signal.throwIfAborted();
@@ -3234,6 +3002,17 @@ export class OmpProviderSession {
3234
3002
  this.handleRuntimeFailure();
3235
3003
  return;
3236
3004
  }
3005
+ if (
3006
+ event.type === "config_warnings_changed" ||
3007
+ event.type === "advisor_cost_changed" ||
3008
+ event.type === "ttsr_triggered"
3009
+ ) {
3010
+ return;
3011
+ }
3012
+ if (event.type === "irc_message") {
3013
+ this.projector.projectPassive(event);
3014
+ return;
3015
+ }
3237
3016
  if (
3238
3017
  event.type === "notice" ||
3239
3018
  event.type === "todo_reminder" ||
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { basename, extname } from "node:path";
2
+ import { basename, extname, isAbsolute } from "node:path";
3
3
  import type { ProviderEvent } from "@getpaseo/plugin/server/provider";
4
4
  import { z } from "zod";
5
5
  import type {
@@ -882,10 +882,32 @@ export class OmpSubsessionProjector {
882
882
  signal.throwIfAborted();
883
883
  if (this.sessionIdByNativeId.has(snapshot.id)) continue;
884
884
  try {
885
- const history = await waitForReplay(
886
- runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
887
- signal,
888
- );
885
+ let history: ReplayHistory;
886
+ if (snapshot.sessionFile && isAbsolute(snapshot.sessionFile) && this.rootSessionFile) {
887
+ try {
888
+ history = await waitForReplay(
889
+ runtime.readPersistedSubagentTranscript({
890
+ parentSessionFile: this.rootSessionFile,
891
+ childTranscriptId: snapshot.id,
892
+ sessionFile: snapshot.sessionFile,
893
+ cwd: this.cwd,
894
+ signal,
895
+ }),
896
+ signal,
897
+ );
898
+ } catch (error) {
899
+ if (signal.aborted) throw error;
900
+ history = await waitForReplay(
901
+ runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
902
+ signal,
903
+ );
904
+ }
905
+ } else {
906
+ history = await waitForReplay(
907
+ runtimeSession.getSubagentMessages({ subagentId: snapshot.id }),
908
+ signal,
909
+ );
910
+ }
889
911
  try {
890
912
  this.accountReplay(history.messages, budget, signal);
891
913
  collected.push({
@@ -899,7 +921,7 @@ export class OmpSubsessionProjector {
899
921
  }
900
922
  } catch (error) {
901
923
  if (signal.aborted) throw error;
902
- collected.push({ snapshot });
924
+ collected.push({ snapshot, sessionFile: snapshot.sessionFile });
903
925
  }
904
926
  }
905
927
  collected.sort((left, right) => {
@@ -6,7 +6,12 @@ import type {
6
6
  } from "@getpaseo/plugin/server/provider";
7
7
  import { OMP_MCP_AUTH_TIMELINE_KIND } from "../../shared/mcp";
8
8
  import { isOmpImageMimeType, isValidImagePayload, type OmpImageMimeType } from "./image";
9
- import { OMP_MAX_CONTENT_PARTS, type OmpMessage, type OmpRpcEvent } from "./omp-rpc";
9
+ import {
10
+ OMP_MAX_CONTENT_PARTS,
11
+ OMP_MESSAGE_REPLAY_POLICIES,
12
+ type OmpMessage,
13
+ type OmpRpcEvent,
14
+ } from "./omp-rpc";
10
15
  import {
11
16
  boundedJsonBytes,
12
17
  type JsonValue,
@@ -134,6 +139,10 @@ function imageBlock(data: string, mimeType: string): StreamBlockSnapshot | undef
134
139
  }
135
140
 
136
141
  type OmpAssistantMessage = Extract<OmpMessage, { role: "assistant" }>;
142
+ type OmpCustomDisplayMessage = Extract<
143
+ OmpMessage,
144
+ { role: "bashExecution" | "pythonExecution" | "custom" | "hookMessage" }
145
+ >;
137
146
 
138
147
  function blockText(
139
148
  message: OmpAssistantMessage,
@@ -417,16 +426,35 @@ export class OmpTimelineProjector {
417
426
  return;
418
427
  }
419
428
  }
429
+ if (
430
+ (event.type === "message_start" ||
431
+ event.type === "message_update" ||
432
+ event.type === "message_end") &&
433
+ OMP_MESSAGE_REPLAY_POLICIES[event.message.role] === "ignore"
434
+ ) {
435
+ return;
436
+ }
420
437
  if (
421
438
  event.type === "todo_reminder" ||
422
439
  event.type === "todo_auto_clear" ||
440
+ event.type === "goal_updated" ||
441
+ event.type === "auto_retry_start" ||
442
+ event.type === "auto_retry_end" ||
443
+ event.type === "retry_fallback_applied" ||
444
+ event.type === "retry_fallback_succeeded" ||
423
445
  event.type === "notice" ||
424
446
  event.type === "extension_ui_request" ||
425
447
  event.type === "auto_compaction_start" ||
426
448
  event.type === "auto_compaction_end" ||
427
449
  event.type === "compaction_start" ||
428
450
  event.type === "compaction_end" ||
429
- event.type === "advisor_yielded"
451
+ event.type === "advisor_yielded" ||
452
+ event.type === "config_warnings_changed" ||
453
+ event.type === "advisor_cost_changed" ||
454
+ event.type === "ttsr_triggered" ||
455
+ event.type === "irc_message" ||
456
+ event.type === "model_changed" ||
457
+ event.type === "thinking_level_changed"
430
458
  ) {
431
459
  this.projectPassive(event);
432
460
  return;
@@ -449,7 +477,12 @@ export class OmpTimelineProjector {
449
477
  this.scheduleFlush();
450
478
  return;
451
479
  case "message_end":
452
- if (event.message.role === "custom" || event.message.role === "bashExecution") {
480
+ if (
481
+ event.message.role === "custom" ||
482
+ event.message.role === "hookMessage" ||
483
+ event.message.role === "bashExecution" ||
484
+ event.message.role === "pythonExecution"
485
+ ) {
453
486
  this.publishCustomMessage(event.message);
454
487
  return;
455
488
  }
@@ -499,12 +532,13 @@ export class OmpTimelineProjector {
499
532
  if (!snapshot.silent) this.publishTool(snapshot, "running");
500
533
  return;
501
534
  }
502
- case "tool_execution_update": {
535
+ case "tool_execution_update":
536
+ case "tool_stream_update": {
503
537
  const previous = this.tools.get(event.toolCallId);
504
538
  if (!previous) return;
505
539
  if (previous.turnId !== turnId || previous.generation !== this.runtimeGeneration) return;
506
540
  const output = this.dataFilter.json(
507
- event.partialResult,
541
+ event.type === "tool_execution_update" ? event.partialResult : event.update,
508
542
  MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
509
543
  MAX_PUBLIC_TOOL_PAYLOAD_BYTES,
510
544
  );
@@ -600,6 +634,10 @@ export class OmpTimelineProjector {
600
634
  this.publishRetryFallback(event);
601
635
  return;
602
636
  }
637
+ if (event.type === "irc_message") {
638
+ this.publishCustomMessage(event.message as OmpCustomDisplayMessage);
639
+ return;
640
+ }
603
641
  if (event.type === "notice") {
604
642
  this.noticeSequence += 1;
605
643
  this.publish({
@@ -839,8 +877,9 @@ export class OmpTimelineProjector {
839
877
 
840
878
  projectReplayMessage(message: OmpMessage): void {
841
879
  if (this.closed) return;
842
- const nativeIdentity = assistantIdentity(message);
843
880
  this.replaySequence += 1;
881
+ if (OMP_MESSAGE_REPLAY_POLICIES[message.role] === "ignore") return;
882
+
844
883
  if (message.role === "user") {
845
884
  this.replayToolCalls.clear();
846
885
  if (this.replayTurnId) this.finishTurn(this.replayTurnId);
@@ -861,7 +900,9 @@ export class OmpTimelineProjector {
861
900
  }
862
901
  return;
863
902
  }
903
+
864
904
  if (message.role === "assistant") {
905
+ const nativeIdentity = assistantIdentity(message);
865
906
  this.replayTurnId ??= `omp:replay-turn:${this.replaySequence}`;
866
907
  this.projectingReplay = true;
867
908
  try {
@@ -880,6 +921,7 @@ export class OmpTimelineProjector {
880
921
  if (nativeIdentity) this.rememberReplayOccurrence(nativeIdentity, message);
881
922
  return;
882
923
  }
924
+
883
925
  this.replayTurnId ??= `omp:replay-turn:${this.replaySequence}`;
884
926
  if (message.role === "toolResult") {
885
927
  const replayCall = this.replayToolCalls.get(message.toolCallId);
@@ -910,10 +952,18 @@ export class OmpTimelineProjector {
910
952
  );
911
953
  return;
912
954
  }
913
- if (message.role === "bashExecution") {
955
+
956
+ if (
957
+ message.role === "bashExecution" ||
958
+ message.role === "pythonExecution" ||
959
+ message.role === "custom" ||
960
+ message.role === "hookMessage"
961
+ ) {
914
962
  this.publishCustomMessage(message);
915
- this.finishTurn(this.replayTurnId);
916
- this.replayTurnId = null;
963
+ if (message.role === "bashExecution" || message.role === "pythonExecution") {
964
+ this.finishTurn(this.replayTurnId);
965
+ this.replayTurnId = null;
966
+ }
917
967
  }
918
968
  }
919
969
 
@@ -1350,9 +1400,17 @@ export class OmpTimelineProjector {
1350
1400
  return undefined;
1351
1401
  }
1352
1402
 
1353
- private publishCustomMessage(message: OmpMessage): void {
1354
- if (message.display === false) return;
1355
- const rawType = message.customType ?? message.role;
1403
+ private publishCustomMessage(message: OmpCustomDisplayMessage): void {
1404
+ if (
1405
+ (message.role === "custom" || message.role === "hookMessage") &&
1406
+ message.display === false
1407
+ ) {
1408
+ return;
1409
+ }
1410
+ const rawType =
1411
+ message.role === "custom" || message.role === "hookMessage"
1412
+ ? (message.customType ?? message.role)
1413
+ : message.role;
1356
1414
  const publicType = this.dataFilter.text(rawType, 256);
1357
1415
  const lowerType = rawType.toLowerCase();
1358
1416
  const details = jsonRecord(this.dataFilter.json(message.details ?? null));
@@ -1361,7 +1419,9 @@ export class OmpTimelineProjector {
1361
1419
  const id = nativeIdentity
1362
1420
  ? `omp:custom:${createHash("sha256").update(nativeIdentity).digest("base64url").slice(0, 12)}`
1363
1421
  : `omp:custom:${this.customSequence}`;
1364
- const contentParts = Array.isArray(message.content) ? message.content : [];
1422
+ const messageContent =
1423
+ message.role === "custom" || message.role === "hookMessage" ? message.content : undefined;
1424
+ const contentParts = Array.isArray(messageContent) ? messageContent : [];
1365
1425
  const imageResult = nativeImageResult(
1366
1426
  {
1367
1427
  content:
@@ -1373,16 +1433,26 @@ export class OmpTimelineProjector {
1373
1433
  this.dataFilter,
1374
1434
  );
1375
1435
  const content =
1376
- typeof message.content === "string"
1377
- ? message.content
1436
+ typeof messageContent === "string"
1437
+ ? messageContent
1378
1438
  : contentParts
1379
1439
  .flatMap((part) => (part.type === "text" && part.text ? [part.text] : []))
1380
1440
  .join("\n\n");
1381
- if (message.role === "bashExecution" || /bash|shell|python/u.test(lowerType)) {
1441
+ const execution = message.role === "bashExecution" || message.role === "pythonExecution";
1442
+ if (execution || /bash|shell|python/u.test(lowerType)) {
1443
+ const nativeCommand =
1444
+ message.role === "bashExecution"
1445
+ ? message.command
1446
+ : message.role === "pythonExecution"
1447
+ ? message.code
1448
+ : undefined;
1382
1449
  const command = this.dataFilter.text(
1383
- message.command ?? firstString(details, "command", "input") ?? publicType,
1450
+ nativeCommand ?? firstString(details, "command", "input") ?? publicType,
1384
1451
  );
1385
- const output = message.output ?? content;
1452
+ const output = execution ? (message.output ?? content) : content;
1453
+ const exitCode = execution ? message.exitCode : undefined;
1454
+ const cancelled = execution ? message.cancelled : undefined;
1455
+ const truncated = execution ? message.truncated : undefined;
1386
1456
  this.publish({
1387
1457
  type: "tool_call",
1388
1458
  id,
@@ -1393,21 +1463,21 @@ export class OmpTimelineProjector {
1393
1463
  command,
1394
1464
  ...(firstString(details, "cwd") ? { cwd: firstString(details, "cwd") } : {}),
1395
1465
  ...(output ? { output: this.dataFilter.text(output, MAX_STREAM_TEXT_LENGTH) } : {}),
1396
- ...(typeof message.exitCode === "number" || message.exitCode === null
1397
- ? { exitCode: message.exitCode }
1466
+ ...(typeof exitCode === "number" || exitCode === null
1467
+ ? { exitCode }
1398
1468
  : typeof details?.exitCode === "number"
1399
1469
  ? { exitCode: details.exitCode }
1400
1470
  : {}),
1401
1471
  },
1402
- ...(message.cancelled !== undefined || message.truncated !== undefined
1472
+ ...(cancelled !== undefined || truncated !== undefined
1403
1473
  ? {
1404
1474
  metadata: {
1405
- ...(message.cancelled !== undefined ? { cancelled: message.cancelled } : {}),
1406
- ...(message.truncated !== undefined ? { truncated: message.truncated } : {}),
1475
+ ...(cancelled !== undefined ? { cancelled } : {}),
1476
+ ...(truncated !== undefined ? { truncated } : {}),
1407
1477
  },
1408
1478
  }
1409
1479
  : {}),
1410
- status: message.cancelled ? "canceled" : "completed",
1480
+ status: cancelled ? "canceled" : "completed",
1411
1481
  error: null,
1412
1482
  });
1413
1483
  if (imageResult) this.publishImages(id, publicType, imageResult.image);