// SPDX-License-Identifier: ISC/* * Lua subagent bindings: agent.new(opts) -> handle; handle:submit(prompt), * handle:free(). * * A subagent is a second, independent struct clm_agent sharing the parent's * clm_host (transport + timers) -- host_uv's mux/loop are already agent- * agnostic (see host_uv.c), so no new loop or connection cache is needed. * clm_agent_new() itself registers the portable core's builtins * (read_file/write_file/list_dir) on the child, same as on any agent; * shell_exec/bg_exec live in libclmuv and are deliberately NOT wired here, * keeping this archive's link graph the same (lua_dep + clm_core_deps only, * see meson.build) -- a subagent that needs a shell is a later, explicit * decision, not a default. * * Same yield/resume pattern as lua_http.c and clm.sleep: handle:submit() * yields the calling tool-invocation coroutine; the child agent's own * on_turn_done callback resumes it once the child's turn lands. Only one * submit may be in flight per handle at a time. * * Permission handling is a prototype default only: on_permission auto- * allows once (mirrors the CLI's pre-gate behaviour, see main.c's * cb_permission), so a subagent's write_file etc. actually runs instead of * being denied by the library's secure default. A real embedder should * replace this with its own policy before this leaves prototype status. */#include <errno.h>#include <stdbool.h>#include <stddef.h>#include <stdlib.h>#include <string.h> #include <lua.h>#include <lauxlib.h>#include <lualib.h> #include "clm/clm.h"#include "clm/internal.h"#include "clm/log.h"#include "banned.h" /* Invocation-thread guard + registry helpers, shared with lua_http.c * (defined in lua_plugin.c). */int clm_lua_is_invocation_thread(lua_State *L);void clm_lua_mark_invocation_thread(lua_State *L, lua_State *co, int on);void clm_lua_clear_invocation_registry(lua_State *L); /* Prototype -- called from lua_plugin.c. */int clm_lua_agent_open(lua_State *L); #define CLM_LUA_SUBAGENT_META "clm_subagent"#define CLM_SUBAGENT_DEFAULT_MAX_ITER 10#define CLM_SUBAGENT_MAX_MAX_ITER 50 struct lua_subagent { struct clm_agent *child; /* NULL once freed */ lua_State *co; /* coroutine parked in submit(), or NULL */ lua_State *main_L; int co_ref; bool busy; char *last_text; /* most recent assistant content, overwritten */ char *last_error;}; static struct lua_subagent *check_subagent(lua_State *L, int idx){ return luaL_checkudata(L, idx, CLM_LUA_SUBAGENT_META);} /* ------------------------------------------------------------------ *//* Child agent callbacks *//* ------------------------------------------------------------------ */ static voidsa_on_assistant_text(const char *text, void *user){ struct lua_subagent *sa = user; char *copy = text ? strdup(text) : NULL; if (text != NULL && copy == NULL) return; /* OOM: keep whatever we had */ free(sa->last_text); sa->last_text = copy;} static voidsa_on_permission(const struct clm_permission_req *req, void *user){ struct lua_subagent *sa = user; /* Prototype default -- see file comment. */ clm_tool_permission_respond(sa->child, req, CLM_PERM_ALLOW_ONCE);} static voidsa_on_turn_done(int status, void *user){ struct lua_subagent *sa = user; lua_State *co = sa->co; lua_State *L = sa->main_L; int nres, rc; sa->busy = false; sa->co = NULL; /* Coroutine already gone (shouldn't happen, but mirror lua_http's * defensive guard rather than crash unroll()ing a dead state). */ if (co == NULL || lua_status(co) != LUA_YIELD) { clm_debug("subagent: coroutine not yielded, dropping turn result"); return; } if (status != 0) { const char *err = clm_agent_get_last_error(sa->child); lua_pushnil(co); lua_pushstring(co, err ? err : "subagent turn failed"); nres = 2; } else { if (sa->last_text != NULL) lua_pushstring(co, sa->last_text); else lua_pushstring(co, ""); nres = 1; } rc = lua_resume(co, L, nres, &nres); if (rc == LUA_OK || rc == LUA_ERRRUN || rc == LUA_ERRMEM || rc == LUA_ERRERR) { clm_lua_mark_invocation_thread(L, co, 0); luaL_unref(L, LUA_REGISTRYINDEX, sa->co_ref); clm_lua_clear_invocation_registry(L); if (rc != LUA_OK) { const char *err = lua_tostring(co, -1); clm_debug("subagent: coroutine error on resume: %s", err ? err : "(unknown)"); } } /* rc == LUA_YIELD: coroutine yielded again (another submit/http/sleep); * whatever it yielded into now owns co_ref until its own callback * fires. */} static const struct clm_callbacks subagent_callbacks = { .on_assistant_text = sa_on_assistant_text, .on_permission = sa_on_permission, .on_turn_done = sa_on_turn_done,}; /* ------------------------------------------------------------------ *//* agent.new(opts) *//* ------------------------------------------------------------------ */ static intlua_agent_new(lua_State *L){ struct clm_agent *parent; struct lua_subagent *sa; struct clm_cfg cfg = {0}; lua_Integer max_iter = CLM_SUBAGENT_DEFAULT_MAX_ITER; int r; luaL_checktype(L, 1, LUA_TTABLE); lua_getfield(L, LUA_REGISTRYINDEX, "_clm_agent"); parent = lua_touserdata(L, -1); lua_pop(L, 1); if (parent == NULL) return luaL_error(L, "agent.new: no parent agent context"); lua_getfield(L, 1, "model"); cfg.model = lua_isstring(L, -1) ? lua_tostring(L, -1) : clm_agent_get_model(parent); if (cfg.model == NULL) cfg.model = "local-model"; lua_getfield(L, 1, "base_url"); cfg.base_url = lua_isstring(L, -1) ? lua_tostring(L, -1) : clm_agent_get_base_url(parent); lua_getfield(L, 1, "api_key"); cfg.api_key = lua_isstring(L, -1) ? lua_tostring(L, -1) : clm_agent_get_api_key(parent); if (cfg.api_key == NULL) cfg.api_key = ""; lua_getfield(L, 1, "provider"); if (lua_isstring(L, -1)) cfg.provider = clm_provider_from_str(lua_tostring(L, -1)); else cfg.provider = clm_agent_get_provider(parent); lua_getfield(L, 1, "system_prompt"); cfg.system_prompt = lua_isstring(L, -1) ? lua_tostring(L, -1) : NULL; lua_getfield(L, 1, "max_iterations"); if (lua_isinteger(L, -1)) max_iter = lua_tointeger(L, -1); if (max_iter < 1) max_iter = 1; if (max_iter > CLM_SUBAGENT_MAX_MAX_ITER) max_iter = CLM_SUBAGENT_MAX_MAX_ITER; cfg.max_iterations = (size_t)max_iter; cfg.stream = false; /* on_assistant_text fires once with full content */ /* Popped in reverse: 6 getfield pushes above (model, base_url, * api_key, provider, system_prompt, max_iterations) = 6 values left * on the stack. */ lua_pop(L, 6); if (cfg.base_url == NULL) return luaL_error(L, "agent.new: no base_url (parent has none either)"); sa = lua_newuserdatauv(L, sizeof(*sa), 0); memset(sa, 0, sizeof(*sa)); luaL_setmetatable(L, CLM_LUA_SUBAGENT_META); /* clm_agent_new() already registers the portable builtins * (read_file/write_file/list_dir) itself -- nothing more to add here. */ r = clm_agent_new(&cfg, parent->host, &subagent_callbacks, sa, &sa->child); if (r < 0) return luaL_error(L, "agent.new: failed to create agent (%d)", r); return 1;} /* ------------------------------------------------------------------ *//* handle:submit(prompt) *//* ------------------------------------------------------------------ */ static intlua_agent_submit(lua_State *L){ struct lua_subagent *sa = check_subagent(L, 1); const char *prompt = luaL_checkstring(L, 2); int r; if (sa->child == NULL) return luaL_error(L, "agent:submit: agent already freed"); if (sa->busy) return luaL_error(L, "agent:submit: a submit is already in flight"); if (!clm_lua_is_invocation_thread(L)) { return luaL_error(L, "agent:submit may only be called directly " "from a tool invocation, not from a nested coroutine or at " "load time"); } lua_getfield(L, LUA_REGISTRYINDEX, "_clm_main_L"); sa->main_L = lua_touserdata(L, -1); lua_pop(L, 1); lua_getfield(L, LUA_REGISTRYINDEX, "_clm_co_ref"); sa->co_ref = (int)lua_tointeger(L, -1); lua_pop(L, 1); free(sa->last_text); sa->last_text = NULL; r = clm_agent_submit(sa->child, prompt); if (r < 0) return luaL_error(L, "agent:submit: failed to start turn (%d)", r); sa->busy = true; sa->co = L; return lua_yield(L, 0);} /* handle:free() -- explicit teardown; also called from __gc as a backstop. */static intlua_agent_free(lua_State *L){ struct lua_subagent *sa = check_subagent(L, 1); if (sa->busy) return luaL_error(L, "agent:free: a submit is still in flight"); if (sa->child != NULL) { clm_agent_free(sa->child); sa->child = NULL; } free(sa->last_text); sa->last_text = NULL; return 0;} static intlua_agent_gc(lua_State *L){ struct lua_subagent *sa = check_subagent(L, 1); /* A subagent GC'd while still busy means the invocation coroutine * that owns it is gone too (co_ref keeps it rooted otherwise) -- in * that pathological case just leak the child rather than free an * agent an in-flight callback might still touch. Normal teardown * goes through handle:free(). */ if (sa->busy) return 0; if (sa->child != NULL) { clm_agent_free(sa->child); sa->child = NULL; } free(sa->last_text); sa->last_text = NULL; return 0;} static const luaL_Reg subagent_methods[] = { {"submit", lua_agent_submit}, {"free", lua_agent_free}, {"__gc", lua_agent_gc}, {NULL, NULL},}; static const luaL_Reg agent_module[] = { {"new", lua_agent_new}, {NULL, NULL},}; intclm_lua_agent_open(lua_State *L){ luaL_newmetatable(L, CLM_LUA_SUBAGENT_META); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); luaL_setfuncs(L, subagent_methods, 0); lua_pop(L, 1); luaL_newlib(L, agent_module); lua_setglobal(L, "agent"); return 0;}
// SPDX-License-Identifier: ISC
/*
* Lua subagent bindings: agent.new(opts) -> handle; handle:submit(prompt),
* handle:free().
*
* A subagent is a second, independent struct clm_agent sharing the parent's
* clm_host (transport + timers) -- host_uv's mux/loop are already agent-
* agnostic (see host_uv.c), so no new loop or connection cache is needed.
* clm_agent_new() itself registers the portable core's builtins
* (read_file/write_file/list_dir) on the child, same as on any agent;
* shell_exec/bg_exec live in libclmuv and are deliberately NOT wired here,
* keeping this archive's link graph the same (lua_dep + clm_core_deps only,
* see meson.build) -- a subagent that needs a shell is a later, explicit
* decision, not a default.
*
* Same yield/resume pattern as lua_http.c and clm.sleep: handle:submit()
* yields the calling tool-invocation coroutine; the child agent's own
* on_turn_done callback resumes it once the child's turn lands. Only one
* submit may be in flight per handle at a time.
*
* Permission handling is a prototype default only: on_permission auto-
* allows once (mirrors the CLI's pre-gate behaviour, see main.c's
* cb_permission), so a subagent's write_file etc. actually runs instead of
* being denied by the library's secure default. A real embedder should
* replace this with its own policy before this leaves prototype status.
*/
#include <errno.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include "clm/clm.h"
#include "clm/internal.h"
#include "clm/log.h"
#include "banned.h"
/* Invocation-thread guard + registry helpers, shared with lua_http.c
* (defined in lua_plugin.c). */
int clm_lua_is_invocation_thread(lua_State *L);
void clm_lua_mark_invocation_thread(lua_State *L, lua_State *co, int on);
void clm_lua_clear_invocation_registry(lua_State *L);
/* Prototype -- called from lua_plugin.c. */
int clm_lua_agent_open(lua_State *L);
#define CLM_LUA_SUBAGENT_META "clm_subagent"
#define CLM_SUBAGENT_DEFAULT_MAX_ITER 10
#define CLM_SUBAGENT_MAX_MAX_ITER 50
struct lua_subagent {
struct clm_agent *child; /* NULL once freed */
lua_State *co; /* coroutine parked in submit(), or NULL */
lua_State *main_L;
int co_ref;
bool busy;
char *last_text; /* most recent assistant content, overwritten */
char *last_error;
};
static struct lua_subagent *
check_subagent(lua_State *L, int idx)
{
return luaL_checkudata(L, idx, CLM_LUA_SUBAGENT_META);
}
/* ------------------------------------------------------------------ */
/* Child agent callbacks */
/* ------------------------------------------------------------------ */
static void
sa_on_assistant_text(const char *text, void *user)
{
struct lua_subagent *sa = user;
char *copy = text ? strdup(text) : NULL;
if (text != NULL && copy == NULL)
return; /* OOM: keep whatever we had */
free(sa->last_text);
sa->last_text = copy;
}
static void
sa_on_permission(const struct clm_permission_req *req, void *user)
{
struct lua_subagent *sa = user;
/* Prototype default -- see file comment. */
clm_tool_permission_respond(sa->child, req, CLM_PERM_ALLOW_ONCE);
}
static void
sa_on_turn_done(int status, void *user)
{
struct lua_subagent *sa = user;
lua_State *co = sa->co;
lua_State *L = sa->main_L;
int nres, rc;
sa->busy = false;
sa->co = NULL;
/* Coroutine already gone (shouldn't happen, but mirror lua_http's
* defensive guard rather than crash unroll()ing a dead state). */
if (co == NULL || lua_status(co) != LUA_YIELD) {
clm_debug("subagent: coroutine not yielded, dropping turn result");
return;
}
if (status != 0) {
const char *err = clm_agent_get_last_error(sa->child);
lua_pushnil(co);
lua_pushstring(co, err ? err : "subagent turn failed");
nres = 2;
} else {
if (sa->last_text != NULL)
lua_pushstring(co, sa->last_text);
else
lua_pushstring(co, "");
nres = 1;
}
rc = lua_resume(co, L, nres, &nres);
if (rc == LUA_OK || rc == LUA_ERRRUN || rc == LUA_ERRMEM ||
rc == LUA_ERRERR) {
clm_lua_mark_invocation_thread(L, co, 0);
luaL_unref(L, LUA_REGISTRYINDEX, sa->co_ref);
clm_lua_clear_invocation_registry(L);
if (rc != LUA_OK) {
const char *err = lua_tostring(co, -1);
clm_debug("subagent: coroutine error on resume: %s",
err ? err : "(unknown)");
}
}
/* rc == LUA_YIELD: coroutine yielded again (another submit/http/sleep);
* whatever it yielded into now owns co_ref until its own callback
* fires. */
}
static const struct clm_callbacks subagent_callbacks = {
.on_assistant_text = sa_on_assistant_text,
.on_permission = sa_on_permission,
.on_turn_done = sa_on_turn_done,
};
/* ------------------------------------------------------------------ */
/* agent.new(opts) */
/* ------------------------------------------------------------------ */
static int
lua_agent_new(lua_State *L)
{
struct clm_agent *parent;
struct lua_subagent *sa;
struct clm_cfg cfg = {0};
lua_Integer max_iter = CLM_SUBAGENT_DEFAULT_MAX_ITER;
int r;
luaL_checktype(L, 1, LUA_TTABLE);
lua_getfield(L, LUA_REGISTRYINDEX, "_clm_agent");
parent = lua_touserdata(L, -1);
lua_pop(L, 1);
if (parent == NULL)
return luaL_error(L, "agent.new: no parent agent context");
lua_getfield(L, 1, "model");
cfg.model = lua_isstring(L, -1) ? lua_tostring(L, -1)
: clm_agent_get_model(parent);
if (cfg.model == NULL)
cfg.model = "local-model";
lua_getfield(L, 1, "base_url");
cfg.base_url = lua_isstring(L, -1) ? lua_tostring(L, -1)
: clm_agent_get_base_url(parent);
lua_getfield(L, 1, "api_key");
cfg.api_key = lua_isstring(L, -1) ? lua_tostring(L, -1)
: clm_agent_get_api_key(parent);
if (cfg.api_key == NULL)
cfg.api_key = "";
lua_getfield(L, 1, "provider");
if (lua_isstring(L, -1))
cfg.provider = clm_provider_from_str(lua_tostring(L, -1));
else
cfg.provider = clm_agent_get_provider(parent);
lua_getfield(L, 1, "system_prompt");
cfg.system_prompt = lua_isstring(L, -1) ? lua_tostring(L, -1) : NULL;
lua_getfield(L, 1, "max_iterations");
if (lua_isinteger(L, -1))
max_iter = lua_tointeger(L, -1);
if (max_iter < 1)
max_iter = 1;
if (max_iter > CLM_SUBAGENT_MAX_MAX_ITER)
max_iter = CLM_SUBAGENT_MAX_MAX_ITER;
cfg.max_iterations = (size_t)max_iter;
cfg.stream = false; /* on_assistant_text fires once with full content */
/* Popped in reverse: 6 getfield pushes above (model, base_url,
* api_key, provider, system_prompt, max_iterations) = 6 values left
* on the stack. */
lua_pop(L, 6);
if (cfg.base_url == NULL)
return luaL_error(L, "agent.new: no base_url (parent has none either)");
sa = lua_newuserdatauv(L, sizeof(*sa), 0);
memset(sa, 0, sizeof(*sa));
luaL_setmetatable(L, CLM_LUA_SUBAGENT_META);
/* clm_agent_new() already registers the portable builtins
* (read_file/write_file/list_dir) itself -- nothing more to add here. */
r = clm_agent_new(&cfg, parent->host, &subagent_callbacks, sa, &sa->child);
if (r < 0)
return luaL_error(L, "agent.new: failed to create agent (%d)", r);
return 1;
}
/* ------------------------------------------------------------------ */
/* handle:submit(prompt) */
/* ------------------------------------------------------------------ */
static int
lua_agent_submit(lua_State *L)
{
struct lua_subagent *sa = check_subagent(L, 1);
const char *prompt = luaL_checkstring(L, 2);
int r;
if (sa->child == NULL)
return luaL_error(L, "agent:submit: agent already freed");
if (sa->busy)
return luaL_error(L, "agent:submit: a submit is already in flight");
if (!clm_lua_is_invocation_thread(L)) {
return luaL_error(L, "agent:submit may only be called directly "
"from a tool invocation, not from a nested coroutine or at "
"load time");
}
lua_getfield(L, LUA_REGISTRYINDEX, "_clm_main_L");
sa->main_L = lua_touserdata(L, -1);
lua_pop(L, 1);
lua_getfield(L, LUA_REGISTRYINDEX, "_clm_co_ref");
sa->co_ref = (int)lua_tointeger(L, -1);
lua_pop(L, 1);
free(sa->last_text);
sa->last_text = NULL;
r = clm_agent_submit(sa->child, prompt);
if (r < 0)
return luaL_error(L, "agent:submit: failed to start turn (%d)", r);
sa->busy = true;
sa->co = L;
return lua_yield(L, 0);
}
/* handle:free() -- explicit teardown; also called from __gc as a backstop. */
static int
lua_agent_free(lua_State *L)
{
struct lua_subagent *sa = check_subagent(L, 1);
if (sa->busy)
return luaL_error(L, "agent:free: a submit is still in flight");
if (sa->child != NULL) {
clm_agent_free(sa->child);
sa->child = NULL;
}
free(sa->last_text);
sa->last_text = NULL;
return 0;
}
static int
lua_agent_gc(lua_State *L)
{
struct lua_subagent *sa = check_subagent(L, 1);
/* A subagent GC'd while still busy means the invocation coroutine
* that owns it is gone too (co_ref keeps it rooted otherwise) -- in
* that pathological case just leak the child rather than free an
* agent an in-flight callback might still touch. Normal teardown
* goes through handle:free(). */
if (sa->busy)
return 0;
if (sa->child != NULL) {
clm_agent_free(sa->child);
sa->child = NULL;
}
free(sa->last_text);
sa->last_text = NULL;
return 0;
}
static const luaL_Reg subagent_methods[] = {
{"submit", lua_agent_submit},
{"free", lua_agent_free},
{"__gc", lua_agent_gc},
{NULL, NULL},
};
static const luaL_Reg agent_module[] = {
{"new", lua_agent_new},
{NULL, NULL},
};
int
clm_lua_agent_open(lua_State *L)
{
luaL_newmetatable(L, CLM_LUA_SUBAGENT_META);
lua_pushvalue(L, -1);
lua_setfield(L, -2, "__index");
luaL_setfuncs(L, subagent_methods, 0);
lua_pop(L, 1);
luaL_newlib(L, agent_module);
lua_setglobal(L, "agent");
return 0;
}