Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions docs/configuration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,82 @@ Public service runtimes still use role-name identities such as `planner`, so the

Many `backlog_id` and `request_id` values may exist while one configured sprint session scope remains active.

### `prompt_context`

Controls how much persisted request event history is copied into model prompts:

```yaml
prompt_context:
enabled: true
recent_events: 8
max_events: 16
```

Defaults:

- `enabled: true`
- `recent_events: 8`
- `max_events: 16`

Both event limits must be positive integers, and `max_events` must be greater than or equal to `recent_events`. Role services load these values at startup, so restart them after changing this section. There is no CLI mutation command for this policy.

Compaction changes only the prompt projection. The canonical request JSON under `.teams_runtime/requests/` retains every event, and the current request metadata, result, artifacts, and selected event payloads are not truncated or summarized.

#### Compaction And Backfill

**Compaction** is executed only when `enabled` is `true` and the request contains more than `max_events` events. The runtime:

1. Includes the last `recent_events` entries by list position, regardless of event shape.
2. Treats roles represented in that recent tail as already covered.
3. Scans older events from newest to oldest.
4. Backfills the newest evidence for each role not already represented, stopping at `max_events`.
5. Restores the selected events to their original chronological order.

**Backfill** means using remaining capacity to retain older role evidence that would otherwise disappear behind the recent-event boundary. An older event qualifies as role evidence when either `type` or legacy `event_type` is `role_report`, or when its `payload` contains non-empty `role` and `status` fields. The role identity comes from `payload.role`, falling back to the event `actor`.

Backfill is not a summary, database repair, or write to history. It copies complete existing event objects into the prompt. Repeated reports for one role do not consume multiple backfill slots; the newest qualifying older report wins. If `max_events` equals `recent_events`, no backfill capacity exists and the projection is recent-only.

#### Worked Example

With `recent_events: 4` and `max_events: 7`, suppose the persisted request has this abbreviated event list:

```json
[
{"timestamp": "T01", "type": "created", "actor": "orchestrator"},
{"timestamp": "T02", "type": "role_report", "payload": {"role": "research", "status": "completed"}},
{"timestamp": "T03", "type": "delegated", "actor": "orchestrator"},
{"timestamp": "T04", "type": "role_report", "payload": {"role": "planner", "status": "completed", "summary": "initial plan"}},
{"timestamp": "T05", "type": "role_report", "payload": {"role": "designer", "status": "completed"}},
{"timestamp": "T06", "type": "role_report", "payload": {"role": "planner", "status": "completed", "summary": "final plan"}},
{"timestamp": "T07", "type": "retried", "actor": "orchestrator"},
{"timestamp": "T08", "type": "role_report", "payload": {"role": "developer", "status": "completed"}},
{"timestamp": "T09", "type": "role_report", "payload": {"role": "architect", "status": "completed"}},
{"timestamp": "T10", "type": "delegated", "actor": "orchestrator"},
{"timestamp": "T11", "type": "role_report", "payload": {"role": "qa", "status": "blocked"}},
{"timestamp": "T12", "type": "resumed", "actor": "orchestrator"}
]
```

The recent tail is `T09` through `T12`, representing `architect` and `qa`. Three slots remain. Scanning backward selects `T08` for `developer`, `T06` for `planner`, and `T05` for `designer`. `T04` is skipped because the newer planner report already represents that role. Capacity is then full, so older `research` evidence at `T02` is not selected.

The prompt receives:

```json
[
{"timestamp": "T05", "type": "role_report", "payload": {"role": "designer", "status": "completed"}},
{"timestamp": "T06", "type": "role_report", "payload": {"role": "planner", "status": "completed", "summary": "final plan"}},
{"timestamp": "T08", "type": "role_report", "payload": {"role": "developer", "status": "completed"}},
{"timestamp": "T09", "type": "role_report", "payload": {"role": "architect", "status": "completed"}},
{"timestamp": "T10", "type": "delegated", "actor": "orchestrator"},
{"timestamp": "T11", "type": "role_report", "payload": {"role": "qa", "status": "blocked"}},
{"timestamp": "T12", "type": "resumed", "actor": "orchestrator"}
]
```

The adjacent prompt notice reports `total_events: 12`, `included_events: 7`, `omitted_events: 5`, the selection policy, and the canonical request path. A role may open that canonical file when its current decision needs omitted evidence.

For immediate rollback, set `enabled: false` and restart role services. This restores full event-history inclusion in prompts without changing persisted request data.

## Changing `sprint.id`

To rotate the configured sprint session scope:
Expand Down
3 changes: 3 additions & 0 deletions docs/implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ Current ownership notes:
- canonical shared contracts: role/session config dataclasses plus typed request/backlog/sprint/workflow/result shapes
- `teams_runtime/shared/config.py`
- canonical runtime and Discord config loading, validation, placeholder-ID guardrails, and runtime config mutation helpers
- `teams_runtime/shared/prompt_context.py`
- canonical pure request-event projection, role-evidence backfill selection, and compacted-history prompt notice rendering
- `teams_runtime/shared/paths.py`
- canonical `RuntimePaths` workspace, runtime state, log, role, shared workspace, sprint artifact, and archive path contract
- `teams_runtime/shared/persistence.py`
Expand Down Expand Up @@ -275,6 +277,7 @@ Current ownership notes:
- Shared contracts now belong in `shared/models.py`.
- Canonical report/progress formatting helpers, backlog item construction, backlog markdown rendering, and current-sprint markdown rendering belong in `shared/formatting.py`; `core/reports.py` remains compatibility-only and `core/sprints.py` re-exports moved formatting helpers.
- Canonical runtime and Discord config loading/updating belongs in `shared/config.py`; `core/config.py` remains compatibility-only.
- Canonical model-facing request event-history projection and backfill selection belongs in `shared/prompt_context.py`; request persistence remains unchanged.
- Canonical workspace/runtime path helpers belong in `shared/paths.py`; `core/paths.py` remains compatibility-only.
- Canonical shared JSON/JSONL persistence, ID/fingerprint generation, and KST timestamp helpers belong in `shared/persistence.py`; `core/persistence.py` remains compatibility-only.
- Canonical request/backlog/sprint/goal file IO, event helpers, planner-review request predicates/lookups/record assembly, internal sprint request predicates/iteration, blocked-backlog review candidate normalization/rendering, non-actionable backlog classification/drop/repair, backlog status/blocker/todo-state helpers, sprint selected-backlog view derivation, backlog kind/acceptance normalization, goal lifecycle reports, and backlog status-report context helpers belong in `workflows/state/request_store.py`, `workflows/state/backlog_store.py`, `workflows/state/sprint_store.py`, and `workflows/state/goal_store.py`; `core/*_store.py` remains compatibility-only.
Expand Down
8 changes: 5 additions & 3 deletions docs/operations_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,17 +279,19 @@ params: {"_teams_kind":"delegate"}

### 2. Persisted request record

The target role also receives the full persisted request record through its prompt context.
The target role receives a prompt projection of the persisted request record. Request metadata, current status, reply route, artifacts, and the most recent role result remain available. Event history is included in full while it is within the configured limit; longer histories use the bounded `prompt_context` compaction policy.

That record contains:

- request metadata
- current status
- reply route
- event history
- full or compacted event history
- most recent role result

So later roles can see earlier role output even though the relay message itself stays small.
Compaction always keeps the configured recent tail, then uses remaining capacity to backfill the newest older evidence for roles missing from that tail. Here, backfill means selecting complete historical event objects; it does not summarize or modify them. An adjacent notice gives the total, included, and omitted counts plus the canonical `.teams_runtime/requests/<request_id>.json` path. The persisted file remains complete and can be inspected when omitted evidence is required.

