Skip to content

[Feature]: Add unitType and phase fields to BeforeAgentStartEvent payload in pi-coding-agent #1997

Description

@efrembaraldo

Summary

Extend the BeforeAgentStartEvent payload in @gsd/pi-coding-agent to include two new optional fields: unitType: string (matches the value emitted by emitUnitStart in hook-emitter.ts) and phase: string (matches the Phase enum defined in src/resources/extensions/gsd/types.ts). Both fields are emitted by the auto-dispatch loop in unit-phase.ts (call site setCurrentPhase(unitType, ...) at line 472) but are not currently exposed to extension listeners.

Problem to solve

The gsd-pi-discussion-arena extension (HEAD a371143, v0.8.0) cannot implement true "forced Tier F" integration with gsd-pi runtime because BeforeAgentStartEvent does not carry the dispatch context. Currently the extension must fall back to:

  1. Polling getCurrentPhase() from gsd-phase-state.ts (works for GSDExtensionAPI wrappers only).
  2. Inferring phase from heuristic correlation (e.g. selectedModelId correlation accuracy ~80%, per spec v2 §4 W1.1 workaround).
  3. Maintaining an internal WeakMap<ExtensionAPI, {value: string}> (currentUnitTypeByApi) populated by intercepting unit_start events that gsd-pi never actually emits (verified via grep -rn 'emitUnitStart(' /home/opengsd/repos/open-gsd_gsd-pi/src → 0 call sites, only the definition in src/resources/extensions/gsd/hook-emitter.ts:169).

Without these fields in the event payload, the extension cannot synchronously know which unitType is being dispatched at the moment before_agent_start fires. The user's intent (declared in BeforeAgentStartEventResult via systemPrompt injection) cannot be reliably produced.

This blocks DoD §7.2 ("DoD-capability") of the spec v2 specification which requires W1.1 to be merged in open-gsd/gsd-pi.

Proposed solution

Add two optional fields to the BeforeAgentStartEvent interface in packages/pi-coding-agent/src/core/extensions/extension-upstream-types.ts (current line 691):

export interface BeforeAgentStartEvent {
    type: "before_agent_start";
    prompt: string;
    images?: ImageContent[];
    systemPrompt: string;
    systemPromptOptions: BuildSystemPromptOptions;
    // NEW (additive, backward-compatible):
    /** Unit type being dispatched, if known. Matches `unitType` from `UnitStartEvent`. */
    unitType?: string;
    /** Current GSD phase, if active. One of the 18 values in `Phase` enum. */
    phase?: string;
}

The emitter (in packages/pi-agent-core/src/harness/agent-harness.ts line 545, which currently emits BeforeAgentStartEvent with prompt, images, systemPrompt, resources) must be updated to read the current phase from gsd-phase-state.getCurrentPhase() and the current unit-type from unit-phase.ts state, and pass them to the event payload.

This change is:

  • Additive (new optional fields, no breaking change).
  • Backward-compatible (existing listeners ignore unknown fields).
  • Forward-compatible with the existing gsd-extension-api.ts wrapper which already exposes getPhase() separately.

Alternatives considered

  1. Reuse existing systemPromptOptions.BuildSystemPromptOptions payload — rejected because systemPromptOptions is intended for resource discovery metadata, not dispatch context. Coupling unit-type to system-prompt-resource options would create semantic confusion.

  2. Add a new event unit_dispatching that fires BEFORE before_agent_start — rejected because it doubles the event surface area and forces every extension to listen to two events. The data belongs in the existing payload.

  3. Continue using getCurrentPhase() polling via GSDExtensionAPI — rejected because this requires the wrapper to be installed (not all extensions have it) and introduces race conditions between getCurrentPhase() read and before_agent_start dispatch (the phase can change between read and dispatch in concurrent sessions).

  4. Wait for the existing emitUnitStart to actually emit unit_start events — rejected because this requires deeper architectural changes to unit-phase.ts and reviewer feedback (#3338) suggests the maintainer prefers gsd-phase-state as the canonical state, not event emission.

Use cases

  1. gsd-pi-discussion-arena forced Tier F integration (primary use case): the extension listens to before_agent_start, reads event.unitType, looks up the corresponding discussion-arena hook module, and injects a systemPrompt instruction to use discussion_arena tool before the agent starts reasoning. This works on ALL gsd-pi runtime versions, not only those with GSDExtensionAPI wrapper.

  2. gsd-pi UI status widget (secondary): the GsdProgressState widget (defined in extension-upstream-types.ts:61) could display the active phase and unitType more accurately without cross-extension polling.

  3. Cross-extension coordination (tertiary): other community extensions (e.g. observability, cost-tracking) can correlate their work with the dispatch phase without depending on gsd-phase-state module-level singletons.

Impact

  • Breaking change: NO (additive fields, optional).
  • API surface change: 2 new optional fields on BeforeAgentStartEvent.
  • Performance impact: negligible (2 extra string reads per event emission).
  • Migration required for existing extensions: NO (existing listeners ignore unknown fields).
  • DoD closed: §7.2 (DoD-capability) becomes fully closed.
  • Severity: medium (extension-side workaround exists via GSDExtensionAPI.getPhase() but requires the wrapper to be installed and propagated; raw pi-coding-agent consumers cannot benefit).

Evidence or prior art

The same payload fields are already used internally by gsd-pi in unit-phase.ts:472:

setCurrentPhase(unitType, {
    basePath: s.basePath,
    traceId: ic.flowId,
    turnId: `iter-${ic.iteration}`,
    causedBy: "unit-start",
});

And in gsd-extension-api.ts:54:

export interface GSDExtensionAPI extends ExtensionAPI {
    getPhase(): Phase | null;
    getActiveUnit(): GSDActiveUnit | null;
}

The GSDExtensionAPI.getActiveUnit() method already exposes a structured equivalent ({milestoneId, milestoneTitle, sliceId, sliceTitle, taskId, taskTitle}) but only via the wrapper. The proposed change makes the same data available to raw event listeners without requiring the wrapper.

Additional information

The GSDExtensionAPI.getActiveUnit() method already exposes a structured equivalent ({milestoneId, milestoneTitle, sliceId, sliceTitle, taskId, taskTitle}) but only via the wrapper. The proposed change makes the same data available to raw event listeners without requiring the wrapper.

Additional information

  • Affected file in gsd-pi: packages/pi-coding-agent/src/core/extensions/extension-upstream-types.ts:691 (interface declaration) and packages/pi-agent-core/src/harness/agent-harness.ts:545 (event emission).
  • Affected extension file (consumer side, already prepared): src/gsd-api-adapter.ts:73 getRuntimePhase() in gsd-pi-discussion-arena repo. The extension is ready to consume event.unitType if exposed.
  • Related discussion in docs/architecture/runtime-fallbacks.md (W1.1 section) inside the extension repo.
  • Related spec v2 §7.2 DoD: this PR is the missing piece to close it.
  • Estimated upstream review time: 1-2 working days (additive change, well-documented, low risk).
  • Backward compatibility test plan: existing extensions that listen to before_agent_start must continue to work unchanged. Verify by running tests/integration/extensions-runner.test.ts in open-gsd/gsd-pi

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions