runtime

package
v0.15.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 15, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

Documentation

Index

Constants

View Source
const (
	AuditSessionStart     = "session_start"
	AuditSessionEnd       = "session_end"
	AuditToolExec         = "tool_exec"
	AuditEgressAllowed    = "egress_allowed"
	AuditEgressBlocked    = "egress_blocked"
	AuditLLMCall          = "llm_call"
	AuditGuardrail        = "guardrail_check"
	AuditScheduleFire     = "schedule_fire"
	AuditScheduleComplete = "schedule_complete"
	AuditScheduleSkip     = "schedule_skip"
	AuditScheduleModify   = "schedule_modify"

	// Auth events. Carry no PII (no email, no claims, no token bytes) —
	// only the audit subject (UserID), tenant (OrgID), and structural
	// metadata. See the auth-package middleware for the emitter.
	EventAuthVerify = "auth_verify" // every successful auth decision
	EventAuthFail   = "auth_fail"   // every failed auth decision (with reason code)

	// MCP events. Like auth, these carry NO byte payload — never the
	// arguments to a tool call, never the result content. Emitters
	// include only sizes (args_size, result_size), durations, server +
	// tool names, and reason codes. See forge-core/mcp and
	// forge-core/tools/adapters/mcp_tool.go.
	EventMCPServerStarted  = "mcp_server_started"
	EventMCPServerFailed   = "mcp_server_failed"
	EventMCPServerDegraded = "mcp_server_degraded"
	EventMCPToolCall       = "mcp_tool_call"
	EventMCPToolResult     = "mcp_tool_result"
	EventMCPToolConflict   = "mcp_tool_conflict"
	EventMCPTokenRefresh   = "mcp_token_refresh"

	// Agent Card events. Emitted once at agent startup with the
	// finalized A2A Agent Card content for traceability. Carries the
	// card's name, version, URL, protocolVersion, skill count, and a
	// sha256 hash of the JSON-encoded card so consumers can detect
	// config drift. See forge-cli/runtime/runner.go's startup pass
	// and the A2A 0.3.0 spec.
	EventAgentCardPublished = "agent_card_published"

	// Lifecycle events emitted at A2A invocation boundaries.
	// AuditInvocationComplete carries total wall-clock duration_ms for
	// the full invocation (auth → dispatch → engine.Execute → response).
	// See issue #87 / FWS-3.
	AuditInvocationComplete = "invocation_complete"

	// AuditLLMCallCancelled is emitted when a streaming LLM call is
	// cancelled mid-flight; carries partial usage counts captured up to
	// the cancellation point. See issue #87 / FWS-3.
	AuditLLMCallCancelled = "llm_call_cancelled"

	// AuditPolicyLoaded is emitted once at agent startup when a
	// non-zero platform policy is present. Carries a summary of the
	// effective policy (sizes of deny lists, max bounds) so audit
	// consumers can confirm which policy was active during a given
	// run without parsing the policy file itself. Absent when no
	// policy is configured. See issue #89 / FWS-5.
	AuditPolicyLoaded = "policy_loaded"

	// AuditPolicyViolationAtBuildTime is emitted when forge.yaml's
	// declaration conflicts with the platform policy at startup
	// (e.g., declares a domain on the policy deny list, declares a
	// forbidden model, exceeds size bounds). Carries the conflict
	// detail in Fields. Emitted ONCE per startup before the runner
	// aborts with a non-zero exit, so the violation lands in the
	// audit pipeline even though the agent never serves traffic.
	// See issue #89 / FWS-5.
	AuditPolicyViolationAtBuildTime = "policy_violation_at_build_time"

	// AuditChannelDeniedByPolicy is emitted at agent startup when a
	// channel adapter would have been started but a policy layer
	// (system / user / workspace) names it on its denied_channels
	// list. The channel is NOT started; the runner continues with
	// the remaining channels rather than aborting — unlike the
	// egress/tool/model violations, channel deny is treated as a
	// scope-down, not as a forge.yaml conflict.
	//
	// Carries fields.channel (registry name), fields.layer
	// ("system" / "user" / "workspace") identifying which file
	// enforced, and fields.source (path to that file). When the
	// channel is denied by multiple layers, the first-match wins
	// for attribution (system > user > workspace precedence; the
	// most restrictive layer takes credit).
	//
	// See issue #90 / FWS-6.
	AuditChannelDeniedByPolicy = "channel_denied_by_policy"

	// AuditInvocationCancelled is emitted when an in-flight A2A
	// invocation is cancelled by tasks/cancel (or internal cancellation
	// like a parent ctx deadline). Carries the classified reason in
	// Fields["reason"], the wall-clock duration up to cancellation in
	// DurationMs, and aggregated partial usage in Fields when any LLM
	// calls completed before the cancel signal. See issue #88 / FWS-4.
	AuditInvocationCancelled = "invocation_cancelled"

	// Deprecated: use EventAuthVerify. Kept as a string alias so any
	// audit-log consumer that grep'd for "auth_success" can be migrated.
	// Scheduled for removal in v0.11.0.
	AuditAuthSuccess = EventAuthVerify
	// Deprecated: use EventAuthFail. Same migration window as AuditAuthSuccess.
	AuditAuthFailure = EventAuthFail
)

Audit event type constants.

View Source
const (
	EnvAuditSocket       = "FORGE_AUDIT_SOCKET"
	EnvAuditHTTPEndpoint = "FORGE_AUDIT_HTTP_ENDPOINT"
	EnvAuditWriteTimeout = "FORGE_AUDIT_WRITE_TIMEOUT"
)

Environment variable names. Exposed for `forge run --help` text and for the integration test. The CLI in forge-cli/cmd/run.go reads these and surfaces matching --audit-* flags; flag wins over env.

View Source
const (
	HeaderForgeOrgID       = "X-Forge-Org-ID"
	HeaderForgeWorkspaceID = "X-Forge-Workspace-ID"
)