This keeps normal later-role prompts bounded while preserving a path to the complete audit history. See [`configuration_guide.md`](./configuration_guide.md#prompt_context) for the exact algorithm and worked input/output example.

## Request Examples

Expand Down
19 changes: 19 additions & 0 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,27 @@ Supported sections:
- `mentions`
- `allowed_guild_ids`
- `role_defaults`
- `prompt_context`
- `enabled`
- `recent_events`
- `max_events`
- `telemetry`
- `enabled`
- `rate_cards`
- `actions`

### Prompt event-history projection

- persisted request records remain the canonical, complete audit history
- model-facing normal role, role-result repair, research-decision, and version-controller prompts share one event projection policy
- when enabled and event count exceeds `max_events`, the projection always includes the final `recent_events` entries
- remaining capacity is backfilled by scanning older events newest-first and selecting the newest evidence for each role not represented in the recent tail
- qualifying evidence is a `role_report` in `type` or `event_type`, or an event whose payload contains non-empty `role` and `status`; identity uses `payload.role` then `actor`
- selected events retain their complete payload and original chronological order
- prompt compaction never mutates the persisted request record
- compacted prompts include counts, policy name, and canonical request path so omitted history remains discoverable
- disabling `prompt_context` restores full event-history prompt inclusion after role services restart

## Backlog And Sprint Model

Normal change and enhancement requests are backlog-first.
Expand Down
14 changes: 14 additions & 0 deletions docs/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,20 @@ Use one representative sprint or at least 24 hours of normal traffic before chan

Do not compare only total role cost. Normalize by request count, todo count, and logical-call count so a frequently used inexpensive role is not confused with an inefficient role.

### Measuring Prompt Compaction

Use comparable requests with long event histories before and after enabling `prompt_context`. Keep the provider, model, reasoning level, request shape, and workflow path stable. Compare:

- `prompt_chars` and native input tokens per logical call
- cached-input tokens and cached-input ratio
- total input tokens per completed request or todo
- contract-repair and retry counts
- failures, QA outcomes, and p95 latency

The expected result is lower prompt size and input-token usage in later workflow stages without a higher repair, retry, failure, or reopen rate. Session reuse can change cached-input behavior independently, so do not attribute every cached-token change to compaction. Telemetry stores prompt size and usage totals, not prompt content or the selected event list; use the content-free `prompt_context_compacted` runtime log for total/included/omitted counts.

For a controlled rollback comparison, set `prompt_context.enabled: false`, restart the same role services, and repeat the same request shape. The canonical persisted event history is identical in either mode.

## Operational Validation

After restarting role services and completing a model-backed request:
Expand Down
2 changes: 2 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
INTERNAL_TEAM_AGENTS,
MessageEnvelope,
ModelRateCard,
PromptContextRuntimeConfig,
ReplyRoute,
RequestEvent,
RequestRecord,
Expand All @@ -40,6 +41,7 @@
"INTERNAL_TEAM_AGENTS",
"MessageEnvelope",
"ModelRateCard",
"PromptContextRuntimeConfig",
"ReplyRoute",
"RequestEvent",
"RequestRecord",
Expand Down
49 changes: 47 additions & 2 deletions runtime/base_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,17 @@
from teams_runtime.runtime.session_manager import RoleSessionManager
from teams_runtime.shared.models import (
MessageEnvelope,
PromptContextRuntimeConfig,
RequestRecord,
RoleResult,
RoleRuntimeConfig,
TelemetryRuntimeConfig,
)
from teams_runtime.shared.prompt_context import (
PromptRequestProjection,
project_request_record_for_prompt,
render_prompt_event_history_notice,
)
from teams_runtime.workflows.roles import render_role_prompt_spec
from teams_runtime.workflows.roles.planner import normalize_planner_proposals

Expand Down Expand Up @@ -266,6 +272,7 @@ def __init__(
agent_root: Path | None = None,
session_identity: str | None = None,
telemetry_config: TelemetryRuntimeConfig | None = None,
prompt_context_config: PromptContextRuntimeConfig | None = None,
):
self.paths = paths
self.role = role
Expand All @@ -283,6 +290,7 @@ def __init__(
self.sprint_id: self.session_manager,
}
self.telemetry_recorder = ModelTelemetryRecorder(paths, self.runtime_identity, telemetry_config)
self.prompt_context_config = prompt_context_config or PromptContextRuntimeConfig()
self.codex_runner = CodexRunner(
runtime_config,
role=role,
Expand All @@ -291,6 +299,31 @@ def __init__(
self.runtime_config = runtime_config
self._run_lock = threading.Lock()

def _project_request_for_prompt(
self,
request_record: RequestRecord,
*,
purpose: str,
) -> PromptRequestProjection:
projection = project_request_record_for_prompt(
request_record,
self.prompt_context_config,
)
if projection.compacted:
LOGGER.info(
"[%s] prompt_context_compacted request_id=%s purpose=%s total_events=%s included_events=%s "
"omitted_events=%s recent_events=%s max_events=%s",
self.role,
str(request_record.get("request_id") or "unknown"),
purpose,
projection.total_events,
projection.included_events,
projection.omitted_events,
projection.recent_events,
projection.max_events,
)
return projection

def _resolve_request_sprint_id(
self,
envelope: MessageEnvelope,
Expand Down Expand Up @@ -643,6 +676,11 @@ def _build_role_result_repair_prompt(
*,
current_sprint_id: str,
) -> str:
request_projection = self._project_request_for_prompt(
request_record,
purpose="contract_repair",
)
event_history_notice = render_prompt_event_history_notice(request_projection)
team_workspace_hint = "./workspace/teams_generated" if self.paths.workspace_root.name == "teams_generated" else "./workspace"
role_specific_rules, extra_fields = render_role_prompt_spec(self.role, team_workspace_hint)
contract_block = render_role_result_contract(
Expand Down Expand Up @@ -674,8 +712,9 @@ def _build_role_result_repair_prompt(
If validation errors mention copied placeholder or scaffold text, do not reuse any wording from the shape block. Write concrete Korean summary and workflow reason text from the actual request state, or return `failed` with a concrete Korean reason.
{role_specific_rules}

{event_history_notice}
Current request:
{json.dumps(request_record, ensure_ascii=False, indent=2)}
{json.dumps(request_projection.request_record, ensure_ascii=False, indent=2)}
"""

def _should_retry_with_bypass(self, payload: dict[str, Any]) -> bool:
Expand Down Expand Up @@ -734,6 +773,11 @@ def _build_prompt(
*,
current_sprint_id: str | None = None,
) -> str:
request_projection = self._project_request_for_prompt(
request_record,
purpose="role_task",
)
event_history_notice = render_prompt_event_history_notice(request_projection)
resolved_sprint_id = str(current_sprint_id or "").strip() or self._resolve_request_sprint_id(
envelope,
request_record,
Expand Down Expand Up @@ -774,8 +818,9 @@ def _build_prompt(
When you claim a file change or validation result, leave enough evidence in `summary`, `insights`, or `proposals` for orchestrator to verify what you actually checked.
{role_specific_rules}

{event_history_notice}
Current request:
{json.dumps(request_record, ensure_ascii=False, indent=2)}
{json.dumps(request_projection.request_record, ensure_ascii=False, indent=2)}

Incoming envelope:
{json.dumps(envelope.to_dict(), ensure_ascii=False, indent=2)}
Expand Down
4 changes: 4 additions & 0 deletions runtime/research_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from teams_runtime.shared.paths import RuntimePaths
from teams_runtime.shared.models import (
MessageEnvelope,
PromptContextRuntimeConfig,
RequestRecord,
ResearchRuntimeConfig,
RoleResult,
Expand Down Expand Up @@ -93,6 +94,7 @@ def __init__(
agent_root: Path | None = None,
session_identity: str | None = None,
telemetry_config: TelemetryRuntimeConfig | None = None,
prompt_context_config: PromptContextRuntimeConfig | None = None,
):
super().__init__(
paths=paths,
Expand All @@ -102,6 +104,7 @@ def __init__(
agent_root=agent_root,
session_identity=session_identity,
telemetry_config=telemetry_config,
prompt_context_config=prompt_context_config,
)
self.research_defaults = research_defaults

Expand Down Expand Up @@ -536,6 +539,7 @@ def _run_research_decision(
envelope,
request_record,
local_sources_checked=local_sources_checked,
prompt_context_config=self.prompt_context_config,
)
output, resolved_session_id = run_with_optional_telemetry(
self.codex_runner,
Expand Down
Loading