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
2 changes: 1 addition & 1 deletion plugins/openflywheel/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openflywheel",
"version": "0.3.0",
"version": "0.4.0",
"description": "Initialize ITSM-bench harness workspaces, query Langfuse trajectories, and record authoritative verifier outcomes.",
"author": {
"name": "OpenFlyWheel"
Expand Down
25 changes: 21 additions & 4 deletions plugins/openflywheel/.mcp.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
{
"mcpServers": {
"openflywheel": {
"command": "sh",
"command": "uvx",
"args": [
"-c",
"exec uv run --project \"${OPENFLYWHEEL_ROOT:-$PWD}\" --extra plugin python \"$PLUGIN_ROOT/scripts/mcp_server.py\""
]
"--from",
"git+https://github.com/divo12/OpenFlyWheel.git@6319f24",
"--with",
"mcp>=1.13,<2",
"openflywheel-mcp"
],
"env_vars": [
"OPENAI_API_KEY",
"OPENAI_BASE_URL",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_BASE_URL",
"LANGFUSE_PUBLIC_KEY",
"LANGFUSE_SECRET_KEY",
"LANGFUSE_BASE_URL",
"HERMES_LANGFUSE_PUBLIC_KEY",
"HERMES_LANGFUSE_SECRET_KEY",
"HERMES_LANGFUSE_BASE_URL"
],
"startup_timeout_sec": 120,
"tool_timeout_sec": 120
}
}
}
8 changes: 7 additions & 1 deletion plugins/openflywheel/program_templates/base.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpenFlywheel Agent Program

This file is generated by `workspace_prepare`. Do not edit it directly.
This file is generated by `prepare_workspace`. Do not edit it directly.

## Mission

Expand Down Expand Up @@ -60,6 +60,12 @@ Keep the change only when the configured gate admits it. Otherwise revert only t
iteration's harness edit, retain the evidence, and try a different hypothesis. Never weaken
the gate to admit a candidate.

Commit each admitted improvement on the prepared `ofw/<experiment-id>` branch before the
next iteration. Keep one hypothesis per commit and include `OFW-Experiment` and `OFW-Run`
trailers. Do not commit failed candidates, generated run artifacts, credentials, or changes
outside the editable surface. Do not push or open a pull request without explicit user
authorization.

### 7. Repeat

Return to step 2 with the newly recorded run. Stop when the configured goal is met, the
Expand Down
213 changes: 4 additions & 209 deletions plugins/openflywheel/scripts/mcp_server.py
Original file line number Diff line number Diff line change
@@ -1,215 +1,10 @@
#!/usr/bin/env python3
"""Typed OpenFlyWheel MCP surface for trace queries and outcome recording."""
"""Compatibility launcher for the installable OpenFlywheel MCP server."""

from __future__ import annotations
from ofw.mcp import main, server

import os
from collections.abc import Callable
from datetime import datetime
from enum import StrEnum
from typing import Annotated, TypeVar

from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations
from pydantic import BaseModel, Field

from ofw.evaluation.langfuse import (
LangfuseOutcomeStore,
OutcomeStoreObservation,
OutcomeStoreStatus,
)
from ofw.evaluation.outcome import OutcomeEvaluation, TaskId, VerifierId
from ofw.observability.langfuse.contracts import LangfuseProject
from ofw.observability.langfuse.domain import TraceId
from ofw.observability.langfuse.trace_query import (
GetSpanContextInput,
GetTraceSchemaInput,
ListTracesInput,
QuerySpansInput,
SessionIdentifier,
SpanFilters,
TraceListObservation,
TraceQueryObservation,
TraceQueryService,
TraceTimeRange,
)
from ofw.observability.langfuse.transport import LangfuseHttpClient
from ofw.runtime import EvidenceReference, VerifierResult, VerifierVerdict

QueryInput = TypeVar("QueryInput")
QueryOutput = TypeVar("QueryOutput", bound=BaseModel)
_QUERY_TIMEOUT_SECONDS = 60.0
TraceIdentifier = Annotated[str, Field(min_length=1, max_length=256)]
SpanIdentifier = Annotated[str, Field(min_length=1, max_length=256)]
CursorIdentifier = Annotated[str, Field(min_length=1, max_length=4096)]
TracePageLimit = Annotated[int, Field(strict=True, ge=1, le=50)]
TaskIdentifier = Annotated[str, Field(min_length=1, max_length=256)]
VerifierIdentifier = Annotated[str, Field(min_length=1, max_length=256)]
OutcomeScore = Annotated[float, Field(strict=True, ge=0.0, le=1.0)]
EvidenceIdentifier = Annotated[str, Field(min_length=1, max_length=1024)]
OutcomeEvidence = Annotated[tuple[EvidenceIdentifier, ...], Field(min_length=1, max_length=10)]