Tenancy header names (issue #157). The X-Forge- prefix is deliberate: these are Forge-defined override headers, distinct from the X-Org-ID / org_id headers the auth providers parse to resolve the user's identity. The auth-derived org_id continues to live in auth_verify.fields.org_id for back-compat; these headers populate the top-level audit fields that get stamped on EVERY event.

Header semantics:

  • Absent: the AuditLogger's deployment-time stamp wins (env vars FORGE_ORG_ID / FORGE_WORKSPACE_ID resolved at startup). This is the static-tenancy case — agent deployed into one workspace, no per-request routing.
  • Present: the header value overrides the env stamp for that invocation. This is the multi-tenant case — one Forge agent serves many workspaces, the orchestrator routes per request.

Both: header wins. Neither: top-level fields are omitted entirely and emitted JSON matches the pre-tenancy shape.

View Source
const (
	HeaderWorkflowID       = "X-Workflow-ID"
	HeaderWorkflowStageID  = "X-Workflow-Stage-ID"
	HeaderWorkflowStepID   = "X-Workflow-Step-ID"
	HeaderInvocationCaller = "X-Invocation-Caller"
)

Workflow correlation header names (issue #86 / FWS-2). Sent by any A2A-compatible orchestrator on every request that's part of a workflow execution. Header names are deliberately vendor-neutral so any orchestrator (initializ Command, custom registries, third-party platforms) can drive Forge's correlation surface without adopting a vendor prefix. Forge agents extract them at the request boundary, stash them in context.Context, and tag every audit event with the matching workflow / stage / step identifiers so audit consumers can correlate events across multiple agents participating in the same workflow.

Absence of these headers is the normal case for direct A2A invocations (e.g. local development, peer agents not orchestrated). When absent, audit events emit without the workflow fields — full backward compatibility with pre-FWS-2 audit consumers.

View Source
const AuditExportStatus = "audit_export_status"

AuditExportStatus is the event type for the periodic per-sink health report. Single event per tick; carries one entry in fields.sinks per registered sink with that sink's counters (writes_ok, drops_timeout, drops_dial, connected).

View Source
const AuditSchemaVersion = "1.0"

AuditSchemaVersion is the current audit event contract version. Every emitted event carries this string in its `schema_version` field so consumers can detect schema upgrades. Backward-compatible additions (new optional fields) do NOT bump the version; removals or semantic changes do.

Version policy:

1.0 — initial documented contract (issue #91 / FWS-8). Includes the
      pre-FWS-8 fields (ts, event, correlation_id, task_id, workflow_*,
      model, provider, input_tokens, output_tokens, duration_ms,
      request_id, fields) plus seq and schema_version.

See docs/security/audit-logging.md for the full schema reference.

View Source
const DefaultPayloadCaptureCapBytes = 16 << 10

DefaultPayloadCaptureCapBytes is the per-field byte cap when the caller doesn't override. 16 KiB matches the runtime's "long-tool-output" threshold (the same threshold the chat-side path uses to switch to a file part), so audit captures roughly align with what's visible in the chat UI.

View Source
const DefaultSpanContentCapBytes = 4 << 10

DefaultSpanContentCapBytes is the per-attribute byte cap for span content. 4 KiB stays comfortably under common observability backend limits (Datadog caps attributes around 5 KiB; Tempo's default attr length limit is 4 KiB) so a long prompt doesn't get re-truncated by the backend with a different marker shape, breaking the correlate-by-marker grep flow.

View Source
const InstrumentationName = "github.com/initializ/forge"

InstrumentationName is the OTel instrumentation scope name used for every tracer Forge obtains. Pinned to the module path so OTel backends can distinguish Forge spans from spans emitted by other instrumentation in the same process. Subsequent phases (#102–#107 in the OTel v1 initiative, #108) read this same constant — do not duplicate it.

View Source
const ProtocolVersion = "0.3.0"

ProtocolVersion is the A2A protocol version every AgentCard claims to conform to. Pinned at build time. Bumping is a deliberate PR, not a runtime negotiation — same discipline Forge uses for the MCP protocol version pin.

View Source
const RedactionMarker = "[REDACTED]"

RedactionMarker is the placeholder substituted for any matched secret. Operators grepping audit logs and traces for "[REDACTED]" can correlate scrub events across both pipelines.

Variables

View Source
var AuditExportStatusInterval = 60 * time.Second

AuditExportStatusInterval is how often StartAuditExportStatus emits an audit_export_status event. The issue calls for 60s; exposed as a package var so tests can shorten it.

View Source
var ModelContextWindows = map[string]int{
	"gpt-4o":        128_000,
	"gpt-4":         128_000,
	"gpt-5":         128_000,
	"gpt-3.5":       16_000,
	"claude-opus":   200_000,
	"claude-sonnet": 200_000,
	"claude-haiku":  200_000,
	"gemini-2.5":    1_000_000,
	"gemini-2.0":    1_000_000,
	"llama3.1":      128_000,
	"llama3":        8_000,
	"mistral":       32_000,
	"codellama":     16_000,
	"deepseek":      64_000,
	"qwen":          32_000,
}

ModelContextWindows maps model name prefixes to context window sizes (in tokens).

Functions

func AgentCardFromConfig

func AgentCardFromConfig(cfg *types.ForgeConfig, baseURL string) *a2a.AgentCard

AgentCardFromConfig constructs an AgentCard from a ForgeConfig and a base URL. The baseURL should be a fully-formed URL (e.g. "http://localhost:8080").

Used when no build-time AgentSpec is available (e.g. local `forge dev` from a freshly-scaffolded project). Identical conformance to A2A 0.3.0 as the spec-derived path.

func AgentCardFromSpec

func AgentCardFromSpec(spec *agentspec.AgentSpec, baseURL string) *a2a.AgentCard

AgentCardFromSpec constructs an AgentCard from an AgentSpec and a base URL. The baseURL should be a fully-formed URL (e.g. "http://localhost:8080").

Per A2A 0.3.0 the card requires `version`, `protocolVersion`, `defaultInputModes`, and `defaultOutputModes`. The function fills those from the spec / config defaults; callers can override after construction (e.g. the runner enriches with SecuritySchemes derived from the auth chain).

func AppendSkillsFromDescriptors

func AppendSkillsFromDescriptors(card *a2a.AgentCard, descs []contract.SkillDescriptor)

AppendSkillsFromDescriptors maps the runtime's SkillDescriptor list (sourced from the embedded + local skill registries) into A2A AgentSkill objects and appends them to the card. Skill IDs already present on the card are skipped so this is safe to call after AgentCardFromSpec / AgentCardFromConfig — those populate the card from build-time artifacts; this fills in any runtime-registered skills the build artifact didn't cover.

Mapping (Forge SKILL.md → A2A AgentSkill):

SkillDescriptor.Name        → Skill.ID + Skill.Name
SkillDescriptor.DisplayName → Skill.Name (when present)
SkillDescriptor.Description → Skill.Description
SkillDescriptor.Category    → Skill.Tags[0] (when present)
SkillDescriptor.Tags        → Skill.Tags (appended)

A2A 0.3.0 requires Tags to be non-empty; when neither category nor tags are set, we fall back to ["skill"] so the field is always populated.

Forge-internal fields (RequiredEnv, RequiredBins, EgressDomains, DeniedTools, TimeoutHint, Provenance) are intentionally NOT mapped into the card. The Agent Card is a public discovery surface; those fields are runtime contracts that stay inside Forge.

func CapOrDefault

func CapOrDefault(configured int) int

CapOrDefault picks the configured cap for the field, falling back to the package default when zero. Negative values are clamped to the default; "0 means no capture" is what AnyEnabled / the per- field flag covers — once a flag is on, some cap applies. Exported so the runner's hook layer can pick the right cap per field without duplicating the fallback logic.

func ContextBudgetForModel

func ContextBudgetForModel(model string) int

ContextBudgetForModel returns the character budget for a given model name. Uses prefix matching against known models, falls back to defaultContextTokens. Prefixes are checked longest-first to avoid e.g. "llama3" matching before "llama3.1".

func CorrelationIDFromContext

func CorrelationIDFromContext(ctx context.Context) string

CorrelationIDFromContext retrieves the correlation ID from the context. Returns "" if not set.

func ExtractText

func ExtractText(msg *a2a.Message) string

ExtractText extracts all text parts from a message into a single string.

func FilesDirFromContext

func FilesDirFromContext(ctx context.Context) string

FilesDirFromContext retrieves the files directory from the context. Returns "" if not set.

func GenerateID

func GenerateID() string

GenerateID produces a 16-character hex random ID using crypto/rand.

func HTTPEndpointForLog

func HTTPEndpointForLog(s Sink) string

HTTPEndpointForLog is exposed so the runner's startup banner can log "exporting audit to <endpoint>" without forcing the runner to reach into a private field. Returns the endpoint URL.

func NextSequence

func NextSequence(ctx context.Context) int64

NextSequence increments the per-invocation counter and returns the new value. Atomic; safe to call from multiple goroutines without external synchronization. Returns 0 when no counter is in context (caller can JSON-omit the field).

func ParseEnvVars

func ParseEnvVars(r io.Reader) (map[string]string, error)

ParseEnvVars reads key=value pairs from an io.Reader. Supports # comments, double/single quotes, and export prefix.

func PopulateSecuritySchemes

func PopulateSecuritySchemes(card *a2a.AgentCard, cfg *types.ForgeConfig)

PopulateSecuritySchemes derives the A2A SecuritySchemes + Security requirements from the agent's configured auth chain (forge.yaml auth: block) and writes them into the card. The function is additive: it preserves any schemes the caller has already set.

The mapping mirrors what Forge's auth middleware actually accepts:

static_token   → http + bearer (opaque token)
http_verifier  → http + bearer (token validated by external endpoint)
oidc           → openIdConnect with issuer discovery URL
azure_ad       → openIdConnect (AAD exposes a standard OIDC discovery)
gcp_iap        → apiKey in header (X-Goog-Iap-Jwt-Assertion)
aws_sigv4      → http + bearer with bearerFormat "forge-aws-v1"
                 (forge-specific Sigv4-reflection-via-bearer pattern)

Every chain entry produces one scheme name = entry's Name field (or Type when Name is empty). The Security array carries one map per scheme; the outer list is OR (any one suffices), matching Forge's first-match-wins chain semantics.

When no auth is configured, no schemes are emitted — A2A 0.3.0 treats absence as "no auth required."

func PrepareSpanContent

func PrepareSpanContent(s string, redact bool, maxBytes int) string

PrepareSpanContent runs the redact (when redact=true) and byte-cap-with-truncation-marker pipeline for content destined for an OTel span attribute. The pipeline is:

  1. Apply RedactSecrets when redact=true.
  2. TruncateForAudit (the same byte-cap helper the audit path uses) so a runaway prompt can't blow past the backend attribute limit and silently drop the marker.

maxBytes <= 0 falls back to DefaultSpanContentCapBytes. The truncation marker is identical to what AuditPayloadCapture writes, so an operator who sees a `…[truncated:N]` suffix on an audit payload-captured field sees the same suffix on the linked span attribute for the same logical event.

Returns the empty string when s is empty (skipping the pipeline).

func RedactSecrets

func RedactSecrets(s string) string

RedactSecrets returns s with every known secret token shape replaced by RedactionMarker. Empty input is returned unchanged (fast path).

Applied in pattern-list order; overlap is fine because ReplaceAllString rewrites the string left-to-right and subsequent patterns operate on the post-replacement output. A run that matches multiple shapes (e.g. an `sk-` prefix that also starts a longer vendor key) is scrubbed once — RedactionMarker doesn't satisfy any other pattern, so re-applying patterns is idempotent.

func ResetTracerProviderForTest

func ResetTracerProviderForTest()

ResetTracerProviderForTest restores the no-op provider. Exists so tests that install a real provider can cleanly tear down. Not part of the production wiring — kept exported so tests in the forge-core/observability subpackage (Phase 1) can also use it.

func SetTracerProvider

func SetTracerProvider(tp trace.TracerProvider)

SetTracerProvider installs the given TracerProvider as the process-wide tracer source for forge-core. Also installs it as the OTel global so any third-party library Forge depends on (the OTLP exporter's own transport, future runtime-loaded SDKs) sees the same provider when it calls otel.Tracer() directly. Also installs the W3C tracecontext + baggage composite propagator as the OTel global text-map propagator so inbound/outbound HTTP plumbing (Phase 5, #106) can extract / inject trace context without further setup.

Calling SetTracerProvider with nil is a no-op (defensive guard for the cli wiring path — a misconfigured exporter resolution must not install a nil provider that would crash on the first Tracer call).

Safe to call from any goroutine. Subsequent calls replace the previous provider; intended to fire exactly once at agent startup.

func StartAuditExportStatus

func StartAuditExportStatus(ctx context.Context, audit *AuditLogger) (stop func())

StartAuditExportStatus spawns a background goroutine that emits one audit_export_status event every AuditExportStatusInterval until the returned stop function is called or ctx is cancelled. The goroutine uses the same AuditLogger it reports on — the status event itself flows through every sink, including the export ones, so operators can see "is my export healthy?" by inspecting the export stream.

Returns a stop func the caller invokes during shutdown. The stop func blocks until the goroutine exits so shutdown ordering is deterministic (no chance of a final status event landing after the runtime has torn down its writers).

Idempotent: calling stop twice is safe.

See issue #95 / FWS-7 acceptance criterion §6.

func TaskIDFromContext

func TaskIDFromContext(ctx context.Context) string

TaskIDFromContext retrieves the task ID from the context. Returns "" if not set.

func Tracer

func Tracer() trace.Tracer

Tracer returns a tracer scoped to the Forge instrumentation name. When no provider has been installed, the returned tracer is the no-op tracer — every span it produces has IsValid() == false and records nothing. Hot-path code calls Tracer().Start unconditionally; the no-op short-circuit is cheap by design.

func TruncateForAudit

func TruncateForAudit(s string, max int) string

TruncateForAudit returns s truncated to at most max bytes; if s exceeded the cap, the returned string ends with the suffix `…[truncated:N]` where N is the original byte length. Use for every captured field so a runaway prompt can't bloat one event.

The function operates on bytes, not runes — UTF-8 sequences may be split mid-codepoint at the truncation boundary. Audit consumers must treat captured strings as opaque bytes, not as user-renderable text. The size info in the field name (`prompt_messages_size_bytes` vs `prompt_messages`) is the contract.

func WithCorrelationID

func WithCorrelationID(ctx context.Context, id string) context.Context

WithCorrelationID stores a correlation ID in the context.

func WithFilesDir

func WithFilesDir(ctx context.Context, dir string) context.Context

WithFilesDir stores a files directory path in the context.

func WithLLMUsageAccumulator

func WithLLMUsageAccumulator(ctx context.Context, acc *LLMUsageAccumulator) context.Context

WithLLMUsageAccumulator stashes a per-invocation accumulator in ctx. The runner creates one per A2A invocation at request entry; the AfterLLMCall hook reads it via LLMUsageAccumulatorFromContext and folds each call's counts into the totals.

func WithProgressEmitter

func WithProgressEmitter(ctx context.Context, emitter ProgressEmitter) context.Context

WithProgressEmitter stores a ProgressEmitter in the context.

func WithSequenceCounter

func WithSequenceCounter(ctx context.Context, c *SequenceCounter) context.Context

WithSequenceCounter stores a per-invocation sequence counter in the context. Called by the A2A request entry point exactly once per invocation; every audit emit downstream picks the counter up via SequenceCounterFromContext. Events emitted outside an invocation scope (startup banners, policy_loaded) inherit no counter and emit with Sequence == 0 (which JSON-omits via omitempty).

func WithTaskID

func WithTaskID(ctx context.Context, id string) context.Context

WithTaskID stores a task ID in the context.

func WithTenancyContext

func WithTenancyContext(ctx context.Context, t TenancyContext) context.Context

WithTenancyContext stores a TenancyContext in the request context. Called at the A2A request boundary right after the workflow context is installed, so per-invocation handlers and the downstream audit emitters see both.

func WithWorkflowContext

func WithWorkflowContext(ctx context.Context, w WorkflowContext) context.Context

WithWorkflowContext stores a WorkflowContext in the request context. Mirrors the WithCorrelationID / WithTaskID pattern already used by the audit layer.

Types

type AgentExecutor

type AgentExecutor interface {
	// Execute processes a message in the context of a task and returns a response.
	Execute(ctx context.Context, task *a2a.Task, msg *a2a.Message) (*a2a.Message, error)
	// ExecuteStream processes a message and returns a channel of response messages.
	ExecuteStream(ctx context.Context, task *a2a.Task, msg *a2a.Message) (<-chan *a2a.Message, error)
	// Close releases any resources held by the executor.
	Close() error
}

AgentExecutor processes individual messages and returns responses. Unlike AgentRuntime (which manages subprocess lifecycle), the executor focuses solely on message-level processing. The handler (runner.go) manages task lifecycle (submitted -> working -> completed/failed).

type AgentRuntime

type AgentRuntime interface {
	// Start launches the agent backend.
	Start(ctx context.Context) error
	// Invoke sends a synchronous task request and returns the completed task.
	Invoke(ctx context.Context, taskID string, msg *a2a.Message) (*a2a.Task, error)
	// Stream sends a streaming task request and returns a channel of task updates.
	Stream(ctx context.Context, taskID string, msg *a2a.Message) (<-chan *a2a.Task, error)
	// Healthy reports whether the agent backend is responsive.
	Healthy(ctx context.Context) bool
	// Stop shuts down the agent backend.
	Stop() error
	// Restart stops and restarts the agent backend.
	Restart(ctx context.Context) error
}

AgentRuntime abstracts the agent execution backend. Implementations include SubprocessRuntime (real agent process) and MockRuntime (canned responses).

type AuditEvent

type AuditEvent struct {
	Timestamp string `json:"ts"`
	Event     string `json:"event"`

	// SchemaVersion advertises the audit-event contract version every
	// emitted event conforms to. Consumers (initializ platform,
	// custom SIEM pipelines) read this once per agent run to detect
	// schema upgrades. Backward-compatible additions to the schema
	// do not bump the version; removals or semantic changes do.
	// See docs/security/audit-logging.md#schema-contract-fws-8.
	SchemaVersion string `json:"schema_version,omitempty"`

	// Sequence is a per-invocation monotonically increasing counter.
	// Starts at 1 for the first event of an invocation; advances by
	// 1 on each subsequent event from that invocation. Consumers
	// detect gaps (lost / out-of-order events) by comparing
	// Sequence values within a (correlation_id, task_id) group.
	//
	// Sequences are scoped to a single A2A invocation — different
	// invocations start their own counters. Events emitted outside
	// any invocation scope (startup events: policy_loaded,
	// agent_card_published, audit_export_status) have no Sequence
	// and omit the field.
	//
	// See issue #91 / FWS-8.
	Sequence int64 `json:"seq,omitempty"`

	// CorrelationID groups events from a single agent invocation —
	// generated by the A2A handler at request entry.
	CorrelationID string `json:"correlation_id,omitempty"`

	// TaskID is the A2A task identifier (params.id on tasks/send).
	TaskID string `json:"task_id,omitempty"`

	// WorkflowID identifies the orchestrator-level workflow run that
	// invoked this agent. Sourced from X-Workflow-ID at request entry;
	// absent for direct A2A invocations.
	WorkflowID string `json:"workflow_id,omitempty"`

	// StageID identifies the workflow stage that invoked this agent.
	StageID string `json:"stage_id,omitempty"`

	// StepID identifies the workflow step that invoked this agent.
	StepID string `json:"step_id,omitempty"`

	// InvocationCaller identifies the upstream caller (orchestrator
	// or upstream agent in an agent-to-agent flow).
	InvocationCaller string `json:"invocation_caller,omitempty"`

	// OrgID + WorkspaceID stamp the tenancy this agent run belongs
	// to. Sourced from one of three layers (highest precedence first):
	//
	//   1. Explicit value set on the event before emit.
	//   2. Per-request override headers parsed at the A2A boundary
	//      (X-Forge-Org-ID / X-Forge-Workspace-ID) and stashed on the
	//      context via WithTenancyContext.
	//   3. Deployment-time stamp installed on the AuditLogger via
	//      WithTenancy(orgID, workspaceID) — typically populated from
	//      FORGE_ORG_ID / FORGE_WORKSPACE_ID at agent startup.
	//
	// Both keys use omitempty so deployments that don't set tenancy
	// keep emitting the pre-tenancy JSON shape verbatim. The
	// AuditSchemaVersion is NOT bumped — additive optional fields are
	// schema-compatible per the documented policy. See issue #157.
	//
	// Distinct from the auth-derived `auth_verify.fields.org_id`,
	// which continues to carry whatever the inbound token claimed.
	// The top-level OrgID here is the operator's declared tenancy,
	// trusted because the deployment / orchestrator set it.
	OrgID       string `json:"org_id,omitempty"`
	WorkspaceID string `json:"workspace_id,omitempty"`

	// EntityID + EntityType identify which entity emitted this event.
	// Sourced from two layers (highest precedence first):
	//
	//   1. Explicit value set on the event before emit.
	//   2. Deployment-time stamp installed on the AuditLogger via
	//      WithEntity(entityType, entityID) — typically populated from
	//      FORGE_AGENT_ID / cfg.AgentID at agent startup, with
	//      EntityType hardcoded to "agent" for now.
	//
	// No per-request ctx layer: entity identity is fixed at process
	// startup. If an agent serves multiple tenancies per request, the
	// OrgID / WorkspaceID layer above already covers that.
	//
	// Field names + values match the guardrails library's BasePayload
	// vocabulary (EntityID, EntityType — "agent" / "workflow" /
	// "assistant"), so the Forge NDJSON stream and the library's
	// MongoDB GuardrailAuditEvent collection share columns 1:1 and
	// can be joined without a translation table. EntityType is
	// hardcoded to "agent" today since Forge only runs agents;
	// future entity types are an additive value change, not a schema
	// change.
	//
	// Both keys use omitempty so deployments that don't set agent_id
	// keep emitting the pre-#164 JSON shape verbatim.
	EntityID   string `json:"entity_id,omitempty"`
	EntityType string `json:"entity_type,omitempty"`

	// LLM call attribution (llm_call, llm_call_cancelled, invocation_complete).
	Model    string `json:"model,omitempty"`
	Provider string `json:"provider,omitempty"`

	// Token counts captured from provider response metadata. Nil when
	// the event is not an LLM call. Non-nil with zero values + a true
	// TokensUnavailable flag when the provider did not return usage
	// (e.g. some self-hosted Ollama setups).
	InputTokens       *int `json:"input_tokens,omitempty"`
	OutputTokens      *int `json:"output_tokens,omitempty"`
	TokensUnavailable bool `json:"tokens_unavailable,omitempty"`

	// DurationMs is the wall-clock duration in milliseconds. Populated on
	// llm_call, tool_exec, and invocation_complete events.
	DurationMs *int64 `json:"duration_ms,omitempty"`

	// RequestID is the provider-specific call identifier (Anthropic
	// `id`, OpenAI `id`, etc.) — kept as an opaque debug-correlation
	// handle, never used for cost attribution.
	RequestID string `json:"request_id,omitempty"`

	// TraceID + SpanID cross-link this audit event to the OTel trace
	// the same logical operation produced (Phase 4 of the OTel
	// Tracing v1 initiative — issue #105 / #108). Populated by
	// EmitFromContext when the context carries a recording span;
	// omitted when there is no span on the context or the tracer is
	// the noop default (tracing disabled).
	//
	// Format: lowercase hex, matching W3C traceparent semantics —
	// trace_id is 32 hex chars (128-bit), span_id is 16 hex chars
	// (64-bit). Operators paste these directly into their trace
	// backend's search box to pivot from an audit row to the parent
	// trace, and vice versa.
	//
	// Backward compatibility: both fields use omitempty so consumers
	// that have not been upgraded continue to see the pre-Phase-4
	// shape verbatim (no trace_id / span_id keys at all). The
	// AuditSchemaVersion is NOT bumped — adding optional fields is a
	// schema-compatible change per the documented policy.
	TraceID string `json:"trace_id,omitempty"`
	SpanID  string `json:"span_id,omitempty"`

	Fields map[string]any `json:"fields,omitempty"`
}

AuditEvent is a single structured audit record emitted as NDJSON.

Workflow correlation fields (WorkflowID, StageID, StepID, InvocationCaller) are tagged onto every event emitted via EmitFromContext when the request carries `X-Workflow-*` / `X-Invocation-Caller` headers from any A2A-compatible orchestrator. Direct A2A invocations omit them entirely so the JSON shape matches the pre-FWS-2 audit consumers.

Token usage, duration, model, and provider fields (issue #87 / FWS-3) are populated by the LLM call site, tool execution path, and per- invocation lifecycle. They use *int / *int64 pointers so the JSON distinguishes "field absent" (nil) from "field present with zero value" — important for llm_call events where zero is a legitimate count and TokensUnavailable signals "provider did not report usage."

Field naming aligns with OTel GenAI semconv (input_tokens / output_tokens / duration_ms) so audit consumers can correlate Forge audit events with OTel traces without a translation table.

type AuditExportConfig

type AuditExportConfig struct {
	// SocketPath is the absolute path to the in-pod Unix Domain Socket
	// the sidecar listens on. Empty disables the socket sink.
	SocketPath string

	// HTTPEndpoint is a localhost URL (e.g. "http://127.0.0.1:9097/v1/audit")
	// the fallback HTTP sink POSTs to. Empty disables the HTTP sink.
	// Ignored when SocketPath is set.
	HTTPEndpoint string

	// WriteTimeout bounds each per-event sink write. Default 50ms.
	// Applies to both the socket and HTTP sinks. The stderr safety-net
	// sink ignores this — stderr writes are bounded by the kernel's
	// pipe buffer, not by us.
	WriteTimeout time.Duration

	// DialTimeout bounds the initial socket dial. Default 1s. Ignored
	// by the HTTP sink (which sets its own per-request timeout to
	// match WriteTimeout).
	DialTimeout time.Duration
}

AuditExportConfig configures the FWS-7 export sinks (issue #95). It is intentionally minimal — three knobs only — because each one maps to a single CLI flag / env var pair and corresponds to one operational decision the deployer has to make:

  • SocketPath: "where does the in-pod sidecar listen?"
  • HTTPEndpoint: "where does the fallback HTTP receiver listen?"
  • WriteTimeout: "how long am I willing to spend per emit before dropping?"

Default zero value means "no export sinks; behave exactly like pre-FWS-7 (stderr only)." This is the right default because the initializ platform deploy receiver injects the env vars; self-managed deployments without a sidecar get the unchanged stderr stream.

When both SocketPath and HTTPEndpoint are non-empty, SocketPath wins (preferred sink path). The HTTP fallback is purely for environments where Unix sockets aren't available — typically Windows containers or platform-managed sandboxes that forbid unix:// dialing.

func AuditExportConfigFromEnv

func AuditExportConfigFromEnv() AuditExportConfig

AuditExportConfigFromEnv reads the three env vars and returns a populated config. Designed for the case where the CLI flag was not set; the caller (`forge run --audit-socket=...`) overrides specific fields after this call. WriteTimeout parses Go duration syntax ("50ms", "200ms"); a parse failure falls back to default (zero, which downstream maps to 50ms).

type AuditLogger

type AuditLogger struct {
	// contains filtered or unexported fields
}

AuditLogger fans serialized NDJSON audit events out to a slice of Sinks. The traditional single-writer constructor wraps the writer in a writerSink; the FWS-7 multi-sink constructor (NewAuditLoggerFromConfig) composes a stderr safety-net sink with an optional Unix socket or localhost HTTP sink for export to a sidecar.

Emit-side semantics:

  • Each sink's Write is called sequentially. Each sink is responsible for its own timeout/drop behavior; the AuditLogger never spawns a goroutine per event. This bounds emit latency to the sum of sink timeouts (stderr is microseconds; socket/HTTP is the configured per-write timeout, default 50ms).
  • Errors from a sink are logged once per (sink, error-class) and suppressed thereafter — a broken sidecar must not flood the operational logs.
  • Events leaving each sink are byte-identical. No sink transforms the payload.

func NewAuditLogger

func NewAuditLogger(w io.Writer) *AuditLogger

NewAuditLogger creates a single-sink AuditLogger wrapping the given writer. Backward-compatible with pre-FWS-7 callers; tests and the CLI's per-command audit loggers (channel.go / run.go) continue to use this. Production code paths that need the export sink should use NewAuditLoggerFromConfig.

func NewAuditLoggerFromConfig

func NewAuditLoggerFromConfig(cfg AuditExportConfig) *AuditLogger

NewAuditLoggerFromConfig constructs an AuditLogger with the standard FWS-7 sink stack:

  • stderr safety-net sink (always registered first; the operator can still grep audit NDJSON out of container logs even if the sidecar is down)
  • socket sink when cfg.SocketPath is set
  • HTTP sink when cfg.SocketPath is empty and cfg.HTTPEndpoint is set

When both export-sink fields are empty, behavior is identical to NewAuditLogger(os.Stderr) — pre-FWS-7 compatibility.

func (*AuditLogger) AddSink

func (a *AuditLogger) AddSink(s Sink)

AddSink appends a sink to the fan-out. Safe to call after construction (e.g. from a delayed sidecar discovery), but most callers should construct via NewAuditLoggerFromConfig.

func (*AuditLogger) Close

func (a *AuditLogger) Close(ctx context.Context) error

Close drains every sink with the given deadline. Honors the context; sinks that don't drain in time are abandoned (each sink's Close is responsible for its own per-sink deadline derivation from ctx). Returns the first non-nil error from any sink so callers can surface shutdown problems; later errors are still logged via opsLog.

func (*AuditLogger) Emit

func (a *AuditLogger) Emit(event AuditEvent)

Emit serializes an event and fans it out to every registered sink. Timestamp is populated to RFC3339 (UTC) if absent. Marshal failures are silently dropped — they indicate a programmer error (an AuditEvent with a non-serializable Fields value), and dropping matches the pre-FWS-7 behavior. Per-sink errors are logged once.

Callers that have a request context.Context in scope should prefer EmitFromContext, which auto-tags CorrelationID, TaskID, and workflow-correlation fields.

func (*AuditLogger) EmitChannelDeniedByPolicy

func (a *AuditLogger) EmitChannelDeniedByPolicy(channel, layer, source string)

EmitChannelDeniedByPolicy records that a channel adapter was skipped at startup because a policy layer's denied_channels list named it. `layer` identifies which file enforced ("system" / "user" / "workspace"); `source` is that file's path. Unlike egress/tool/model violations, channel deny does NOT abort startup — the agent runs without the denied channel. Operators see the skip in their audit pipeline and group by layer to understand which policy file owns the decision. See issue #90 / FWS-6.

func (*AuditLogger) EmitFromContext

func (a *AuditLogger) EmitFromContext(ctx context.Context, event AuditEvent)

EmitFromContext writes an audit event after auto-tagging CorrelationID, TaskID, and workflow-correlation fields from the request context. Fields already set on the passed event are preserved — the context is a fallback, not an override. This makes it safe to migrate callers from Emit to EmitFromContext: any already-explicit value continues to win.

func (*AuditLogger) EmitInvocationCancelled

func (a *AuditLogger) EmitInvocationCancelled(ctx context.Context, reason CancellationReason, duration time.Duration, fields map[string]any)

EmitInvocationCancelled emits an invocation_cancelled audit event for an in-flight A2A invocation that was signalled mid-execution via tasks/cancel (or internal cancellation: parent ctx deadline, graceful shutdown). Routed through EmitFromContext so workflow correlation auto-tags. The reason is folded into Fields["reason"] as a string — operators classify these via the CancellationReason constants but consumers should pass-through unknown values.

Partial usage data should be present in fields (the runner reads the per-invocation LLMUsageAccumulator snapshot and adds input_tokens_total / output_tokens_total / llm_call_count / model / provider when llm_call_count > 0). When no LLM calls completed before cancellation, the field map carries reason + state only — downstream billing sees zero tokens which is correct: the invocation was cancelled before incurring spend.

See issue #88 / FWS-4.

func (*AuditLogger) EmitInvocationComplete

func (a *AuditLogger) EmitInvocationComplete(ctx context.Context, duration time.Duration, fields map[string]any)

EmitInvocationComplete emits an invocation_complete audit event carrying the total wall-clock duration of the A2A invocation (auth → dispatch → engine.Execute → response). Routed through EmitFromContext so workflow-correlation fields are inherited from the inbound request. One event per invocation; emitted by the runner at the response boundary. See issue #87 / FWS-3.

func (*AuditLogger) EmitLLMCall

func (a *AuditLogger) EmitLLMCall(ctx context.Context, args LLMCallAuditArgs)

EmitLLMCall builds and emits an llm_call (or llm_call_cancelled) audit event from the captured args. Routed through EmitFromContext so workflow-correlation fields (workflow_id / stage_id / step_id / invocation_caller from FWS-2) auto-tag every LLM call event when the inbound request carried orchestrator headers. This is the shared capture point that the OTel tracing work will hook into. See issue #87 / FWS-3.

func (*AuditLogger) EmitPolicyLoaded

func (a *AuditLogger) EmitPolicyLoaded(fields map[string]any)

EmitPolicyLoaded emits a policy_loaded audit event at agent startup when a non-zero platform policy is active. Fields are a summary of the effective policy (deny-list sizes, max bounds, source path) — NOT the full policy contents, which can be large and may contain internal infrastructure hints operators don't want in every audit stream. Consumers that need the full policy can read the source file via the path field.

Emitted via plain Emit (not EmitFromContext) because no request context exists at startup. See issue #89 / FWS-5.

func (*AuditLogger) EmitPolicyViolationAtBuildTime

func (a *AuditLogger) EmitPolicyViolationAtBuildTime(fields map[string]any)

EmitPolicyViolationAtBuildTime emits a policy_violation_at_build_time audit event when forge.yaml's declaration conflicts with the platform policy. Fields carry the conflict detail (which kind of violation — denied_egress, denied_tool, forbidden_model, size_bound — and the offending value(s)). Called once at startup before the runner returns a non-zero exit; the audit lands even though the agent never serves traffic, so the operator's audit pipeline captures the violation.

Emitted via plain Emit (not EmitFromContext) because no request context exists at startup. See issue #89 / FWS-5.

func (*AuditLogger) EmitToolExec

func (a *AuditLogger) EmitToolExec(ctx context.Context, tool string, duration time.Duration, fields map[string]any)

EmitToolExec emits a tool_exec audit event tagged with the tool name + wall-clock duration. Routed through EmitFromContext so workflow-correlation fields auto-tag every tool execution when the inbound request was orchestrated. The Fields map may carry arg-shape metadata (e.g. arg sizes, types) — raw arg values are deliberately not emitted here; that question is FWS-8's payload-stripping concern, not FWS-3's. See issue #87 / FWS-3.

func (*AuditLogger) SetOpsLogger

func (a *AuditLogger) SetOpsLogger(l Logger)

SetOpsLogger wires a structured logger into the audit pipeline for reporting sink failures (one log per (sink, error-class)). nil disables ops logging; in that mode sink errors are silently swallowed — appropriate for tests and for the channel CLI subcommand where there's no logger in scope.

func (*AuditLogger) Sinks

func (a *AuditLogger) Sinks() []Sink

Sinks returns a snapshot of currently registered sinks. Used by the periodic audit_export_status emitter to read per-sink stats.

func (*AuditLogger) WithEntity

func (a *AuditLogger) WithEntity(entityType, entityID string) *AuditLogger

WithEntity installs the deployment-time entity stamp on the AuditLogger. entityType matches the guardrails library's EntityType constants ("agent" / "workflow" / "assistant"); today Forge only runs agents, so the runner hardcodes "agent". Empty arguments disable the stamp for that field. Called once at runner startup after resolving FORGE_AGENT_ID / cfg.AgentID. Returns the receiver for fluent construction.

Precedence at emit time (highest first):

  1. Explicit EntityID/EntityType set on the AuditEvent.
  2. The static stamp installed here.

No per-request context layer: entity identity is fixed at process startup. If a deployment needs per-request entity routing, that's the tenancy layer's job (OrgID/WorkspaceID) — agent identity is the process, by definition.

See issue #164.

func (*AuditLogger) WithTenancy

func (a *AuditLogger) WithTenancy(orgID, workspaceID string) *AuditLogger

WithTenancy installs the deployment-time tenancy stamp on the AuditLogger. Both arguments are optional — passing "" disables the stamp for that field. Called once at runner startup after resolving FORGE_ORG_ID / FORGE_WORKSPACE_ID. Returns the receiver for fluent construction.

Precedence at emit time (highest first):

  1. Explicit OrgID/WorkspaceID set on the AuditEvent.
  2. TenancyContext from the request context (per-request override header X-Forge-Org-ID / X-Forge-Workspace-ID).
  3. The static stamp installed here.

Setting tenancy on an already-running AuditLogger is allowed but not the common path; hot-reload is the typical caller.

type AuditPayloadCapture

type AuditPayloadCapture struct {
	// LLMMessages controls whether each `llm_call` event carries the
	// list of inbound chat messages (role + content) the agent sent
	// to the model. Off by default.
	LLMMessages bool
	// LLMResponse controls whether `llm_call` carries the model's
	// completion text. Off by default.
	LLMResponse bool
	// ToolArgs controls whether `tool_exec` carries the raw input
	// the agent passed to the tool. Off by default.
	ToolArgs bool
	// ToolResult controls whether `tool_exec` carries the raw
	// output the tool returned. Off by default.
	ToolResult bool

	// CapLLMMessagesBytes is the max bytes serialized for the
	// captured chat messages array. 0 = use the package default
	// (DefaultPayloadCaptureCapBytes).
	CapLLMMessagesBytes int
	// CapLLMResponseBytes — same shape, for completion text.
	CapLLMResponseBytes int
	// CapToolArgsBytes — same shape, for tool input.
	CapToolArgsBytes int
	// CapToolResultBytes — same shape, for tool output.
	CapToolResultBytes int
}

AuditPayloadCapture controls whether the audit pipeline emits raw LLM prompt / completion text and raw tool args / results in audit events. Every field defaults to false; the default audit posture is "metadata only" (size + type + token counts + duration). This matches the security commitment baked into the audit emission sites today and codified by FWS-8 (issue #91).

Customers who need raw payloads in audit (debugging, replay, supervised-learning corpora) opt in field by field, NEVER globally. The cap fields bound per-event byte size so a 1MB prompt doesn't turn one audit event into a memory-hostile record — the captured substring is the first CapXxxBytes bytes followed by a truncation marker `…[truncated:N]`.

Capture flags + caps are read by the runner's hook-registered audit emitters (registerAuditHooks). The Sink layer is unaware of capture settings — it just emits what the AuditEvent says.

THIS IS A SECURITY-RELEVANT CONFIGURATION. Operators who enable any capture flag must ensure the audit transport (the FWS-7 sink or the stderr safety net) lands in a store that respects the captured payloads' sensitivity (PII, secrets that may end up in prompts, etc.). The Forge codebase does not redact; what flows from the LLM call site flows verbatim into the event.

func (AuditPayloadCapture) AnyEnabled

func (c AuditPayloadCapture) AnyEnabled() bool

AnyEnabled reports whether at least one capture flag is on. The runner skips the hook overhead entirely when nothing is enabled.

type CancellationReason

type CancellationReason string

CancellationReason classifies why an in-flight A2A invocation was cancelled. Sourced from the tasks/cancel JSON-RPC params and carried onto the invocation_cancelled audit event so a downstream consumer (cost aggregator, workflow UI, SIEM) can distinguish "the operator hit stop" from "the orchestrator hit a cost ceiling." See issue #88 / FWS-4.

New reasons are additive — audit consumers that don't recognize a reason string should pass it through, not reject the event.

const (
	// CancelReasonWorkflowFailure is set by the orchestrator when a
	// sibling step in a parallel stage failed under fail_workflow
	// semantics and this in-flight agent should abandon its work.
	CancelReasonWorkflowFailure CancellationReason = "workflow_failure"

	// CancelReasonCostLimitExceeded is set by the orchestrator when
	// the workflow's cumulative cost ceiling (from the FWS-3 token
	// totals in A2A response headers) was hit and the platform is
	// cutting off further LLM spend.
	CancelReasonCostLimitExceeded CancellationReason = "cost_limit_exceeded"

	// CancelReasonTimeout is set by the orchestrator (or by Forge's
	// own task deadline) when the wall-clock budget for the
	// invocation has been exhausted.
	CancelReasonTimeout CancellationReason = "timeout"

	// CancelReasonExternalSignal is the default — operator-initiated
	// cancel, debugging stop, anything else not covered by the more
	// specific reasons.
	CancelReasonExternalSignal CancellationReason = "external_signal"
)

func CancellationReasonFromCause

func CancellationReasonFromCause(ctx context.Context) CancellationReason

CancellationReasonFromCause unwraps the reason stamped on ctx by the tasks/cancel path. Call this after observing ctx.Err() in the executeTask goroutine. Returns CancelReasonExternalSignal when ctx was cancelled without a typed reason (e.g. parent ctx deadline, graceful shutdown signal) — those are still cancellations but the emitting handler didn't classify them.

func (CancellationReason) IsValid

func (r CancellationReason) IsValid() bool

IsValid reports whether r is one of the documented reason values. Used at the tasks/cancel boundary to validate operator input; the runtime itself happily forwards whatever string was supplied — the validation is a UX nicety, not a security boundary.

type CancellationRegistry

type CancellationRegistry struct {
	// contains filtered or unexported fields
}

CancellationRegistry tracks in-flight A2A invocations so the tasks/cancel handler can signal them. One registry per Runner; one entry per active invocation, keyed by task ID.

The registry is the bridge between the JSON-RPC handler (which sees the cancel request) and the long-running executeTask goroutine (which holds the context.CancelCauseFunc). The handler looks up the task ID, invokes the stored cancel function with a typed reason, and the goroutine's ctx propagates the cancellation through the LLM client, tool execution, and audit emission.

func NewCancellationRegistry

func NewCancellationRegistry() *CancellationRegistry

NewCancellationRegistry returns a fresh empty registry.

func (*CancellationRegistry) Cancel

func (r *CancellationRegistry) Cancel(taskID string, reason CancellationReason) bool

Cancel signals the in-flight invocation for taskID with a typed reason. Returns true when an entry was found and its cancel function invoked, false when no invocation is registered (already completed, never started, or already cancelled and unregistered). The handler maps false → a no-op response so cancel-after-complete is idempotent rather than an error.

Reason validation is the caller's job; Cancel forwards whatever it gets so internal cancellations (graceful shutdown, parent deadline translation) can supply their own reason without going through the JSON-RPC validator.

func (*CancellationRegistry) Len

func (r *CancellationRegistry) Len() int

Len returns the number of in-flight registrations. Exposed for tests and operational observability — there is no per-task lookup API by design (the handler only needs Cancel; the executeTask goroutine reads its own reason via context.Cause on ctx).

func (*CancellationRegistry) Register

func (r *CancellationRegistry) Register(taskID string, cancel context.CancelCauseFunc) (release func())

Register associates a CancelCauseFunc with a task ID. Returns a release closure the caller must defer — release pops the entry from the registry so it doesn't leak after the invocation finishes (success, failure, or cancellation).

If a registration already exists for taskID (concurrent retries on the same ID, or buggy callers), Register overwrites it. The returned release uses pointer identity to pop only its own entry, so a stale release from the previous owner is a no-op.

type Compactor

type Compactor struct {
	// contains filtered or unexported fields
}

Compactor manages memory compaction by summarizing old messages and optionally flushing to disk.

Each Memory instance is single-threaded per task execution (the agent loop is sequential), so holding mem.mu during the LLM summarization call is acceptable — no concurrent access occurs.

func NewCompactor

func NewCompactor(cfg CompactorConfig) *Compactor

NewCompactor creates a Compactor from the given config.

func (*Compactor) MaybeCompact

func (c *Compactor) MaybeCompact(taskID string, mem *Memory) (bool, error)

MaybeCompact checks whether the memory exceeds the trigger threshold and, if so, compacts the oldest 50% of messages into a summary. Returns true if compaction occurred.

The first user message (the original task request) is always preserved so the LLM retains the objective across compaction cycles.

The method holds mem.mu for its entire duration including any LLM call. This is safe because each Memory is used by a single sequential agent loop.

func (*Compactor) SetMemoryFlusher

func (c *Compactor) SetMemoryFlusher(f MemoryFlusher)

SetMemoryFlusher sets the long-term memory flusher. This allows wiring the flusher after construction (e.g., when the memory manager depends on the same embedder resolution that happens after the compactor is created).

type CompactorConfig

type CompactorConfig struct {
	// Client is the LLM client for abstractive summarization. If nil,
	// only extractive (bullet-point) summarization is used.
	Client llm.Client
	// Store persists sessions to disk after compaction. If nil, compaction
	// still reduces in-memory messages but nothing is flushed.
	Store *MemoryStore
	// Logger for compaction events.
	Logger Logger
	// CharBudget is the total character budget. Compaction triggers when
	// totalChars exceeds CharBudget * TriggerRatio. Default: 200,000.
	CharBudget int
	// TriggerRatio is the fraction of CharBudget at which compaction fires.
	// Default: 0.6.
	TriggerRatio float64
	// MemoryFlusher flushes key observations to long-term memory before
	// compaction discards old messages. Optional.
	MemoryFlusher MemoryFlusher
}

CompactorConfig configures a Compactor.

type FallbackModelConfig

type FallbackModelConfig struct {
	Provider string
	Client   llm.ClientConfig
}

FallbackModelConfig holds a resolved fallback provider's configuration.

type GuardrailChecker

type GuardrailChecker interface {
	// CheckInbound validates an inbound (user) message — InputGate.
	CheckInbound(ctx context.Context, msg *a2a.Message) error

	// CheckOutbound validates an outbound (agent) message —
	// OutputGate. Implementations should prefer redacting sensitive
	// content over blocking.
	CheckOutbound(ctx context.Context, msg *a2a.Message) error

	// CheckToolCall validates the arguments the agent is about to
	// pass to a tool — ToolCallGate. Called from the BeforeToolExec
	// hook. Returns the (possibly redacted) args string and any
	// blocking error. Empty args short-circuit to (args, nil).
	CheckToolCall(ctx context.Context, toolName, args string) (string, error)

	// CheckToolOutput scans tool output text — OutputGate with tool
	// metadata so the emitted guardrail_check carries `tool` for
	// SIEM grouping. Returns the (possibly redacted) text.
	CheckToolOutput(ctx context.Context, toolName, text string) (string, error)

	// CheckContext validates retrieved context (RAG chunks, memory
	// recall, dynamic system-prompt content) before it is injected
	// into the LLM prompt — ContextGate. Returns the (possibly
	// redacted) content. Empty content short-circuits.
	//
	// The current Forge call site is the BeforeLLMCall hook, which
	// scans system-role messages assembled by the loop. Future memory
	// / RAG work can call this directly from the recall path when a
	// dedicated context-injection seam exists.
	CheckContext(ctx context.Context, content string) (string, error)

	// CheckStream validates a single chunk emitted by a streaming
	// LLM call — StreamGate. Returns the (possibly redacted) chunk.
	//
	// Forge's current Execute loop does not call provider streaming
	// (ExecuteStream is a buffered wrapper around non-streaming
	// Execute), so this is not auto-wired yet. The method is exposed
	// for callers that consume llm.Client.ChatStream directly and
	// for future loop work that adds a real per-chunk seam.
	CheckStream(ctx context.Context, chunk string) (string, error)
}

GuardrailChecker validates messages, tool calls, retrieved context, and tool / LLM output against guardrail policies. Implementations may use file-based config, database-backed config, or no-op passthrough.

Method names mirror the five gates the underlying guardrails library distinguishes (input / context / tool_call / output / stream) rather than the older inbound/outbound nomenclature. See issue #159.

All Check methods accept a context so implementations can route audit emissions through AuditLogger.EmitFromContext and inherit correlation_id, task_id, sequence number, tenancy, and workflow tags from the request scope.

type Hook

type Hook func(ctx context.Context, hctx *HookContext) error

Hook is a function invoked at a specific point in the agent loop.

type HookContext

type HookContext struct {
	Messages      []llm.ChatMessage
	Response      *llm.ChatResponse
	ToolName      string
	ToolInput     string
	ToolOutput    string
	Error         error
	TaskID        string
	CorrelationID string

	// LLMCallDuration is the wall-clock time spent in the provider
	// client.Chat call. Populated for AfterLLMCall hooks.
	LLMCallDuration time.Duration
	// Provider / Model identify the LLM provider + model used for the
	// call. Populated for AfterLLMCall hooks so audit + A2A-header
	// emitters can stamp attribution without re-walking config.
	Provider string
	Model    string
	// ToolExecDuration is the wall-clock time spent executing the tool.
	// Populated for AfterToolExec hooks.
	ToolExecDuration time.Duration
}

HookContext carries data available to hooks at each hook point.

LLMCallDuration / ToolExecDuration / Provider / Model are populated at the call site (loop.go) before the After* hook fires, so audit emitters can tag llm_call and tool_exec events with wall-clock timing and provider attribution. See issue #87 / FWS-3.

type HookPoint

type HookPoint int

HookPoint identifies when a hook fires in the agent loop.

const (
	BeforeLLMCall HookPoint = iota
	AfterLLMCall
	BeforeToolExec
	AfterToolExec
	OnError
)

type HookRegistry

type HookRegistry struct {
	// contains filtered or unexported fields
}

HookRegistry manages registered hooks for each hook point.

func NewHookRegistry

func NewHookRegistry() *HookRegistry

NewHookRegistry creates an empty HookRegistry.

func (*HookRegistry) Fire

func (r *HookRegistry) Fire(ctx context.Context, point HookPoint, hctx *HookContext) error

Fire invokes all hooks registered for the given point in order. If any hook returns an error, execution stops and the error is returned.

func (*HookRegistry) Register

func (r *HookRegistry) Register(point HookPoint, h Hook)

Register adds a hook for the given point. Hooks fire in registration order.

type JSONLogger

type JSONLogger struct {
	// contains filtered or unexported fields
}

JSONLogger writes structured JSON log entries to an io.Writer.

func NewJSONLogger

func NewJSONLogger(w io.Writer, verbose bool) *JSONLogger

NewJSONLogger creates a JSONLogger writing to w. Debug entries are only emitted when verbose is true.

func (*JSONLogger) Debug

func (l *JSONLogger) Debug(msg string, fields map[string]any)

func (*JSONLogger) Error

func (l *JSONLogger) Error(msg string, fields map[string]any)

func (*JSONLogger) Info

func (l *JSONLogger) Info(msg string, fields map[string]any)

func (*JSONLogger) Warn

func (l *JSONLogger) Warn(msg string, fields map[string]any)

type LLMCallAuditArgs

type LLMCallAuditArgs struct {
	Model     string
	Provider  string
	RequestID string
	Usage     LLMUsage
	Duration  time.Duration
	// Cancelled flips the emitted event from llm_call to llm_call_cancelled.
	// Used for streaming calls aborted mid-flight; partial usage counts are
	// still carried.
	Cancelled bool
	// Fields carries optional extra metadata to fold into the emitted
	// event's `fields` map. Populated by the runner's hook layer when
	// AuditPayloadCapture has any flag enabled (issue #91 / FWS-8):
	// captured prompt_messages, completion_text, etc. Nil for the
	// default metadata-only audit posture.
	Fields map[string]any
}

LLMCallAuditArgs is the shared input to AuditLogger.EmitLLMCall. The LLM call site captures these fields once at provider-call completion and the audit logger fans them out to the llm_call NDJSON event. The OTel tracing work (FORGE_OTEL_TRACING.md) will hook into this same capture point to populate gen_ai.usage.input_tokens / gen_ai.usage.output_tokens span attributes without re-doing the per-provider extraction. See issue #87 / FWS-3.

type LLMExecutor

type LLMExecutor struct {
	// contains filtered or unexported fields
}

LLMExecutor implements AgentExecutor using an LLM client with tool calling.

func NewLLMExecutor

func NewLLMExecutor(cfg LLMExecutorConfig) *LLMExecutor

NewLLMExecutor creates a new LLMExecutor with the given configuration.

func (*LLMExecutor) Close

func (e *LLMExecutor) Close() error

Close is a no-op for LLMExecutor.

func (*LLMExecutor) Execute

func (e *LLMExecutor) Execute(ctx context.Context, task *a2a.Task, msg *a2a.Message) (outMsg *a2a.Message, outErr error)

Execute processes a message through the LLM agent loop.

func (*LLMExecutor) ExecuteStream

func (e *LLMExecutor) ExecuteStream(ctx context.Context, task *a2a.Task, msg *a2a.Message) (<-chan *a2a.Message, error)

ExecuteStream runs the tool-calling loop non-streaming, then emits the final response as a single message on the channel. True word-by-word streaming is v2.

type LLMExecutorConfig

type LLMExecutorConfig struct {
	Client         llm.Client
	Tools          ToolExecutor
	Hooks          *HookRegistry
	SystemPrompt   string
	MaxIterations  int
	Compactor      *Compactor
	Store          *MemoryStore
	Logger         Logger
	ModelName      string        // model name for context-aware budgeting
	Provider       string        // provider name (anthropic, openai, ollama, custom) — for audit attribution
	CharBudget     int           // explicit char budget override (0 = auto from model)
	FilesDir       string        // directory for file_create output (default: $TMPDIR/forge-files)
	SessionMaxAge  time.Duration // max idle time before session recovery is skipped (0 = 30m default)
	WorkflowPhases []string      // workflow phases from skills (edit, finalize, query)
	// TracingConfig is the same observability.TracingConfig the cli
	// runner resolves and passes to NewTracerProvider. The executor
	// reads CaptureContent + Redact to decide whether to stamp
	// prompt / completion / tool I/O content on Phase 3 spans
	// (issue #130). Zero value disables content capture.
	TracingConfig observability.TracingConfig
}

LLMExecutorConfig configures the LLM executor.

type LLMUsage

type LLMUsage struct {
	InputTokens  int
	OutputTokens int
	TotalTokens  int
}

LLMUsage carries the normalized token counts an LLM call site captures from provider response metadata. Mirrors llm.UsageInfo but kept in the runtime package so the audit layer has no llm-package dependency. The audit emitter sets TokensUnavailable=true on the event when both Input and Output are zero — signal to billing consumers that the provider did not report usage rather than "the call genuinely consumed zero tokens."

type LLMUsageAccumulator

type LLMUsageAccumulator struct {
	// contains filtered or unexported fields
}

LLMUsageAccumulator aggregates per-invocation LLM usage so the A2A response handler can populate X-Forge-Tokens-In / X-Forge-Tokens-Out / X-Forge-Duration-Ms / X-Forge-Model / X-Forge-Provider headers.

One accumulator is created per A2A invocation by the runner and stashed in context.Context. Every AfterLLMCall hook calls AddLLMCall to fold the current call's counts into the running totals. At response time the runner reads Snapshot() and stamps the headers.

Headers are the orchestration channel for real-time cost enforcement during parallel workflow execution. They populate regardless of whether OTel tracing is enabled — they're the orchestration channel, not the observability channel. See issue #87 / FWS-3.

func LLMUsageAccumulatorFromContext

func LLMUsageAccumulatorFromContext(ctx context.Context) *LLMUsageAccumulator

LLMUsageAccumulatorFromContext returns the per-invocation accumulator from ctx, or nil when no accumulator was attached (e.g. internal cron-fire paths that don't need response headers).

func NewLLMUsageAccumulator

func NewLLMUsageAccumulator() *LLMUsageAccumulator

NewLLMUsageAccumulator returns a fresh accumulator with its invocation clock started at the time of the call.

func (*LLMUsageAccumulator) AddLLMCall

func (a *LLMUsageAccumulator) AddLLMCall(model, provider string, usage LLMUsage, duration time.Duration)

AddLLMCall folds one LLM call's usage + duration into the running totals. The most-recently-added call's model + provider become the "primary" reported in the X-Forge-Model / X-Forge-Provider headers, matching the issue's spec: "the primary model used (most recent if multiple)".

func (*LLMUsageAccumulator) Snapshot

func (a *LLMUsageAccumulator) Snapshot() LLMUsageSnapshot

Snapshot returns the current totals. Safe to call from a goroutine different from AddLLMCall callers.

type LLMUsageSnapshot

type LLMUsageSnapshot struct {
	InputTokens        int
	OutputTokens       int
	LLMTimeTotal       time.Duration // sum of per-LLM-call durations
	InvocationDuration time.Duration // wall-clock since accumulator creation
	PrimaryModel       string
	PrimaryProvider    string
	LLMCallCount       int
	TokensUnavailable  bool
}

LLMUsageSnapshot is an immutable readout of the accumulator's totals at a single point in time. Returned by Snapshot for use by the A2A response handler.

type Logger

type Logger interface {
	Info(msg string, fields map[string]any)
	Warn(msg string, fields map[string]any)
	Error(msg string, fields map[string]any)
	Debug(msg string, fields map[string]any)
}

Logger defines the structured logging interface for the runtime.

type Memory

type Memory struct {
	// contains filtered or unexported fields
}

Memory manages per-task conversation history with token budget tracking.

func NewMemory

func NewMemory(systemPrompt string, maxChars int, model string) *Memory

NewMemory creates a Memory with the given system prompt and character budget. If maxChars is 0, the budget is computed from the model name using ContextBudgetForModel. If both maxChars and model are zero/empty, a default of 512K chars (~128K tokens) is used. The budget must comfortably exceed the per-message truncation cap so that a single tool result plus its surrounding messages fit without triggering aggressive trimming.

func (*Memory) Append

func (m *Memory) Append(msg llm.ChatMessage)

Append adds a message to the conversation history and trims if over budget. Individual messages exceeding maxMessageChars are truncated as a safety net.

func (*Memory) LoadFromStore

func (m *Memory) LoadFromStore(data *SessionData)

LoadFromStore restores memory state from a persisted SessionData. It runs the loaded messages through sanitizeMessages, which strips the two known kinds of corruption that cause strict providers to reject the recovered conversation:

  1. Orphaned tool_calls — assistant messages whose tool_calls have no matching tool result (Responses API: "No tool output found for function call").
  2. Empty assistant turns — assistant messages with both empty content AND no tool_calls (issue #131). The OpenAI chat-completions schema considers that shape invalid; Moonshot, hosted OpenRouter, and OpenAI strict mode return HTTP 400 if a recovered conversation contains one. Such turns appear when the provider hits `finish_reason: length` and the in-loop empty- response recovery fires — pre-#131 builds persisted the empty turn alongside the recovered real response. Stripping on load rescues sessions written by those builds without a migration.

func (*Memory) Messages

func (m *Memory) Messages() []llm.ChatMessage

Messages returns the full message list with the system prompt prepended. If an existing summary is present (from compaction), it is appended to the system prompt so the LLM has prior context.

func (*Memory) Reset

func (m *Memory) Reset()

Reset clears the conversation history (keeps the system prompt).

type MemoryFlusher

type MemoryFlusher interface {
	AppendDailyLog(ctx context.Context, observation string) error
}

MemoryFlusher is the interface for flushing observations to long-term memory. Implemented by memory.Manager.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore provides file-backed session persistence. Each session is stored as a JSON file in the configured directory.

func NewMemoryStore

func NewMemoryStore(dir string) (*MemoryStore, error)

NewMemoryStore creates a MemoryStore backed by the given directory. The directory is created if it does not exist.

func (*MemoryStore) Cleanup

func (s *MemoryStore) Cleanup(maxAge time.Duration) (int, error)

Cleanup removes sessions older than maxAge based on their UpdatedAt timestamp. Returns the number of sessions deleted.

func (*MemoryStore) Delete

func (s *MemoryStore) Delete(taskID string) error

Delete removes a session file from disk.

func (*MemoryStore) List

func (s *MemoryStore) List() ([]string, error)

List returns all session task IDs stored on disk.

func (*MemoryStore) Load

func (s *MemoryStore) Load(taskID string) (*SessionData, error)

Load reads a SessionData from disk. Returns (nil, nil) if the session file does not exist.

func (*MemoryStore) Save

func (s *MemoryStore) Save(data *SessionData) error

Save persists a SessionData to disk using atomic write (temp+fsync+rename). On the first write for a task, CreatedAt is set to now. On subsequent writes, the original CreatedAt is preserved from the existing file.

type ModelConfig

type ModelConfig struct {
	Provider  string
	Client    llm.ClientConfig
	Fallbacks []FallbackModelConfig
}

ModelConfig holds the resolved model provider and configuration.

func ResolveModelConfig

func ResolveModelConfig(cfg *types.ForgeConfig, envVars map[string]string, providerOverride string) *ModelConfig

ResolveModelConfig resolves the LLM provider and configuration from multiple sources with the following priority (highest wins):

  1. CLI --provider flag (providerOverride)
  2. Environment variables: FORGE_MODEL_PROVIDER, OPENAI_API_KEY, ANTHROPIC_API_KEY, LLM_API_KEY
  3. forge.yaml model section

Returns nil if no provider could be resolved.

type NoopGuardrailChecker

type NoopGuardrailChecker struct{}

NoopGuardrailChecker is a passthrough implementation that performs no checks. Used as a fallback when no guardrail configuration is available.

func (*NoopGuardrailChecker) CheckContext

func (n *NoopGuardrailChecker) CheckContext(_ context.Context, content string) (string, error)

func (*NoopGuardrailChecker) CheckInbound

func (n *NoopGuardrailChecker) CheckInbound(_ context.Context, _ *a2a.Message) error

func (*NoopGuardrailChecker) CheckOutbound

func (n *NoopGuardrailChecker) CheckOutbound(_ context.Context, _ *a2a.Message) error

func (*NoopGuardrailChecker) CheckStream

func (n *NoopGuardrailChecker) CheckStream(_ context.Context, chunk string) (string, error)

func (*NoopGuardrailChecker) CheckToolCall

func (n *NoopGuardrailChecker) CheckToolCall(_ context.Context, _, args string) (string, error)

func (*NoopGuardrailChecker) CheckToolOutput

func (n *NoopGuardrailChecker) CheckToolOutput(_ context.Context, _ string, text string) (string, error)

type ProgressEmitter

type ProgressEmitter func(event ProgressEvent)

ProgressEmitter is a callback that emits progress events to the client.

func ProgressEmitterFromContext

func ProgressEmitterFromContext(ctx context.Context) ProgressEmitter

ProgressEmitterFromContext retrieves the ProgressEmitter from the context, or nil.

type ProgressEvent

type ProgressEvent struct {
	Phase   string // "tool_start", "tool_end"
	Tool    string
	Message string
}

ProgressEvent describes a progress update during task execution.

type SequenceCounter

type SequenceCounter = atomic.Int64

SequenceCounter is the per-invocation atomic counter that drives AuditEvent.Sequence. One counter per A2A invocation; stuffed into the request context by the A2A handler at request entry, read by EmitFromContext (and any emit-from-context helper) to stamp the next sequence number.

Type alias for *atomic.Int64 so callers can construct one with `new(atomic.Int64)` and so the package stays small.

func SequenceCounterFromContext

func SequenceCounterFromContext(ctx context.Context) *SequenceCounter

SequenceCounterFromContext returns the per-invocation counter, or nil if none was set. The audit emit path uses nil-vs-non-nil to decide whether to stamp a Sequence on outbound events.

type SessionData

type SessionData struct {
	TaskID    string            `json:"task_id"`
	Messages  []llm.ChatMessage `json:"messages"`
	Summary   string            `json:"summary,omitempty"`
	CreatedAt time.Time         `json:"created_at"`
	UpdatedAt time.Time         `json:"updated_at"`
}

SessionData holds the persisted state for a single task's conversation.

type Sink

type Sink interface {
	// Write delivers a single event. The event is already marshaled
	// NDJSON (one line, trailing newline included). Returns nil even
	// on transient failure; sinks are responsible for their own
	// retry/buffering policy. A non-nil error indicates a permanent
	// sink failure that should be logged once.
	Write(ctx context.Context, eventBytes []byte) error

	// Close flushes any buffered events and releases resources. Called
	// during agent shutdown. Implementations must honor any deadline
	// on the passed context and never block beyond it.
	Close(ctx context.Context) error

	// Name returns a stable identifier ("stderr" / "unix-socket" /
	// "localhost-http") used in self-reporting and operator logs.
	Name() string

	// Stats returns counters describing sink health since process
	// start. Keys are stable strings (writes_ok, drops_timeout,
	// drops_dial, connected); values are monotonic counts or 0/1
	// flags. Used by the periodic audit_export_status emitter and
	// the /health endpoint.
	Stats() map[string]int64
}

Sink consumes serialized audit event bytes. Implementations must be safe for concurrent use. Sinks should never block the emitter under back-pressure for longer than their configured timeout; on timeout the sink drops the event and increments its drop counter, never returns an error to the caller.

The audit pipeline composes one or more sinks (stderr safety-net + optional Unix socket / HTTP sink for export). Each sink is independent; a failure on one does not stop emission on the others.

See issue #95 / FWS-7.

func NewHTTPSink

func NewHTTPSink(endpoint string, writeTimeout time.Duration) Sink

NewHTTPSink constructs a localhost HTTP sink. Returns nil if endpoint is empty. The http.Client is built with a per-request timeout matching the write timeout; the transport defaults are fine — no keep-alive tuning needed because we expect one POST per emit and localhost RTT is sub-millisecond.

func NewSocketSink

func NewSocketSink(path string, writeTimeout, dialTimeout time.Duration) Sink

NewSocketSink constructs a Unix Domain Socket sink. Zero values for writeTimeout / dialTimeout fall back to defaults. The socket is NOT dialed eagerly — the first Write triggers the connection attempt.

Returns nil if path is empty (caller should not register an empty sink). Path validation (length, parent dir exists) is deliberately deferred to dial time: a sidecar that creates its socket lazily shouldn't cause the agent to fail at startup.

type SkillGuardrailEngine

type SkillGuardrailEngine struct {
	// contains filtered or unexported fields
}

SkillGuardrailEngine enforces skill-declared deny patterns on command inputs, tool outputs, and user prompts. It complements the global GuardrailEngine with domain-specific rules authored by skill developers.

func NewSkillGuardrailEngine

func NewSkillGuardrailEngine(rules *agentspec.SkillGuardrailRules, enforce bool, logger Logger) *SkillGuardrailEngine

NewSkillGuardrailEngine creates a SkillGuardrailEngine from aggregated skill rules. Invalid regex patterns are skipped with a warning.

func (*SkillGuardrailEngine) CheckCommandInput

func (s *SkillGuardrailEngine) CheckCommandInput(toolName, toolInput string) error

CheckCommandInput validates a tool call before execution. It only fires for cli_execute tool calls. Returns an error if the command matches a deny pattern.

func (*SkillGuardrailEngine) CheckCommandOutput

func (s *SkillGuardrailEngine) CheckCommandOutput(toolName, toolOutput string) (string, error)

CheckCommandOutput validates tool output after execution. It only fires for cli_execute tool calls. Returns the (possibly redacted) output and an error if the output matches a "block" pattern.

func (*SkillGuardrailEngine) CheckLLMResponse

func (s *SkillGuardrailEngine) CheckLLMResponse(text string) (string, bool)

CheckLLMResponse validates the LLM's response text against deny_responses patterns. When a match is found, the response is replaced with the skill-defined redirect message to prevent binary/tool enumeration leaks. Returns the (possibly replaced) text and whether a replacement occurred.

func (*SkillGuardrailEngine) CheckUserInput

func (s *SkillGuardrailEngine) CheckUserInput(text string) error

CheckUserInput validates a user message against deny_prompts patterns. Returns an error with the skill-defined redirect message if the prompt matches.

type TenancyContext

type TenancyContext struct {
	OrgID       string
	WorkspaceID string
}

TenancyContext carries the org / workspace identifiers a Forge agent extracts from inbound A2A request headers. Zero value is meaningful — it means "no per-request override; fall back to whatever the AuditLogger's static stamp says."

func TenancyContextFromContext

func TenancyContextFromContext(ctx context.Context) TenancyContext

TenancyContextFromContext retrieves the TenancyContext from the context. Returns the zero value (IsZero == true) when none was set, which is the signal EmitFromContext uses to fall back to the AuditLogger's static tenancy stamp.

func TenancyContextFromHTTPHeaders

func TenancyContextFromHTTPHeaders(h http.Header) TenancyContext

TenancyContextFromHTTPHeaders extracts X-Forge-Org-ID and X-Forge-Workspace-ID from an inbound HTTP request's headers. Missing headers map to empty fields; the returned TenancyContext is IsZero when neither is set. Mirrors WorkflowContextFromHTTPHeaders — same pattern, same precedence rules at the call site.

func (TenancyContext) ApplyToHTTPHeaders

func (t TenancyContext) ApplyToHTTPHeaders(h http.Header)

ApplyToHTTPHeaders writes any non-empty TenancyContext fields onto outbound request headers. Used by tools that explicitly propagate tenancy to downstream A2A calls in an agent-to-agent flow. Auto-propagation is NOT built into the egress proxy — same rationale as WorkflowContext: a tenancy header would leak if the agent called a non-Forge third party. Tools propagate explicitly when they know the target is a tenancy-aware peer.

func (TenancyContext) IsZero

func (t TenancyContext) IsZero() bool

IsZero reports whether the TenancyContext carries no overrides. EmitFromContext checks this before reaching for the AuditLogger's static stamp.

type ToolExecutor

type ToolExecutor interface {
	Execute(ctx context.Context, name string, arguments json.RawMessage) (string, error)
	ToolDefinitions() []llm.ToolDefinition
}

ToolExecutor provides tool execution capabilities to the engine. The tools.Registry satisfies this interface via Go structural typing.

type WorkflowContext

type WorkflowContext struct {
	// WorkflowID identifies the orchestrator-level workflow run.
	WorkflowID string

	// StageID identifies a stage within the workflow (a group of
	// steps that may run in parallel).
	StageID string

	// StepID identifies the specific step within the stage that
	// invoked this agent.
	StepID string

	// InvocationCaller identifies the upstream caller — typically the
	// orchestrator's identity, but for agent-to-agent calls within a
	// workflow it carries the upstream agent's identifier.
	InvocationCaller string
}

WorkflowContext carries the orchestration identifiers a Forge agent extracts from inbound A2A request headers. Zero value is meaningful — it represents "no workflow context" (direct A2A invocation).

func WorkflowContextFromContext

func WorkflowContextFromContext(ctx context.Context) WorkflowContext

WorkflowContextFromContext retrieves the WorkflowContext from the context. Returns the zero value (IsZero == true) when none was set.

func WorkflowContextFromHTTPHeaders

func WorkflowContextFromHTTPHeaders(h http.Header) WorkflowContext

WorkflowContextFromHTTPHeaders extracts the orchestration identifiers from an inbound HTTP request's headers. Missing headers map to empty fields; the returned WorkflowContext is `IsZero` when none are set.

func (WorkflowContext) ApplyToHTTPHeaders

func (w WorkflowContext) ApplyToHTTPHeaders(h http.Header)

ApplyToHTTPHeaders writes any non-empty WorkflowContext fields onto outbound request headers. Used by tools that explicitly propagate workflow context to downstream A2A calls (the issue's "agent invoking another agent during workflow execution" path).

Auto-propagation is deliberately not built into the egress proxy — the X-Workflow-* headers identify the workflow and would leak if the agent calls a non-workflow third-party API. Tools propagate explicitly when they know the target is a workflow peer.

func (WorkflowContext) IsZero

func (w WorkflowContext) IsZero() bool

IsZero reports whether the WorkflowContext carries no orchestration identifiers. Used by audit and helpers to decide whether to stamp workflow fields (when zero, fields are omitted entirely so the emitted JSON matches the pre-FWS-2 shape).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL