pi-herdr-agents 0.1.2 → 0.1.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.
@@ -317,13 +317,18 @@ The full bounded `RunEnvelope` exists exactly once, inside the single terminal e
317
317
  ```text
318
318
  approved
319
319
  started
320
+ reader_checkout_ready | reader_checkout_retained | reader_checkout_disposed
320
321
  agent_started
322
+ agent_completed
321
323
  agent_result
324
+ pane_close_failed
322
325
  workflow_log
323
326
  completed | failed | cancelled | interrupted
324
327
  delivery
325
328
  ```
326
329
 
330
+ `agent_completed` records the observed child exit, session availability, final assistant-text length, and (when present) final stop reason before the corresponding `agent_result` records the workflow-facing success or failure envelope.
331
+
327
332
  `delivery` follows the terminal event and contains only its terminal-event ID/state, target parent session, attempt time, and `sent | failed` delivery status. It never duplicates the task result. Evidence readers obtain the result from the referenced terminal event. Do not add mutable snapshots, locks, leases, backups, call caching, or replay.
328
333
 
329
334
  One process-global owner records the run ID, canonical project identity, lifecycle gate, Worker, and private child handles. `/new`, `/resume`, and `/fork` cannot start a second workflow while this owner exists; cancellation names the run ID and must match the current canonical project identity. On same-process `/reload`, the owner and Worker continue, and completion selects the latest extension API once. On full startup, scan only direct child directories under the current project's `.pi/plans/` and inspect only each journal's last valid event. A `delivery` event carries its referenced terminal state, a terminal event is already settled, and a non-terminal running event with no live process-global owner receives one `interrupted` terminal event. Do not recurse, restart, clean, or expose history UI.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-herdr-agents",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Asynchronous Pi subagents and approved review workflows in Herdr, with optional isolated Git worktrees",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -85,6 +85,7 @@ import {
85
85
 
86
86
  import {
87
87
  findLastAssistantMessage,
88
+ inspectFinalAssistantMessage,
88
89
  findObservedSessionRuntime,
89
90
  getNewEntries,
90
91
  seedSubagentSessionFile,
@@ -3033,18 +3034,43 @@ export default function subagentsExtension(pi: ExtensionAPI) {
3033
3034
  if (childController.signal.aborted || watched.error === "cancelled") {
3034
3035
  return workflowFailure("cancelled", "Workflow cancelled.");
3035
3036
  }
3036
- const childEntries = existsSync(sessionFile)
3037
+ const sessionExists = existsSync(sessionFile);
3038
+ const childEntries = sessionExists
3037
3039
  ? getNewEntries(sessionFile, 0)
3038
3040
  : [];
3039
- const summary = findLastAssistantMessage(childEntries);
3040
- if (watched.exitCode !== 0 || watched.errorMessage || !summary) {
3041
+ const finalAssistant = inspectFinalAssistantMessage(childEntries);
3042
+ journal.append("agent_completed", {
3043
+ id,
3044
+ role: roleName,
3045
+ sessionFile,
3046
+ sessionExists,
3047
+ exitCode: watched.exitCode,
3048
+ ...(watched.errorMessage
3049
+ ? { errorMessage: watched.errorMessage }
3050
+ : {}),
3051
+ finalAssistantContentLength: finalAssistant.contentLength,
3052
+ ...(finalAssistant.stopReason
3053
+ ? { finalAssistantStopReason: finalAssistant.stopReason }
3054
+ : {}),
3055
+ });
3056
+ if (watched.exitCode !== 0 || watched.errorMessage) {
3041
3057
  return workflowFailure(
3042
3058
  "child_error",
3043
3059
  watched.errorMessage ??
3044
- summary ??
3045
3060
  `Workflow child exited with code ${watched.exitCode}`,
3046
3061
  );
3047
3062
  }
3063
+ if (!finalAssistant.text) {
3064
+ return workflowFailure(
3065
+ "empty_completion",
3066
+ `Workflow child completed without assistant text${
3067
+ finalAssistant.stopReason
3068
+ ? ` (stopReason: ${finalAssistant.stopReason})`
3069
+ : ""
3070
+ }.`,
3071
+ );
3072
+ }
3073
+ const summary = finalAssistant.text;
3048
3074
  const observed = findObservedSessionRuntime(childEntries);
3049
3075
  const observedModel =
3050
3076
  observed.provider && observed.modelId
@@ -136,6 +136,39 @@ export function findObservedSessionRuntime(entries: SessionEntry[]): ObservedSes
136
136
  return observed;
137
137
  }
138
138
 
139
+ export interface FinalAssistantMessage {
140
+ text: string | null;
141
+ contentLength: number;
142
+ stopReason?: string;
143
+ }
144
+
145
+ /** Inspect only the final assistant message for workflow completion evidence. */
146
+ export function inspectFinalAssistantMessage(
147
+ entries: SessionEntry[],
148
+ ): FinalAssistantMessage {
149
+ for (let i = entries.length - 1; i >= 0; i--) {
150
+ const entry = entries[i];
151
+ if (entry.type !== "message") continue;
152
+ const msg = entry as MessageEntry;
153
+ if (msg.message.role !== "assistant") continue;
154
+
155
+ const texts = msg.message.content
156
+ .filter(
157
+ (block) =>
158
+ block.type === "text" && typeof block.text === "string",
159
+ )
160
+ .map((block) => block.text as string);
161
+ const text = texts.join("\n");
162
+ const stopReason = (msg.message as { stopReason?: unknown }).stopReason;
163
+ return {
164
+ text: text.trim() ? text : null,
165
+ contentLength: text.length,
166
+ ...(typeof stopReason === "string" ? { stopReason } : {}),
167
+ };
168
+ }
169
+ return { text: null, contentLength: 0 };
170
+ }
171
+
139
172
  export function findLastAssistantMessage(entries: SessionEntry[]): string | null {
140
173
  for (let i = entries.length - 1; i >= 0; i--) {
141
174
  const entry = entries[i];