import { agent_emit_event } from "std/agent/state"
// `agent_progress` is a cosmetic progress-report surface — it emits a task-list
// event for the user, nothing more. A malformed `status`/`priority` enum from a
// weaker model must NOT abort the whole turn (the historical hard `throw` crashed
// the agent loop on a single typo like `status: "in-progress"` or `"doing"`).
// Normalize the obvious synonyms and otherwise fall back to a sane default; never
// throw. `_label` is retained for signature symmetry with the other validators.
fn __agent_progress_status(value, _label) {
if type_of(value) != "string" {
return "in_progress"
}
if contains(["pending", "in_progress", "completed"], value) {
return value
}
let normalized = replace(replace(lowercase(trim(value)), "-", "_"), " ", "_")
if contains(["pending", "todo", "not_started", "queued", "blocked", "waiting"], normalized) {
return "pending"
}
if contains(["completed", "complete", "done", "finished", "resolved", "closed"], normalized) {
return "completed"
}
// in_progress, doing, active, started, working, wip, … and any unknown value.
return "in_progress"
}
fn __agent_progress_priority(value, _label) {
if type_of(value) != "string" {
return nil
}
if contains(["high", "medium", "low"], value) {
return value
}
let normalized = lowercase(trim(value))
if contains(["high", "urgent", "critical", "p0", "p1"], normalized) {
return "high"
}
if contains(["medium", "med", "normal", "p2"], normalized) {
return "medium"
}
if contains(["low", "minor", "p3", "p4"], normalized) {
return "low"
}
// Unknown → unprioritized rather than a turn-aborting throw.
return nil
}
fn __agent_progress_entries(value) {
if value == nil {
return []
}
// Lenient by design (cosmetic surface): a single entry passed as a bare dict
// is wrapped; a non-list/non-dict shape yields no entries instead of aborting
// the turn. Individual entries that lack usable content are skipped, not fatal.
let list = if type_of(value) == "list" {
value
} else if type_of(value) == "dict" {
[value]
} else {
[]
}
var entries = []
var index = 0
for raw in list {
let label = "agent_progress: entries[" + to_string(index) + "]"
index = index + 1
if type_of(raw) != "dict" {
continue
}
let content = raw?.content
if type_of(content) != "string" || trim(content) == "" {
continue
}
var entry = {content: content, status: __agent_progress_status(raw?.status, label)}
let priority = __agent_progress_priority(raw?.priority, label)
if priority != nil {
entry = entry + {priority: priority}
}
entries = entries.push(entry)
}
return entries
}
fn __agent_progress_payload(input) {
if type_of(input) != "dict" {
throw "agent_progress: argument must be a dict"
}
let message = input?.message
if message != nil && type_of(message) != "string" {
throw "agent_progress: message must be a string or nil"
}
let entries = __agent_progress_entries(input?.entries)
if (message == nil || trim(message) == "") && len(entries) == 0 {
throw "agent_progress: message or entries is required"
}
let replace = input?.replace ?? true
if type_of(replace) != "bool" {
throw "agent_progress: replace must be a bool"
}
let metadata = input?.metadata ?? {}
if type_of(metadata) != "dict" {
throw "agent_progress: metadata must be a dict"
}
return {
message: if message == nil {
nil
} else {
message
},
entries: entries,
replace: replace,
metadata: metadata,
}
}
fn __agent_progress_tool_name(config) {
let name = config?.name ?? "agent_progress"
if type_of(name) != "string" || trim(name) == "" {
throw "agent_loop: progress_tool.name must be a non-empty string"
}
return name
}
fn __agent_progress_tool_description(config) {
let description = config?.description
?? "Report concise task progress to the host without ending the turn."
if type_of(description) != "string" || trim(description) == "" {
throw "agent_loop: progress_tool.description must be a non-empty string"
}
return description
}
fn __agent_progress_tool_nudge(config) {
let tool_name = __agent_progress_tool_name(config)
let nudge = config?.system_prompt_nudge
?? "When useful, call "
+ tool_name
+ " to report concise progress. Use message for narration, entries for task-list state, and replace=true unless the update should append."
if nudge == nil {
return nil
}
if type_of(nudge) != "string" {
throw "agent_loop: progress_tool.system_prompt_nudge must be a string or nil"
}
let trimmed = trim(nudge)
if trimmed == "" {
return nil
}
return trimmed
}
fn __agent_progress_tool_config(value) {
if value == nil {
return nil
}
let kind = type_of(value)
if kind == "bool" {
if value {
return {}
}
return nil
}
if kind != "dict" {
throw "agent_loop: progress_tool must be true, false, a dict, or nil"
}
return value
}
/**
* agent_progress emits a structured progress_reported event for the current agent session.
*
* @effects: [agent]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: agent_progress(input)
*/
pub fn agent_progress(input) {
let payload = __agent_progress_payload(input)
let session_id = agent_session_current_id()
if session_id == nil || session_id == "" {
throw "agent_progress: no active agent session"
}
agent_emit_event(session_id, "progress_reported", payload)
return nil
}
/**
* agent_progress_tool adds a handler-backed agent_progress tool to a registry.
*
* @effects: [agent]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: agent_progress_tool(registry, options)
*/
pub fn agent_progress_tool(registry = nil, options = nil) {
let config = options ?? {}
return tool_define(
registry ?? tool_registry(),
__agent_progress_tool_name(config),
__agent_progress_tool_description(config),
{
parameters: {
message: {type: "string", description: "Short progress narration", required: false},
entries: {
type: "array",
description: "Task-list entries with content, status, and optional priority",
required: false,
},
replace: {
type: "boolean",
description: "Whether this update replaces the prior progress view",
required: false,
},
metadata: {type: "object", description: "Free-form progress metadata", required: false},
},
returns: {type: "null"},
handler: { args -> agent_progress(args) },
},
)
}
/**
* agent_progress_apply_options injects the progress tool when agent_loop opts in.
*
* @effects: [agent]
* @allocation: heap
* @errors: []
* @api_stability: experimental
* @example: agent_progress_apply_options(options)
*/
pub fn agent_progress_apply_options(options = nil) {
let opts = options ?? {}
let config = __agent_progress_tool_config(opts?.progress_tool)
if config == nil {
return opts
}
let tools = agent_progress_tool(opts?.tools, config)
let nudge = __agent_progress_tool_nudge(config)
var out = opts + {tools: tools}
if nudge != nil {
out = out + {_progress_tool_system_prompt_nudge: nudge}
}
return out
}