server = FastMCP[None]( # type: ignore[misc] # MCP auth generics are untyped upstream.
name="openflywheel",
instructions=(
"Read bounded Langfuse trace evidence and record only authoritative external-verifier "
"outcomes. Never infer outcomes or mutate traces."
),
log_level="DEBUG",
)
read_only = ToolAnnotations(
readOnlyHint=True,
destructiveHint=False,
idempotentHint=True,
openWorldHint=True,
)
record_write = ToolAnnotations(
readOnlyHint=False,
destructiveHint=False,
idempotentHint=True,
openWorldHint=True,
)


class OutcomeToolErrorCode(StrEnum):
STORE_FAILED = "outcome_store_failed"


class OutcomeToolError(Exception):
"""Sanitized outcome-recording failure returned to the MCP client."""

__slots__ = ("code", "trace_id")

def __init__(self, code: OutcomeToolErrorCode, trace_id: str) -> None:
self.code = code
self.trace_id = trace_id
super().__init__(f"{code.value}: {trace_id}")


def _project() -> LangfuseProject:
return LangfuseProject.from_env(
environment=os.environ.get("LANGFUSE_ENVIRONMENT", "ofw-local"),
allow_private_network=os.environ.get("LANGFUSE_ALLOW_PRIVATE_NETWORK") == "1",
)


def _client() -> LangfuseHttpClient:
return LangfuseHttpClient(_project(), timeout_seconds=_QUERY_TIMEOUT_SECONDS)


def _outcome_store() -> LangfuseOutcomeStore:
return LangfuseOutcomeStore.from_project(_project())


def _execute(
query: QueryInput,
operation: Callable[[TraceQueryService, QueryInput], QueryOutput],
) -> QueryOutput:
client = _client()
try:
return operation(TraceQueryService(client), query)
finally:
client.close()


@server.tool(annotations=read_only, structured_output=True)
def list_traces(
session_id: SessionIdentifier,
time_range: TraceTimeRange,
environment: TraceIdentifier | None = None,
release: TraceIdentifier | None = None,
cursor: CursorIdentifier | None = None,
limit: TracePageLimit = 20,
) -> TraceListObservation:
"""List bounded logical-root traces for one session and time range."""
query = ListTracesInput(
session_id=session_id,
environment=environment,
release=release,
time_range=time_range,
cursor=cursor,
limit=limit,
)
return _execute(query, TraceQueryService.list_traces)


@server.tool(annotations=read_only, structured_output=True)
def get_trace_schema(
trace_id: TraceIdentifier,
cursor: CursorIdentifier | None = None,
) -> TraceQueryObservation:
"""Skim bounded trace structure without loading span input or output."""
query = GetTraceSchemaInput(trace_id=trace_id, cursor=cursor)
return _execute(query, TraceQueryService.get_trace_schema)


@server.tool(annotations=read_only, structured_output=True)
def query_spans(
trace_id: TraceIdentifier,
filters: SpanFilters | None = None,
cursor: CursorIdentifier | None = None,
) -> TraceQueryObservation:
"""Find bounded span IDs using exact structural filters."""
query = QuerySpansInput(
trace_id=trace_id,
filters=filters or SpanFilters(),
cursor=cursor,
)
return _execute(query, TraceQueryService.query_spans)


@server.tool(annotations=read_only, structured_output=True)
def get_span_context(
trace_id: TraceIdentifier,
span_id: SpanIdentifier,
cursor: CursorIdentifier | None = None,
) -> TraceQueryObservation:
"""Read one span, its parent, and up to ten direct children with bounded excerpts."""
query = GetSpanContextInput(trace_id=trace_id, span_id=span_id, cursor=cursor)
return _execute(query, TraceQueryService.get_span_context)


@server.tool(annotations=record_write, structured_output=True)
def record_outcome(
trace_id: TraceIdentifier,
task_id: TaskIdentifier,
verifier_id: VerifierIdentifier,
evaluated_at: datetime,
verdict: VerifierVerdict,
evidence: OutcomeEvidence,
score: OutcomeScore | None = None,
) -> OutcomeStoreObservation:
"""Record one authoritative external-verifier outcome on its exact trace."""
result = VerifierResult(
verdict=verdict,
score=score,
feedback="Recorded by the OpenFlywheel outcome tool.",
evidence=tuple(EvidenceReference(reference) for reference in evidence),
)
outcome = OutcomeEvaluation.from_verifier_result(
trace_id=TraceId(trace_id),
task_id=TaskId(task_id),
verifier_id=VerifierId(verifier_id),
evaluated_at=evaluated_at,
result=result,
)
try:
store = _outcome_store()
try:
submission = store.store(outcome)
finally:
store.close()
except Exception:
raise OutcomeToolError(OutcomeToolErrorCode.STORE_FAILED, trace_id) from None
return OutcomeStoreObservation(
status=OutcomeStoreStatus.SUCCESS,
summary=f"Stored authoritative {verdict.value} outcome on the trace.",
next_actions=("Continue only after retaining this score receipt.",),
artifacts=(trace_id, submission.score_id.value),
trace_id=trace_id,
score_id=submission.score_id.value,
)
__all__ = ["main", "server"]


if __name__ == "__main__":
server.run(transport="stdio")
main()
49 changes: 26 additions & 23 deletions plugins/openflywheel/skills/workspace-init/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: workspace-init
description: Initialize an OpenFlywheel ITSM-bench optimization workspace by inspecting an agent-harness repository, collecting its experiment configuration one field at a time, creating the managed PROGRAM.md placeholder, and handing preparation to workspace_prepare. Use when onboarding a primary harness for ITSM-bench; do not use for other benchmarks, an already-prepared workspace, ordinary trace queries, or outcome recording.
description: Initialize an OpenFlywheel ITSM-bench optimization workspace by inspecting an agent-harness repository, collecting its experiment configuration one field at a time, and handing isolated branch, worktree, PROGRAM.md, and baseline creation to prepare_workspace. Use when onboarding a primary harness for ITSM-bench; do not use for other benchmarks, an already-prepared workspace, ordinary trace queries, or outcome recording.
---

# Workspace Init
Expand All @@ -21,48 +21,51 @@ different harnesses without confirmation.
Ask one focused question at a time. Infer repository facts first and recommend a default
when the evidence supports one. Collect, in order:

1. Harness root and explicitly editable files or directories.
1. Harness root, Git base ref, sibling worktree parent, and explicitly editable files or
directories.
2. Optimization goal, primary metric, target, and stopping condition. Keep quality, cost,
and latency constraints separate rather than hiding them in one average.
3. ITSM-bench root, Harbor task manifest or selection, and expected task count.
4. Authoritative verifier, reward interpretation, and pass threshold.
5. Frozen model, reasoning effort, concurrency, per-task timeout, retry policy, and budget.
6. Langfuse environment, release, and session naming rule.
3. ITSM-bench root, Harbor executable, Harbor configuration, and expected task count.
4. Experiment ID and maximum baseline duration.

Read and report the frozen model from the Harbor configuration. `prepare_workspace` fixes
concurrency to one and retries to zero for deterministic trace mapping, uses ITSM-bench as
the authoritative verifier, fixes the Langfuse environment to `itsm-bench`, derives the
session from the experiment ID, and derives the release from the initialization commit. Do
not ask the user to restate those derived values.

Never request secret values in chat or write them into configuration. Check only whether
the required environment-variable names are present.

Summarize the complete proposed experiment and obtain confirmation before writing files or
starting a potentially costly baseline. Then create `<harness-root>/experiment_config.yaml`
using only the confirmed values.

## 3. Create the managed program placeholder
starting a potentially costly baseline. Pass only those confirmed values to
`prepare_workspace`.

Copy [assets/PROGRAM.md](assets/PROGRAM.md) byte-for-byte to
`<harness-root>/PROGRAM.md`. Do not overwrite an existing different `PROGRAM.md`; stop and
ask whether the existing program should be preserved or replaced.
## 3. Prepare the isolated workspace

Do not compose the final program yourself. Call `workspace_prepare` with the confirmed
experiment configuration. That tool owns validation, baseline execution, result parsing,
and deterministic composition from `program_templates/base.md` and
`program_templates/itsm.md`.
Do not modify or switch the user's original checkout. Call `prepare_workspace` with the
confirmed experiment configuration. That tool owns validation, creation of an isolated
`ofw/<experiment-id>` branch and sibling Git worktree, deterministic composition of
`PROGRAM.md` from `program_templates/base.md` and `program_templates/itsm.md`, creation of
`experiment_config.yaml`, the initialization commit, baseline execution, and result parsing.

`workspace_prepare` is long-running and re-entrant:
`prepare_workspace` is long-running and re-entrant:

- On `running`, retain the preparation ID and poll the same request after the returned
interval. Never start a second baseline.
- On `failed`, report its typed recovery instruction and stop at its declared stop
condition.
- On `ready`, retain the baseline artifacts and confirm that `PROGRAM.md` is no longer the
placeholder.
placeholder. Use the returned worktree path for every later Codex action.

If `workspace_prepare` is unavailable, stop after the confirmed configuration and
placeholder. Report that workspace preparation is not installed. Do not replace the tool
with an improvised shell command.
If `prepare_workspace` is unavailable, stop after confirming the configuration. Report that
workspace preparation is not installed. Do not replace the tool with an improvised shell
command.

## 4. Hand off to the optimization program

When preparation is `ready`, start a fresh Codex session with exactly this task:
When preparation is `ready`, start a fresh Codex session in the returned worktree with
exactly this task:

```text
Read PROGRAM.md and start the optimization loop.
Expand Down
8 changes: 0 additions & 8 deletions plugins/openflywheel/skills/workspace-init/assets/PROGRAM.md

This file was deleted.

5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "openflywheel"
version = "0.1.0"
version = "0.4.0"
description = "A governed self-improving agent harness"
requires-python = ">=3.11"
dependencies = [
Expand All @@ -14,6 +14,9 @@ dependencies = [
"pydantic>=2.10,<3",
]

[project.scripts]
openflywheel-mcp = "ofw.mcp:main"

[project.optional-dependencies]
dev = [
"mypy>=1.10,<2",
Expand Down
Loading