// @harn-entrypoint-category agent.stdlib
import { agent_emit_event } from "std/agent/state"
import { filter_nil, pick_keys } from "std/collections"
type ResumeTimeoutAction = "resume_with_summary" | "fail" | "resume_with_input"
type ResumeTimeoutSpec = {duration_minutes: int, on_timeout: ResumeTimeoutAction?}
type ResumeConditions = {trigger: dict?, timeout: ResumeTimeoutSpec?, on_event: string?}
type AgentAwaitResumptionRequest = {reason: string, conditions: ResumeConditions?}
fn __agent_option_keys() {
return [
"provider",
"model",
"model_tier",
"temperature",
"top_p",
"top_k",
"max_tokens",
"thinking",
"reasoning_effort",
"reasoning_policy",
"thinking_policy",
"reasoning_scale",
"problem_scale",
"reasoning_task",
"tools",
"subagents",
"subagent_tools",
"max_iterations",
"max_nudges",
"nudge",
"turn_policy",
"tool_format",
"native_tool_fallback",
"structural_experiment",
"context_callback",
"context_filter",
"tool_retries",
"tool_backoff_ms",
]
}
/**
* agent creates an agent spec dict.
*
* @effects: []
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: agent(name, config)
*/
pub fn agent(name, config = nil) {
if type_of(name) != "string" {
throw "agent: name must be a string"
}
let cfg = config ?? {}
if type_of(cfg) != "dict" {
throw "agent: second argument must be a config dict"
}
return cfg + {_type: "agent", name: name}
}
/**
* agent_name reads an agent spec name.
*
* @effects: []
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: agent_name(agent)
*/
pub fn agent_name(agent) {
if type_of(agent) != "dict" {
throw "agent_name: argument must be an agent"
}
return agent?.name
}
/**
* agent_config builds prompt/system/options for an agent spec.
*
* @effects: [agent]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: agent_config(agent, prompt)
*/
pub fn agent_config(agent, prompt) {
if type_of(agent) != "dict" || agent?._type != "agent" {
throw "agent_config: first argument must be an agent (created with agent())"
}
let options = pick_keys(agent, __agent_option_keys(), {drop_nil: true})
return {prompt: prompt, system: agent?.system, options: options}
}
fn __worker_dict(value) {
if type_of(value) == "dict" {
return value
}
return {}
}
fn __worker_list(value) {
if type_of(value) == "list" {
return value
}
return []
}
fn __worker_options(value, label) {
if value == nil {
return {}
}
if type_of(value) == "dict" {
return value
}
throw label + ": options must be a dict or nil"
}
fn __worker_resume_options(options) {
if options?.resume_when == nil {
return options
}
return options + {resume_when: parse_resume_conditions(options.resume_when)}
}
fn __agent_tool_entry_name(entry) {
if type_of(entry) != "dict" {
return ""
}
if entry?.name != nil {
return to_string(entry.name)
}
let func = entry?.function
if type_of(func) == "dict" {
return to_string(func?.name ?? "")
}
return ""
}
fn __agent_tool_registry_has(registry, name) {
let entries = registry?.tools ?? []
if type_of(entries) != "list" {
return false
}
for entry in entries {
if __agent_tool_entry_name(entry) == name {
return true
}
}
return false
}
fn __agent_lifecycle_tool_audit(tool_name, initiator, reason, handle = nil, conditions = nil) {
let session_id = agent_session_current_id()
if session_id == nil || session_id == "" {
return
}
let _ = try {
agent_emit_event(
session_id,
"tool_call_audit",
{
tool_call_id: "",
tool_name: tool_name,
audit: filter_nil(
{
layer: "agent_lifecycle",
status: "invoked",
initiator: initiator,
reason: reason,
handle: handle,
conditions: conditions,
},
),
},
)
}
}
fn __subagent_pause_tool(args) {
let handle = args?.handle
if handle == nil || trim(to_string(handle)) == "" {
throw "subagent_pause: handle is required"
}
let reason = trim(to_string(args?.reason ?? ""))
if reason == "" {
throw "subagent_pause: reason is required"
}
__agent_lifecycle_tool_audit("subagent_pause", "parent", reason, handle)
return suspend_agent(handle, reason, {initiator: "parent"})
}
fn __subagent_resume_tool(args) {
let handle = args?.handle
if handle == nil || trim(to_string(handle)) == "" {
throw "subagent_resume: handle is required"
}
let continue_transcript = args?.continue_transcript ?? true
if type_of(continue_transcript) != "bool" {
throw "subagent_resume: continue_transcript must be a bool"
}
__agent_lifecycle_tool_audit("subagent_resume", "parent", "resume requested", handle)
return resume_agent(handle, args?.input, continue_transcript)
}
fn __agent_await_resumption_tool(_args) {
throw "HARN-SUS-005: agent_await_resumption is handled structurally by agent_loop and cannot be invoked directly"
}
fn __agent_lifecycle_has_subagent_capability(registry, opts) {
if opts?.subagents ?? false {
return true
}
if opts?.subagent_tools ?? false {
return true
}
return __agent_tool_registry_has(registry, "sub_agent_run")
|| __agent_tool_registry_has(registry, "spawn_agent")
}
fn __agent_lifecycle_add_await_tool(registry) {
if __agent_tool_registry_has(registry, "agent_await_resumption") {
return registry
}
return tool_define(
registry,
"agent_await_resumption",
"Suspend this agent and return control to its caller until a parent, operator, or resume trigger wakes it.",
{
handler: __agent_await_resumption_tool,
parameters: {
reason: {type: "string", description: "Why this agent is parking."},
conditions: {
type: "object",
required: false,
description: "Optional ResumeConditions with trigger, timeout, or on_event fields.",
},
},
returns: {type: "object"},
annotations: {agent_lifecycle: true, structural: true, initiator: "self"},
},
)
}
fn __agent_lifecycle_add_subagent_tools(registry) {
var tools = registry
if !__agent_tool_registry_has(tools, "subagent_pause") {
tools = tool_define(
tools,
"subagent_pause",
"Cooperatively pause a running subagent after its current turn settles. Returns a resumable handle.",
{
handler: __subagent_pause_tool,
parameters: {
handle: {type: "string", description: "Worker id or worker handle to pause."},
reason: {type: "string", description: "Why the child should pause."},
},
returns: {type: "object"},
annotations: {agent_lifecycle: true, initiator: "parent"},
},
)
}
if !__agent_tool_registry_has(tools, "subagent_resume") {
tools = tool_define(
tools,
"subagent_resume",
"Resume a suspended subagent with optional input.",
{
handler: __subagent_resume_tool,
parameters: {
handle: {type: "string", description: "Worker id or worker handle to resume."},
input: {type: "any", required: false, description: "Optional input for the resumed turn."},
continue_transcript: {
type: "boolean",
required: false,
description: "Keep prior transcript context when resuming; defaults to true.",
},
},
returns: {type: "object"},
annotations: {agent_lifecycle: true, initiator: "parent"},
},
)
}
return tools
}
/**
* agent_lifecycle_tools adds model-facing suspend/resume lifecycle tools.
*
* Every returned registry includes `agent_await_resumption`. Parent-side
* `subagent_pause` and `subagent_resume` are added only when the caller opts
* into subagent lifecycle control with `{subagents: true}` or
* `{subagent_tools: true}`, or when the supplied registry already exposes a
* `sub_agent_run` / `spawn_agent` tool.
*
* @effects: []
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: agent_lifecycle_tools(registry, options)
*/
pub fn agent_lifecycle_tools(registry = nil, options = nil) {
let opts = __worker_options(options, "agent_lifecycle_tools")
let base = registry ?? tool_registry()
var tools = __agent_lifecycle_add_await_tool(base)
if __agent_lifecycle_has_subagent_capability(base, opts) {
tools = __agent_lifecycle_add_subagent_tools(tools)
}
return tools
}
fn __worker_mode(config) {
if config?.graph != nil {
return "workflow"
}
return "stage"
}
fn __worker_workflow_config(config) {
let options = __worker_resume_options(__worker_dict(config?.options))
return filter_nil(
config
+ {graph: workflow_graph(config.graph), artifacts: __worker_list(config?.artifacts), options: options},
)
}
fn __worker_stage_config(config) {
return filter_nil(
config
+ {
artifacts: __worker_list(config?.artifacts),
options: __worker_resume_options(__worker_dict(config?.options)),
},
)
}
fn __worker_config(config) {
let cfg = __worker_dict(config)
let mode = __worker_mode(cfg)
if mode == "workflow" {
return __worker_workflow_config(cfg)
}
return __worker_stage_config(cfg)
}
/**
* spawn_agent.
*
* @effects: [host]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: spawn_agent(config)
*/
pub fn spawn_agent(config) {
return __host_worker_spawn(__worker_config(config))
}
/**
* parse_resume_conditions.
*
* Validate and normalize a ResumeConditions dict used by self-parking
* agents and `spawn_agent({options: {resume_when: ...}})`. `trigger`
* is validated through the same trigger-spec parser used by
* `trigger_register`; `timeout.duration_minutes` must be positive; and
* `on_event` must be a non-empty EventLog topic.
*
* @effects: [host]
* @allocation: heap
* @errors: [HARN-SUS-002]
* @api_stability: experimental
* @example: parse_resume_conditions(conditions)
*/
pub fn parse_resume_conditions(conditions = nil) -> ResumeConditions? {
return __host_resume_conditions_parse(conditions)
}
/**
* agent_await_resumption.
*
* Build the normalized request used by the model-facing
* `agent_await_resumption` lifecycle tool. `agent_loop` decides whether that
* request becomes a worker suspension, a top-level suspension checkpoint, or
* daemon idle metadata.
*
* @effects: [host]
* @allocation: heap
* @errors: [HARN-SUS-002, HARN-SUS-005]
* @api_stability: experimental
* @example: agent_await_resumption("waiting on review", {timeout: {duration_minutes: 5}})
*/
pub fn agent_await_resumption(reason, conditions = nil) -> AgentAwaitResumptionRequest {
let normalized_reason = trim(to_string(reason ?? ""))
if normalized_reason == "" {
throw "HARN-SUS-005: agent_await_resumption reason is required"
}
return {reason: normalized_reason, conditions: parse_resume_conditions(conditions)}
}
/**
* send_input.
*
* @effects: [host]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: send_input(worker, task)
*/
pub fn send_input(worker, task) {
return __host_worker_send_input(worker, task)
}
/**
* worker_trigger.
*
* @effects: [host]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: worker_trigger(worker, payload)
*/
pub fn worker_trigger(worker, payload) {
return __host_worker_trigger(worker, payload)
}
fn __is_pool_task(value) {
return type_of(value) == "dict" && value?._type == "pool_task"
}
/**
* wait_agent.
*
* @effects: [host]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: wait_agent(worker_or_workers)
*/
pub fn wait_agent(worker_or_workers) {
if __is_pool_task(worker_or_workers) {
return __pool_wait(worker_or_workers)
}
if type_of(worker_or_workers) == "list" {
var results = []
for item in worker_or_workers {
results = results.push(wait_agent(item))
}
return results
}
return __host_worker_wait(worker_or_workers)
}
/**
* close_agent.
*
* @effects: [host]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: close_agent(worker)
*/
pub fn close_agent(worker) {
return __host_worker_close(worker)
}
/**
* suspend_agent.
*
* Cooperatively mark a worker as suspended, persist its resumable snapshot,
* and return the worker summary with `status: "suspended"` plus
* `suspension` metadata.
*
* @effects: [host]
* @allocation: heap
* @errors: [HARN-SUS-001, HARN-SUS-002, HARN-SUS-007, HARN-SUS-008]
* @api_stability: experimental
* @example: suspend_agent(worker, reason)
*/
pub fn suspend_agent(worker, reason = "", options = nil) {
return __host_worker_suspend(worker, reason, options)
}
/**
* resume_agent.
*
* Resume a suspended worker, optionally wiring new input into the resumed
* task, or restore a persisted worker snapshot into the current runtime.
* By default the resumed worker keeps its transcript. Pass
* `continue_transcript = false` to resume from the prior transcript summary
* plus the new input only.
*
* @effects: [host]
* @allocation: heap
* @errors: [HARN-SUS-003, HARN-SUS-004, HARN-SUS-006, HARN-SUS-009, HARN-SUS-010]
* @api_stability: experimental
* @example: resume_agent(worker_or_snapshot, resume_input, continue_transcript)
*/
pub fn resume_agent(worker_or_snapshot, resume_input = nil, continue_transcript = true) {
return __host_worker_resume(
worker_or_snapshot,
{input: resume_input, continue_transcript: continue_transcript},
)
}
/**
* list_agents.
*
* @effects: [host]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: list_agents()
*/
pub fn list_agents() {
return __host_worker_list()
}
fn __sub_agent_string(value, fallback = "") {
if type_of(value) != "string" {
return fallback
}
let text = trim(value)
if text == "" {
return fallback
}
return value
}
fn __sub_agent_string_list(value, label) {
if value == nil {
return []
}
if type_of(value) != "list" {
throw label + ": expected a list of strings"
}
var out = []
for item in value {
if type_of(item) != "string" {
throw label + ": expected a list of strings"
}
let text = trim(item)
if text != "" && !contains(out, text) {
out = out.push(text)
}
}
return out
}
fn __sub_agent_base_tools(opts) {
if opts?.tools != nil {
return opts.tools
}
return __host_current_tool_registry()
}
fn __sub_agent_selected_tools(opts, allowed_tools) {
let base = __sub_agent_base_tools(opts)
if len(allowed_tools) == 0 {
return base
}
if base == nil {
return nil
}
return tool_select(base, allowed_tools)
}
fn __sub_agent_options(opts, session_id, selected_tools) {
var out = {}
let internal = [
"background",
"carry",
"returns",
"allowed_tools",
"name",
"execution",
"system",
"session_id",
"policy",
"reminder_propagation",
]
for key in opts.keys() {
if !contains(internal, key) {
out = out + {[key]: opts[key]}
}
}
out = out + {session_id: session_id}
if selected_tools != nil {
out = out + {tools: selected_tools}
}
return out
}
/**
* sub_agent_request builds the Harn-owned child-agent request envelope.
*
* @effects: []
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: sub_agent_request(task, options)
*/
pub fn sub_agent_request(task, options = nil) {
let task_text = __sub_agent_string(task)
if task_text == "" {
throw "sub_agent_run: task is required"
}
let opts = __worker_resume_options(__worker_options(options, "sub_agent_run"))
let allowed_tools = __sub_agent_string_list(opts?.allowed_tools, "sub_agent_run.allowed_tools")
let selected_tools = __sub_agent_selected_tools(opts, allowed_tools)
let session_id = __sub_agent_string(opts?.session_id, "sub_agent_session_" + uuid_v7())
return filter_nil(
{
_type: "sub_agent_request",
name: __sub_agent_string(opts?.name, "sub-agent"),
task: task_text,
system: __sub_agent_string(opts?.system, nil),
options: __sub_agent_options(opts, session_id, selected_tools),
returns_schema: opts?.returns?.schema,
session_id: session_id,
background: opts?.background ? true : false,
carry: opts?.carry,
execution: opts?.execution,
policy: opts?.policy,
reminder_propagation: opts?.reminder_propagation,
allowed_tools: allowed_tools,
},
)
}
/**
* sub_agent_run.
*
* @effects: [host]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: sub_agent_run(task, options)
*/
pub fn sub_agent_run(task, options = nil) {
return __host_sub_agent_run(sub_agent_request(task, options))
}