Documentation
¶
Overview ¶
Package harnessx is the single choke point every SEC-AF agent uses to call the AgentField harness for structured output.
It replaces two things the Python source does implicitly:
- `app.harness(prompt=..., schema=SomeModel, cwd=..., project_dir=...)` — the pydantic model is resolved here into the JSON-schema map the Go SDK consumes, by GO TYPE NAME (see SchemaFor);
- `extract_harness_result(result, SomeModel, "AgentName")` from src/sec_af/agents/_utils.py — ported byte-for-byte as Extract, including the diagnostic block it prints to stdout.
RunExtract combines both, because `result = await app.harness(...)` followed immediately by `extract_harness_result(result, Model, name)` is what every single agent module in src/sec_af/agents does.
Index ¶
- Variables
- func Extract[T any](res *appx.HarnessResult, dest *T, agentName string) (T, error)
- func FieldOrders(t reflect.Type) map[string][]string
- func FieldOrdersFor[T any]() map[string][]string
- func FixtureNames() []string
- func JSONFieldNames(t reflect.Type) []string
- func LoadFixture(name string) (map[string]any, error)
- func Run[T any](ctx context.Context, app appx.Harnesser, prompt string, ...) (*T, *appx.HarnessResult, error)
- func RunExtract[T any](ctx context.Context, app appx.Harnesser, prompt string, ...) (T, error)
- func SchemaFor[T any]() map[string]any
Constants ¶
This section is empty.
Variables ¶
var Diagnostics io.Writer = io.Discard
Diagnostics is where Extract writes the diagnostic block Python printed to stdout for a failed or unparseable harness run. It is io.Discard by default: inside codeaf stdout is the terminal the person is looking at, so the lines go nowhere unless the codeaf side points this at a log. Set it before any audit runs; it is read concurrently by every agent.
Functions ¶
func Extract ¶
func Extract[T any](res *appx.HarnessResult, dest *T, agentName string) (T, error)
Extract ports extract_harness_result from src/sec_af/agents/_utils.py exactly:
def extract_harness_result(result, schema, agent_name):
is_error = bool(getattr(result, "is_error", False))
if is_error:
...
print(f"[{agent_name}] HARNESS ERROR: {error_message}\n"
f" turns={num_turns}, duration_ms={duration_ms}\n"
f" result_text={str(result_text)[:500] if result_text else None}",
flush=True)
raise RuntimeError(f"{agent_name} harness error: {error_message}")
parsed = getattr(result, "parsed", None)
if isinstance(parsed, schema):
return parsed
debug_message = (...)
if isinstance(parsed, dict):
try: return schema.model_validate(parsed)
except Exception: print(debug_message, flush=True); raise
print(debug_message, flush=True)
raise TypeError(f"{agent_name} did not return a valid {schema.__name__}")
dest is the pointer Run handed to app.Harness. An appx.Harnesser sets HarnessResult.Parsed to that same pointer once it holds a schema-valid object (the SDK runner this seam used to drive did exactly that), so a non-nil Parsed means "*dest is populated and schema-valid" — the exact condition `isinstance(parsed, schema)` tests in Python.
Python parity notes:
- The diagnostics are reproduced verbatim (written to Diagnostics rather than stdout), including the `result_text=None` spelling that Python's `... if result_text else None` produces for an EMPTY result string (falsy, not just missing), and the 500-character (not byte) truncation of Python's str slice.
- `type(result).__name__` in the debug line is the Python SDK's `HarnessResult`; Go's concrete type is `appx.HarnessResult`, so the line prints "Result". The line is diagnostic only.
- The `isinstance(parsed, dict)` branch is Python duck-typing for a SDK that may hand back a raw dict. The Go SDK's Parsed is always the `dest` pointer it was given, so that branch is unreachable here and is deliberately not ported.
func FieldOrders ¶
FieldOrders returns, for t and every struct type reachable from it, the ordered json field names of that struct keyed by its Go type name. Anonymous (embedded) structs are flattened, `json:"-"` fields are dropped, and a field with no json tag keeps its Go name — the same rules encoding/json applies.
func FieldOrdersFor ¶
FieldOrdersFor is FieldOrders for a type parameter.
func FixtureNames ¶
func FixtureNames() []string
FixtureNames lists every committed schema fixture basename, sorted.
func JSONFieldNames ¶
JSONFieldNames returns t's json field names in declaration order.
func LoadFixture ¶
LoadFixture decodes the committed pydantic schema fixture with the given basename (== the pydantic class name). Exported for tests and for tooling that needs a schema without a Go type in hand.
func Run ¶
func Run[T any](ctx context.Context, app appx.Harnesser, prompt string, opts appx.HarnessOptions) (*T, *appx.HarnessResult, error)
Run is the Go form of
result = await app.harness(prompt=prompt, schema=Model, cwd=..., project_dir=...)
It resolves Model's schema by Go type name (SchemaFor), allocates the destination, and calls app.Harness.
The returned error is NON-NIL ONLY when the run could not be made at all. A harness that ran and failed (the agent gave up, unparseable output, schema validation exhausted) comes back as a nil error with HarnessResult.IsError set, exactly as the Python SDK returned a HarnessResult with is_error=True rather than raising; appx.HarnessResult documents that contract for the codeaf implementation. Turning that into an error is Extract's job, so the diagnostic block is written in one place.
*T is always non-nil so callers can hand it straight to Extract; it holds the harness-populated value when HarnessResult.Parsed is set and a default-seeded value otherwise.
func RunExtract ¶
func RunExtract[T any](ctx context.Context, app appx.Harnesser, prompt string, opts appx.HarnessOptions, agentName string) (T, error)
RunExtract is the shape every SEC-AF agent module actually uses:
result = await app.harness(prompt=prompt, schema=Model, cwd=harness_cwd, project_dir=repo_path) return extract_harness_result(result, Model, "AgentName")
A transport error from app.Harness propagates unchanged (Python would let the SDK's exception propagate out of the `await` the same way); everything else goes through Extract.
func SchemaFor ¶
SchemaFor resolves the JSON-schema map the Go SDK harness consumes for T.
Resolution order:
- testdata/schemas/<reflect.TypeOf(T).Name()>.json, embedded above. This is the pydantic model_json_schema() of the identically named Python class.
- invopop reflection over the Go type, for types with no fixture (ad-hoc test structs, and any future destination whose Python counterpart is not in gen_schemas.py's MODELS list).
Why the fixture must win: the SDK runs REAL JSON-Schema validation on parsed output (harness/runner.go -> runSchemaValidation, santhosh-tekuri/jsonschema/v5) and retries the whole harness call when it fails. An invopop reflection marks every field required, renders pointer fields non-nullable and sets additionalProperties:false, so Python-valid model output would be rejected and the node would burn retries and drop findings.
Invopop reflector configuration (fallback path only):
- ExpandedStruct: inline the root type's own properties at the top level so map["properties"] is populated for the SDK's DiagnoseOutputFailure.
- DoNotReference=false (default): emit a $defs map for nested struct types.
- Anonymous: suppress the auto-generated $id derived from the package path.
Types ¶
This section is empty.