@felan-ai/agent-core
v0.12.0
Published
Portable Felan agent contracts and composition
Readme
@felan-ai/agent-core
Portable Felan agent contracts and the Node.js host runtime.
@felan-ai/agent-core is the portable composition layer between a
Felan host and the pinned Pi packages. It is intended for applications and
portable extensions, not as a complete end-user CLI.
import { HostAgentRuntime } from '@felan-ai/agent-core';
const runtime = new HostAgentRuntime(process.cwd(), {
sessionStorageRoot: '/var/lib/felan/sessions/current',
agentStorageRoot: '/var/lib/felan/agent',
agentDir: '/etc/felan',
pathAccess: 'host',
});
const result = await runtime.exec('node', ['--version']);HostAgentRuntime uses its immutable cwd to resolve relative paths. The
default pathAccess: 'workspace' contains ordinary file operations and process
working directories to that cwd. pathAccess: 'host' permits any path
available to the current user. Use exec(command, args) for literal argument
boundaries and shell(command) only when shell parsing is intentional. File
reads and writes use Uint8Array so binary content is preserved. Reads accept a
maxBytes bound, and writes support exclusive creation for race-safe new files.
listFiles supports recursive, glob-filtered traversal with ignored-directory,
depth, and result-count bounds; host implementations enumerate one directory at
a time rather than materializing an unbounded recursive scan. exec and shell
accept an optional maxOutputBytes cap; capped results set truncated without
turning normal command completion into cancellation.
Shell calls use the host's default shell unless they request the explicit
shellFlavor: 'posix' option. POSIX hosts use /bin/sh; native Windows hosts
validate and use a same-host Git Bash installation discovered through
FELAN_POSIX_SHELL, PATH, or standard Git-for-Windows locations. The
HostAgentRuntimeOptions.posixShell override is available to embedding hosts.
The default Windows cmd.exe behavior is unchanged, and WSL is not selected
automatically because its process and path namespace is separate from the host.
Every AgentRuntime exposes scoped storage through storage(scope). The
default storage() handle is identical to storage('session') and belongs to
one root session plus all of its subagents. Session storage is readable through
ordinary runtime reads and shares a filesystem namespace with shell, allowing
tools to return absolute output paths that regular agents can inspect.
storage('agent') holds longer-retention extension state owned by the runtime
host. Host consumers provide exact sessionStorageRoot and agentStorageRoot
paths; each storage handle preserves binary content, rejects lexical and
symlink escapes, and cannot remove its root. Workspace path access excludes
agent storage, while host path access lets ordinary operations inspect both
storage scopes. Storage handles also support appendFile for host log files.
Every runtime has a structured logger; hosts inject the destination. Runtimes
may also expose an optional classifier capability. Its single
classify(state, questions, signal) operation supports discriminated choice,
bool, and score questions and answers. canEvaluate is an optional admission
helper for bounded evidence packing, not a second inference operation.
createPiClassifier(modelRuntime, model) delegates requests to Pi's native
classify; hosts select an authenticated catalog model and own credentials.
The bridge validates JSON-object state and answers, packs complete question
sets against the existing byte budgets, and maps native usage metadata. Unknown
catalog pricing is not reported as verified free inference. There is no owned
HTTP client or environment-key discovery in Agent Core.
validateClassifierRequest and validateClassifierAnswers expose boundary
checks to portable consumers, including custom classifier hosts. The explicit
request validator requires JSON-object state and rejects functions, circular
references, nonfinite numbers, accessors, and non-JSON objects. The existing
bridge retains its JSON serialization behavior for programmatic callers.
Reserved property names remain ordinary question IDs.
The public Pi composition surface also exports ImageApi, ImageModel,
AssistantImages, ImagesContext, and ImagesOptions; image feature behavior
belongs in extensions, not AgentRuntime.
In classifier-enabled root sessions, Agent Core exposes the optional
FelanExtensionAPI.turnClassification registry. Extensions register a unique
producer ID and a prepare(input, context, signal) contribution during
initialization, returning questions and optional JSON state or skipping the turn.
Core captures one sanitized input.session snapshot, namespaces question IDs as
producer:question and state as extensions[producer], and makes one logical
classification call. Consumers await result(id, { prompt, imageCount }, context)
in their before_agent_start handler; returned answers use their original keys.
Questions are independent, so conditional branches must state their premises.
Eligibility, thresholds, actions and fallbacks stay feature-owned.
The existing 2-second root budget covers preparation and inference, including custom classifiers. Invalid preparation is isolated; timeout or incomplete answers fail closed. Expansion supersedes an earlier prompt; continuation turns are not reclassified. Existing bridge byte budgets can split transport requests. Registration resets on reload; pending work is isolated per root session and cancelled on lifecycle/manual changes. Without composed root registration, extensions retain static guidance. Later explicit classifier calls remain outside this preflight. See classifier lifecycle.
collectClassifierSessionEvidence handles branch/compaction summaries, bounds
conversation and tool metadata, omits images/raw thinking/tool-result bodies and
redacts recognized credential patterns. Arbitrary secrets cannot be guaranteed
safe. Additional producer state is also validated and redacted before dispatch.
The optional selectionAutomation service exposes temporary ownership and
selection provenance. Workflow owners acquire a release callback; regular effort
selection pauses until ownership releases. updateDefault: false model/thinking
changes remain session-local and are distinguishable from manual choices, even
across asynchronous transitions. Manual choices stay authoritative; stale release
callbacks cannot clear a newer owner. Core owns this mechanism, not feature modes.
onManualThinkingSelection observes explicit effort choices, including reselecting
the current level. Composed sessions detect this at Pi's public setter boundary;
implicit model-switch clamps and automated selections do not produce this signal.
Session composition accepts an optional dynamicThinking capability. When the
runtime has a classifier, it starts eligible effort selection at input and
applies it on before_agent_start,
not queued follow-ups or tool continuations, without persisting the user's
default. Eligible GPT-6 requests on official OpenAI or Codex Responses are
classified only when the Codex extension is loaded;
eligible Anthropic models use Pi's mid-conversation effort support.
Explicit thinking changes take precedence for the remainder of the session.
Absent the option or the classifier, composition does not enable this feature.
With an optional savings reporter, Agent Core reports a heuristic
API-equivalent estimate for any supported model's automatic high → low or
high → medium change when the run settles without tool failures and its
first assistant response includes reasoning-token usage. It estimates 5%
extra reasoning output at high, keeps observed input, cache and visible-output
usage unchanged, and subtracts reported classifier
cost. Missing classifier pricing leaves that overhead unknown; estimates are
not a complete bill. No change, a return to high, other models/levels, missing usage, or a
failed response produces no measurement. The unvalidated cross-model
assumption and quality caveats are in the public benchmark notes.
Host runtimes expose optional persistent process operations for extensions that
need incremental output and stdin. startShell() keeps process ownership in
the runtime adapter and returns a bounded polling handle with write, terminate,
interrupt, and dispose operations. startStdio() uses literal argv and exposes
separate bounded stdout and stderr streams for protocols whose stdout must stay
machine-readable. The optional privateRuntime.ensureDirectory() capability
creates or validates owner-private short-lived coordination directories. The
separate optional terminals capability
allocates a real operating-system PTY with terminal input; adapters without PTY
support omit that capability. The optional readAgentFile()
boundary reads only inside the configured agentDir; ordinary runtime file
operations follow the configured path access mode.
Host mode runs with the current user's filesystem and process permissions. It does not provide OS isolation or a sandbox boundary. Run untrusted workloads in an isolated runtime instead.
The package also composes Pi sessions with inline Felan extensions,
optional explicitly supplied native Pi extension paths, and runtime-backed
coding tools. createAgentCoreSession returns a headless,
inactive session, while createAgentCoreSessionRuntimeFactory provides the
typed seam used with Pi's createAgentSessionRuntime. Applications retain
ownership of model credentials, settings, session storage, stream wrappers,
feature extensions, and presentation listeners. FelanExtensionAPI adds the
selected AgentRuntime, application agent directory, and session-aware model
selection options to Pi's extension API; feature-specific contracts remain in
their owning extension packages. Feature automation can pass
{ updateDefault: false } to setModel or setThinkingLevel to update the
active session without changing the user's default model or thinking
preference. Session-only model switches carry the active thinking level instead
of applying persisted global or per-model defaults. Ordinary selections retain
Pi's default-updating behavior.
Applications may also pass adapter-neutral inlineExtensions directly into
session composition for host-owned integration such as presentation controls;
these remain opt-in and are not discovered from ambient configuration.
Runtime-backed coding tools are installed during composition as hidden fallback
extension tools, so feature extensions can override standard tool names while
explicit application customTools retain final precedence.
Agent Core owns the exact Pi dependency versions used by its consumers. Its
public entry point exposes the Pi model, credential, streaming, session,
resource, skill, tool, and context-inspection symbols needed to compose Felan applications, so
consumers import those symbols from @felan-ai/agent-core without declaring
Pi packages directly.
The composition surface re-exports Pi's createCodemodeExtension and
CodemodeExtensionOptions. Registration alone leaves codemode inactive;
the host must activate it and owns its configuration and lifecycle. Agent Core
does not enable it automatically. Felan's local host supports codemode.mode
values off, on, and only, defaults to off, and disables the sandbox's
extra model helpers with models: false.
Agent Core 0.12 follows Pi 1.1's public contracts. Extension integrations must
handle aborted on agent_settled events, include durationMs and outputPad
when implementing Pi tool-render callbacks, and provide
ToolLoadout.getPromptGuidelines(). Custom stream functions should return an
AssistantMessageEventStream, typically created by
createAssistantMessageEventStream().
Agent Core also exposes shared xhigh, high, medium, and low model tiers for
extensions that need model-strength selection:
import { selectModelForTier } from '@felan-ai/agent-core';
const models = ctx.scopedModels.length > 0
? ctx.scopedModels.map(({ model }) => model)
: ctx.modelRegistry.getAvailable();
const selection = selectModelForTier('low', models, {
preferredModel: ctx.model,
});Callers provide the models already allowed and authenticated by their host.
Selection prefers candidates from the current provider and model family, then
falls back across the supplied model scope. getModelFamily and
getModelStrength classify the host's live model list with version-independent
family and role names, so Claude Fable, GPT Astra, Opus, Sonnet, Haiku, Sol,
Terra, Luna, Pro, Flash, Max, and similar releases do not require an exact-ID
catalog update. Aggregate
providers including OpenCode, OpenCode Go, OpenRouter, and GitHub Copilot are
classified from each model's identity rather than treated as one family.
Unknown naming schemes default to medium, and hosts can pass a custom
classifyModel function to selectModelForTier. Model tiers do not imply a
thinking level. xhigh is reserved for exceptional model-assisted work such as
complex architecture, design, planning, difficult debugging, and high-stakes
code review; it is not a routine default. FELAN_THINKING_LEVELS separately
defines off, low, medium, high, xhigh, and max; minimal is outside
the Felan-facing scale. Agent Core also re-exports Pi's clampThinkingLevel so extensions can
resolve a requested effort against a host-provided model without duplicating
provider capability rules. Agent Core does not load model-tier configuration or
resolve credentials.
Agent Core owns the runtime-neutral Felan base system prompt. Every composed
session uses this prompt; consumers extend it with appendSystemPrompt and
cannot replace it. During composition, Agent Core also reads at most one
instruction file from the session cwd through AgentRuntime, with AGENTS.md
taking precedence over CLAUDE.md. Missing, unreadable, and blank instruction
files are nonfatal. The selected file is delivered by Pi's context transform as
a path-labelled, hidden user-role context message on each model
request. It follows the system prompt and is not part of the system prompt or
persisted conversation history. getProjectInstructions(resourceLoader) lets
hosts inspect the selected path and content without adding it to Pi's prompt
resources. Inline extensions can contribute model-facing behavior during
initialization:
const extension: FelanExtension = (pi) => {
pi.registerCapability({
id: 'review',
instructions: 'Review changed code and report concrete findings.',
});
};Capability IDs and instructions are validated, duplicate IDs report both extension sources, and contributions retain extension load and registration order across resource reloads. Agent Core renders enabled capabilities as one section after the base prompt. Consumer appends follow that section; Pi then adds explicit skills and the current working directory. The separate context transform supplies cwd project instructions after the leading system prompt.
Tool definitions sent with the model request remain the authoritative tool
inventory. The Felan-owned prompt intentionally does not render Pi's default
promptSnippet or promptGuidelines sections; extensions use named
capabilities for multi-tool workflow guidance.
Applications may pass explicit skills or skillPaths into session
composition. Agent Core exposes only those resources while ambient project,
user, and package skill discovery remains disabled. Ambient system prompt,
append prompt, and context discovery are also disabled; the selected cwd
instruction file is the only built-in project-instruction input.
Extension settings
Extensions declare configuration with defineExtensionConfig and configField.
Defaulted fields retain their defaults through resolution. Use
configField.optionalEnum(['normal', 'fast'], { description: 'Request priority' })
when omission has meaning: the resolved configuration omits the key until an
explicit choice is supplied. Optional enum properties are optional in
InferExtensionConfig, and explicit invalid values, including null, are
rejected. Hosts can display an unset value without saving a synthetic default.
Fields may carry a deprecated message. Deprecated fields remain valid for
configuration and CLI compatibility; Felan's /settings hides them.
Package boundary
Agent Core owns AgentRuntime, HostAgentRuntime, the Felan base prompt, cwd
project instructions, provider-aware model tiers, runtime-backed coding tools,
capabilities, session/resource composition, and the public Pi composition
exports. Hosts own credentials, settings, storage roots, model scope,
presentation, and feature extension selection. Feature-specific behavior such
as tasks, memory scheduling, progressive context, and Prewalk belongs in its
own extension or application.
Host mode is deliberately not a sandbox. Use a runtime with an external
isolation boundary for untrusted workloads. Runtime callers should use literal
argv with exec, bounded byte-based I/O, and the scoped storage APIs rather
than reaching around the adapter.
Development
Source: packages/agent-core in https://github.com/felan-ai/felan.
corepack enable
pnpm install --frozen-lockfile
pnpm --filter @felan-ai/agent-core build
pnpm --filter @felan-ai/agent-core type-check
pnpm --filter @felan-ai/agent-core testAttribution
Agent Core composes the pinned MIT-licensed Pi packages listed in the package manifest. See NOTICE and LICENSE for the complete third-party attribution boundary.
