Tlamatini — Complete New-Agent Creation Runbook (700+ steps)
Audience: Claude Code working ON the Tlamatini codebase for Angela. Scope: adding ONE brand-new workflow agent end-to-end across every surface Tlamatini touches — backend pool script, Django view/url, migration, Parametrizer, CSS coloring, all the frontend JS, the
agentic_control_panel.htmlinputs/outputs connector contract, the configuration dialog, Multi-Turn (the wrapped chat-agent tool), the Exec Report, FlowCreator'sagentic_skill.md, FlowHypervisor'smonitoring-prompt.pmt, the command watchdog + orphan reaper, demo "Prompts example" creation,requirements.txt/build.pypackaging, a full Python unit-test module, AND a Playwright regression in Claude's own harness — then docs, lint, migrate, and a visible dogfood run.This skill is the master checklist. The two
@-imported guides (Tlamatini/.agents/workflows/create_new_agent.md,Tlamatini/.mcps/create_new_mcp.md) are the canonical mechanics for individual surfaces; this skill is the superset that also nails the things they leave implicit (watchdog, FlowHypervisor, config dialog, demo prompts, Python tests, Playwright tests, packaging). Read both@-imports and thetlamatini-agent-namingskill first, then execute the steps below in order.
Placeholders used throughout (decide these in Phase 0, then NEVER drift)
| Token | Meaning | Example (Pingerer) |
|---|---|---|
<Display> |
exact-cased display name = DB agentDescription (single source of truth) |
Pingerer |
<lower> |
<Display>.lower(), spaces→nothing for single-word; underscores for multi-word dir |
pingerer |
<dash> |
<Display>.toLowerCase().replace(/\s+/g,'-') — CSS classMap key |
pingerer |
<space> |
<Display>.toLowerCase() preserving spaces — JS connection checks |
pingerer |
<Pascal> |
JS connector symbol fragment update<Pascal>Connection |
Pingerer |
<CAPS> |
ALL-CAPS protocol token base — INI_SECTION_<CAPS> + <CAPS> SPECIAL NOTES |
PINGERER |
<css> |
the canvas/exec-report CSS class root (== <dash> minus dashes for single-word, kept dashed for multi-word; equals <lower> for single-word) |
pingerer |
N |
the agent's idAgent / migration sequence number | (next free) |
Worked multi-word example (
Node Manager):<Display>=Node Manager,<lower>=node_manager(dir/pool/file),<dash>=node-manager(CSS classMap key),<space>=node manager(connection checks),<Pascal>=NodeManager,<CAPS>=NODE_MANAGER,<css>=node-manager/nodemanager-agent(verify against an existing multi-word agent — historically some classMap values dropped the dash, e.g.'node-manager': 'nodemanager-agent'; COPY an existing sibling exactly).
PHASE 0 — Preflight, scoping & naming (lock these before any code)
- Confirm with Angela the agent's purpose in one sentence (what task it performs).
- Decide whether the agent is deterministic (no LLM) or LLM-powered — this changes config keys and FlowHypervisor timing notes.
- Decide whether the agent is state-changing (mutates files/DB/remote/GUI/sends messages) or observational/read-only (Shoter/Camcorder/Recorder/AudioPlayer/VideoPlayer/Monitor-*). This decides Exec-Report membership.
- Decide whether the agent is Active (starts downstream via
target_agents) or Terminal/Monitoring (does not). - Decide whether the agent produces structured output consumed by Parametrizer (emits
INI_SECTION_<CAPS>). - Decide whether the agent should be LLM-callable in Multi-Turn (a wrapped
chat_agent_<lower>tool) — most new agents should be. - Decide whether the agent is long-running (Monitor-style) or short-lived.
- Decide whether the agent scaffolds project directories (firmware/engine style → defaults to
<app>/Templates) or writes scratch (→<app>/Temp). 8a. If it delegates placement to Mover or cleanup to Deleter, lock the v1.48.13 contract: empty, relative, and legacyC:/Temp/...scratch paths re-root underTLAMATINI_TEMP; explicit absolute user paths remain authoritative; normalization never broadens deletion scope. 8b. If it adds UI, reusedialog_theme.cssanddialog_policy.js; place long-operation navigation controls inLONG_OPERATION_DISABLED_MENU_BUTTONS; bumpSTATIC_VERSIONafter every JavaScript/CSS/template edit. - Decide whether the agent spawns console child processes (relevant to the orphan reaper + command watchdog).
- Decide whether the agent has a singleton constraint (only FlowCreator/FlowHypervisor are; a normal agent is not).
- Lock the
<Display>name with EXACT casing — this isagentDescriptionand the single source of truth. - Invoke the
tlamatini-agent-namingskill and derive<lower>,<dash>,<space>,<Pascal>,<CAPS>,<css>from<Display>per its transform table. - NEVER let any non-identifier surface display a different casing of
<Display>(Angela is emphatic — STM32er must never become STM32Er). - Pick the next free idAgent / migration number
N: runGloboverTlamatini/agent/migrations/0*.pyand takemax+1; confirm noagentDescription='<Display>'already exists. - Pick a reference sibling agent that most resembles the new one (e.g. Shoter for capture, Apirer for HTTP, Kalier for an API bridge, Camcorder/Recorder for media). You will COPY its structure.
- Pick a second reference sibling that already does Multi-Turn + Parametrizer + Exec-Report so you can copy those wirings (Camcorder and Recorder are the most recent fully-wired examples).
- Read the chosen sibling's
agent/agents/<sibling>/<sibling>.pyin full before writing anything. - Read the chosen sibling's
config.yamlin full. - Read the sibling's
update_<sibling>_connection_viewinviews.py. - Read the sibling's CSS block in
agentic_control_panel.css. - Read the sibling's
ChatWrappedAgentSpecinchat_agent_registry.py. - Read the sibling's
_PARAMETRIZER_OUTPUT_FIELDSentry inservices/agent_contracts.py. - Read the sibling's
_EXEC_REPORT_TOOLSentry (if state-changing) inmcp_agent.py. - Read the sibling's
test_<sibling>_agent.pyin full — it is your test template. - Write down the full list of
config.yamlkeys the new agent needs (params + connection fields). This list is referenced by ~8 later surfaces; keeping it stable prevents silent drift. - Decide the agent's connection-field shape:
target_agents+source_agents(normal),target_agents_a/_b(Asker/Forker),target_agents_l/_g(Counter),source_agent_1/_2(OR/AND), oroutput_agents(Stopper/Ender/Cleaner). - Decide the INI_SECTION KV header fields (what downstream agents can address) + whether there is a
response_body. - Create a small scratch note (or a dated pivot file per
feedback_track_changes_pivot_file) listing the verbatim request + every file you will touch, so a later "roll back just that change" is exact.
PHASE 1 — Backend: the pool agent script + config.yaml
- Create directory
Tlamatini/agent/agents/<lower>/. - Create
Tlamatini/agent/agents/<lower>/config.yaml. - In
config.yaml, add a top comment# <Display> Agent Configuration. - Add each functional param key with a sensible default value (from your Phase-0 key list).
- Add the connection fields that apply:
target_agents: []if Active. - Add
source_agents: []if it monitors upstream logs. - Use
output_agents: []INSTEAD oftarget_agentsONLY for Stopper/Ender/Cleaner-style agents. - For OR/AND use scalar
source_agent_1: ""/source_agent_2: "". - For Asker/Forker use
target_agents_a: []/target_agents_b: []. - For Counter use
target_agents_l: []/target_agents_g: []. - Leave any credential/secret field as an empty string default (never hardcode a key —
regen_secrets.py/ Flow-Compiler redaction depends on this). - If a param is numeric, default it to a real number (not a string) so
yaml.safe_loadyields the right type. - If the agent has nested config, model it as a nested mapping (e.g.
llm:block withbase_url/model/temperature) mirroring the sibling. - Create
Tlamatini/agent/agents/<lower>/<lower>.py. - Copy the FULL boilerplate from the reference sibling's
.py(module preamble + all helpers +main()shape). DO NOT hand-roll helpers. - Make
os.environ['FOR_DISABLE_CONSOLE_CTRL_HANDLER'] = '1'the FIRST statement afterimport os, sys. - Keep the standard helpers verbatim:
load_config,get_python_command,get_user_python_home,get_agent_env,get_pool_path,get_agent_directory,get_agent_script_path,is_agent_running,wait_for_agents_to_stop,start_agent,write_pid_file,remove_pid_file. - Ensure the log file name is exactly
{directory_name}.log(the canvas reads this; any other name breaks LED/log surfacing). - Compute
CURRENT_DIR_NAMEandLOG_FILE_PATHthe same way the sibling does. - Keep the top-of-module
subprocess.Popen.__init__monkey-patch (_chg_guarded_init) that defaultscreationflagstoCREATE_NO_WINDOW— this is the orphan seatbelt; do not remove it. - Implement the agent's core logic between
logging.info("🚀 <CAPS> AGENT STARTED")and the target-trigger block. - Use a distinctive emoji +
<Display>-cased phrase in the STARTED log line (FlowHypervisor markers key off these — see Phase 18). - Wrap external/hardware/network calls in try/except so a missing device/host produces a logged error, not a crash.
- For any numeric
config.get(...)that could arrive as a wrapped-parser string (e.g."5 from the default mic"), coerce via a_coerce_int/_coerce_floathelper that extracts the leading number and never raises (see the Recorder fix — this caught a real incident). - Write the PID file immediately at the top of
main()viawrite_pid_file(). - Remove the PID file in a
finally:block viaremove_pid_file(). - Add a short
time.sleep(0.4)beforeremove_pid_file()to keep the LED green briefly (sibling pattern). - End
main()withsys.exit(0). - If Active, place the target-trigger block at the END of the work:
if target_agents: wait_for_agents_to_stop(target_agents)then afor target in target_agents: start_agent(target)loop. - The concurrency guard
wait_for_agents_to_stop(target_agents)MUST come BEFORE thestart_agentloop (prevents duplicate spawns in looping flows). - If state-changing, ensure
target_agentsare triggered REGARDLESS of success/failure (so a downstream Forker can branch on the outcome) — match the "ALWAYS triggers target_agents" contract used by Kalier/Unrealer/STM32er/Camcorder. - Do NOT trigger
target_agentsfrom a Terminal/Monitoring agent (Emailer/Notifier/Monitor-*) — those leavetarget_agentsas canvas-only metadata. - Add a final completion log line
logging.info("🏁 <Display> agent finished.")before thefinally. - Do NOT import anything from
agent.*(theagentDjango package) inside the pool script — pool subprocesses have nosys.pathback into it (ModuleNotFoundError). Port any needed runtime mechanics inline (~100–200 lines) as ACPXer does. - Resolve any bundled asset path for BOTH frozen (
os.path.dirname(sys.executable)) and source (os.path.dirname(os.path.abspath(__file__))) modes. - If the agent needs a third-party lib (e.g.
opencv-python,sounddevice), import it lazily inside the function that uses it and report a clean message if it is absent (do not crash at import time). - Keep
main()callable underif __name__ == "__main__": main(). - Verify
config.yamlround-trips:python -c "import yaml; print(yaml.safe_load(open(r'...config.yaml')))". - Confirm the script has no top-level side effects beyond the documented
os.chdir/logging.basicConfigthat the test harness saves+restores. - Confirm there is no top-level
defplaced above the imports (that trips ruff E402) — if you need a module-top guard (temp policy), make it anif-block, not adef. - Re-read the whole
.pyonce and diff it mentally against the sibling to ensure no helper was accidentally dropped.
PHASE 2 — Reanimation & lifecycle (pause/resume correctness)
- Right after
LOG_FILE_PATHis set and BEFORElogging.basicConfig(...), add_IS_REANIMATED = os.environ.get('AGENT_REANIMATED') == '1'. - Immediately after, add
if not _IS_REANIMATED: open(LOG_FILE_PATH, 'w').close()(truncate only on a fresh start). - NEVER truncate the log when
_IS_REANIMATEDis true (resume appends). - In
main(), when_IS_REANIMATED, log🔄 <Display> REANIMATED (resuming from pause)as the first line. - If the agent persists restart state (file offsets, counters, checkpoints), store it in files named
reanim*(e.g.reanim.pos) so Ender can reset them on stop. - If the agent polls a source log, implement
save_reanim_offset(offset)/get_reanim_offset(...)usingreanim.pos(orreanim_<source>.posper source). - Load the offset at startup and call
save_reanim_offsetafter each read. - Confirm the agent is idempotent under resume: resuming produces the same behavior as if never interrupted.
- Confirm the agent does NOT delete its own
reanim*files (only Ender clears them on Stop). - Confirm a Counter-style agent uses
reanim.counter; a Gatewayer-style usesreanim_queue.json/reanim_dedup.json; a registry agent usesreanim_registry.json(only if applicable). - Verify the three lifecycle modes mentally: Fresh start (no env var → truncate → STARTED), Reanimation (
AGENT_REANIMATED=1→ no truncate → REANIMATED → load reanim files), Stop (Ender clears reanim files). - Confirm pressing Start while PAUSED acts as Resume (no special code needed — the ACP handles it).
- Confirm the agent does not assume it is always a fresh start anywhere in its logic.
PHASE 3 — Structured output (INI_SECTION — Parametrizer producer)
- If the agent feeds Parametrizer, define the section in the unified format:
INI_SECTION_<CAPS><<<…>>>END_SECTION_<CAPS>. - Use
<CAPS>= the UPPERCASE base name (single ALL-CAPS token convention — do NOT mixed-case it). - Put the KV header (one
key: valueper line) BEFORE the first blank line. - Put the multi-line body AFTER the first blank line (it becomes
response_body). - If there is no body, omit the blank line (KV-only section).
- Emit each section in a SINGLE atomic
logging.info(...)call (concurrent writes interleave and corrupt otherwise). - Emit N separate sections for N results (one section per result/response).
- Choose KV header field names that downstream agents will address (e.g.
output_path,status,url,return_code,success). - Include
response_bodyin the header field list ONLY if the section has a body. - Build the section string with explicit
\njoins exactly like the sibling (do not use f-string multiline that swallows indentation). - Confirm the section start/end tokens match exactly (
INI_SECTION_<CAPS><<<and>>>END_SECTION_<CAPS>). - If the agent ALWAYS emits the section even on failure (recommended for routable branching), document that in the section body (e.g.
status: error). - Verify a round-trip parse: feed a sample log line through Parametrizer's
_parse_section_contentmentally or with a quick test (Phase 22 covers the real test).
PHASE 4 — Temp & Templates directory policy (2026-06-02)
- If the agent writes TEMPORARY/scratch files, route them under
<app>/Temp— NEVERC:\Temp,%TEMP%, or a baretempfile.gettempdir(). - Copy the module-top temp guard from
executer.pyverbatim:if (os.environ.get('TLAMATINI_TEMP') or '').strip(): import tempfile as _tlt_tempfile; _tlt_tempfile.tempdir = os.environ['TLAMATINI_TEMP'].strip(); …. - Keep that guard as an
if-block (NOT a top-leveldef) so it sits above the imports without tripping ruff E402. - If the agent SCAFFOLDS a project/template directory (firmware/engine style), default its parent to
<app>/Templates(TLAMATINI_TEMPLATES) unless Angela supplies a path. - Use
agent/path_guard.pyresolvers conceptually (get_app_temp_root/get_app_templates_root) — but remember the pool script can't importagent.*, so rely on the inheritedTLAMATINI_TEMP/TLAMATINI_TEMPLATESenv vars (the parent exports them). - Confirm
Temp= throwaway scratch;Templates= deliverable project trees (never viatempfile). - Confirm no path the agent writes escapes the Tlamatini app root.
- If you add an in-process
@toolinstead (rare), route its scratch throughpath_guard.get_app_temp_root()/resolve_temp_path()directly. - Note that
prompt.pmtRules 15/16 inject the absolute{temp_directory}/{templates_directory}for the LLM — you do not edit those unless the policy itself changes.
PHASE 5 — Watchdog & orphan-reaper properties
- If the agent spawns console child processes (
cmd/powershell/external CLI), understand it is subject to the command watchdog (agent/command_watchdog.py). - Know the watchdog kills only console interpreters + descendants that make NO PROGRESS (CPU-seconds + IO bytes across the whole subtree) for N idle ticks past
hang_grace_seconds— it is progress-based, NOT duration-based. - Ensure the agent's children make observable progress (CPU or IO) while working so the watchdog never kills a long-but-working job.
- If the agent legitimately runs a long quiet external job, document expected behavior and consider that
command_watchdog_*config keys are tunable (do not weaken the watchdog contract for one agent). - Confirm the agent never relies on a child that blocks on stdin with zero CPU+IO for the full grace window (that is exactly the hang class the watchdog targets — feed
DEVNULL/EOF to children). - For an in-process
@toolthat runs a command, use the bounded_run_command_boundedpattern (Popen + stdin=DEVNULL +communicate(timeout=...)+ whole-tree kill), not a naivesubprocess.run(which is a fake guarantee forshell=Truegrandchildren). - Understand the orphan reaper (
agent/orphan_reaper.py) Tier 1/2/3: if the new agent spawns console children via a NEW tool name, either add that tool name to_PROCESS_SPAWNING_TOOL_NAMESinmcp_agent.py(Tier-1 reap after it) or rely on Tier-2's pool-cmdline scan. - Confirm the agent's children carry the
CREATE_NO_WINDOWdefault (the_chg_guarded_initmonkey-patch handles this automatically — verify it is present from Phase 1). - Confirm the reaper/watchdog can never be tripped into killing the agent's OWN long-running python runtime (those are python, not console interpreters; the watchdog scopes to
cmd/powershell/pwsh+ descendants only). - For a VISIBLE/desktop agent (a window the user must SEE) that you launch yourself during dogfooding, recall the reaper protects ancestors + console-window owner + main PID — but the agent runs as its own subprocess, so it is reaped only if genuinely orphaned.
- Note: the watchdog + reaper changes take effect in a frozen build only after
python build.py— flag this to Angela if she runs the frozenC:\Tlamatiniinstall.
PHASE 6 — Backend: Django connection-update view + urls.py
- Open
Tlamatini/agent/views.py. - Copy
update_<sibling>_connection_viewand rename itupdate_<lower>_connection_view. - Keep the
@csrf_exempt+@require_POSTdecorators. - Parse
data = json.loads(request.body.decode('utf-8')). - Read
target_agent,action(default'add'), andconnection_type(default'target'). - Return a 400 JSON error if
target_agentis missing. - Normalize the agent id
<lower>-N→ pool name<lower>_N(split on-, pop trailing digit as cardinal, join base with_). - Reject path-traversal in the pool name (
'..','/','') with a 400. - Build
config_path = os.path.join(get_pool_path(request), pool_name, 'config.yaml'); 404 if missing. - Load the config with
yaml.safe_load(...) or {}. - Normalize the
target_agentid → target pool name the same way. - Choose the list key:
source_agentsifconnection_type=='source'elsetarget_agents(or the agent's special connection field per Phase 0). - Ensure the list exists (
if not isinstance(config.get(list_name), list): config[list_name] = []). - On
action=='add', append the target pool name if not present. - On
action=='remove', remove it if present. - Omit-if-empty rule: never write an empty string into a connection field (the deep-merge in
save_agent_config_viewwould destroy a template default). - Write the config back with
yaml.dump(..., default_flow_style=False, allow_unicode=True, sort_keys=False). - Return
{"success": True, ...}JSON. - Wrap the whole body in try/except returning a 500 JSON on error.
- If the agent uses a special connection shape (Asker/Forker/Counter/OR/AND/Ender), copy that sibling's view instead — those write
target_agents_a/_b,target_agents_l/_g,source_agent_1/_2, oroutput_agents. - Open
Tlamatini/agent/urls.py. - Add
path('update_<lower>_connection/<str:agent_name>/', views.update_<lower>_connection_view, name='update_<lower>_connection'),. - Confirm the route name is unique and matches the connector fetch URL you will write in Phase 10.
- If the agent needs any extra backend endpoint (rare), add it to both
views.pyandurls.pynow and note it for the docs sweep. - Re-read the view once to confirm it matches the producer/consumer shape (a target-only producer like Shoter/Camcorder has no
sourcebranch usage in practice but should still acceptconnection_type).
PHASE 7 — Database migration (seed the Agent row)
- Find the highest existing migration with
GlobTlamatini/agent/migrations/0*.py. - Create
Tlamatini/agent/migrations/<NNNN>_add_<lower>.py(next sequential number). - Implement
add_<lower>_agent(apps, schema_editor)that gets theAgentmodel viaapps.get_model('agent','Agent'). - Guard against duplicates:
if Agent.objects.filter(agentDescription='<Display>').exists(): return. - Compute
next_id = (max idAgent or 0) + 1. - Create the row:
Agent.objects.create(idAgent=next_id, agentName=f'agent-{next_id}', agentDescription='<Display>', agentContent='true'). - Use the EXACT
<Display>casing inagentDescription. ⛔ 2026-07-26 — the migration is NOT the source of truth:apps.py::ready()DELETES everyAgentrow on each server start and re-derives the name fromagent/services/agent_paths.py::display_name_from_agent_type, so you MUST also add"<lower>": "<Display>"to thatoverridesmap or your agent shipsstr.title()-mangled ("Pdfer", "Sqler", "Esp32Er", "Latexer"). Use the HYPHENATED display form ifacp-canvas-core.jsonly tests a hyphenated literal for it (file-creator,video-analyzer,kyber-keygen,monitor-log, …) — a spaced name matches nothing there and the canvas connection is silently never saved. Keepchat_agent_registry.display_namebyte-identical in the same pass (it keys the fail-openagent_<display>_statusenable gate). Verify:python manage.py test agent.test_agent_display_names. - Implement
remove_<lower>_agentreverse that deletes the row byagentDescription. - Set
dependencies = [('agent', '<previous_migration_name>')]. - Add
operations = [migrations.RunPython(add_<lower>_agent, remove_<lower>_agent)]. - Do NOT edit
0002_populate_db.py— always add a new migration. - If the agent is Multi-Turn-callable, you will ALSO create a SECOND migration in Phase 14 that seeds the
Toolrow forchat_agent_<lower>— note it now. - Run
python Tlamatini/manage.py makemigrations --check --dry-runto confirm no model drift was introduced. - Do not run
migrateyet (batch it in Phase 24 with the Tool-row migration), or run it now and re-run after Phase 14 — either is fine, just end Phase 24 with a clean migrate.
PHASE 8 — Parametrizer registration (make the agent a usable source)
- Open
Tlamatini/agent/agents/parametrizer/parametrizer.py. - Add
'<lower>'to theSECTION_AGENT_TYPESlist (the generic parser handles the rest — no per-agent parser code). - ⚠️ CORRECTED 2026-08-23 — do NOT hand-edit
views.pyfor this.views.PARAMETRIZER_SOURCE_OUTPUT_FIELDSis DERIVED (= get_parametrizer_source_fields()), not a hand-maintained dict, so editing it is either a no-op or a fresh source of drift. Register the fields in ONE place —agent_contracts.py, the next step. - Open
Tlamatini/agent/services/agent_contracts.pyand add'<lower>': (...)to_PARAMETRIZER_OUTPUT_FIELDSwith the same field tuple (this is the registry the Flow-Compiler reads). - Keep the TWO lists coherent:
parametrizer.py::SECTION_AGENT_TYPES(membership) andagent_contracts.py::_PARAMETRIZER_OUTPUT_FIELDS(the field tuple). There is no third list to sync —views.pyderives its copy from the second one. - Add the agent to the Supported Source Agents table in
README.md(Phase 20 sweep, but note the field list now). - If the agent is NOT a Parametrizer source (no INI_SECTION), SKIP 155–160 entirely.
- Confirm
get_agent_contract('<lower>')will resolve (alias-normalized) — if the agent has an alias spelling, add it to the contract'saliases(override inagent_contracts.pybuiltin overrides if needed). - Decide the agent's
AgentContractflags if it needs non-default behavior:singleton,long_running,never_starts_targets,exclude_from_validation,no_input,no_output,special. A normal agent needs none (synthesized default works). - If you add a builtin contract override, set
input_field_by_slot/output_field_by_slotto match the agent's connection shape (slot 2 →target_agents_bfor Forker, etc.). - Add
secret_pathsto the contract for anyconfig.yamldotted path holding a credential (so.flwexport redacts it). - Confirm
connection_fieldson the contract covers every connection key the agent uses (so stale wiring is cleared on recompile). - Re-read the Parametrizer "strict single-lane queue" rule — one source, one target, one-at-a-time — to confirm the agent's section granularity (N results = N sections) matches that model.
- Confirm the agent's
display_nameresolves throughagent_paths.display_name_from_agent_typeto exactly<Display>(centralized capitalization quirks live there).
PHASE 9 — Frontend: CSS coloring (the gradient)
- Open
Tlamatini/agent/static/agent/css/agentic_control_panel.css. - Scan the WHOLE file for existing 4-color gradients to avoid a visual collision.
- Choose a UNIQUE 4-stop gradient (
0% / 33% / 66% / 100%) visually distinct from every existing agent. - Add the rule
.canvas-item.<css>-agent { background-color: #c1; background: linear-gradient(135deg, #c1 0%, #c2 33%, #c3 66%, #c4 100%); color: white; font-size: smaller; }. - Add the hover rule
.canvas-item.<css>-agent:hover { background: linear-gradient(135deg, #c1l 0%, #c2l 33%, #c3l 66%, #c4l 100%); box-shadow: 0 6px 15px rgba(r,g,b,0.5); }. - The gradient must live ONLY in CSS — never type a gradient string in JS.
- Ensure the sidebar icon inherits the gradient via
applyAgentToolIconStyle(iconDiv, '<Display>')(Phase 10) — no per-agent JS branch. - Confirm the CSS class root
<css>matches the JS classMap value you will set (<dash>': '<css>-agent'). - Pick a memorable name for the gradient theme (e.g. "Deep-Ocean Teal") and note it for the memory + commit message.
- If the agent is state-changing, you will MIRROR this gradient in the Exec-Report caption CSS in Phase 15 — keep the primary colors handy.
- Verify the gradient renders by eye after deployment (Phase 25) for BOTH a freshly dragged node and a
.flw-loaded node. - Confirm no existing selector accidentally also matches
.canvas-item.<css>-agent(search for the class root).
PHASE 10 — Frontend JS: connector + acp-canvas-core.js (6 locations)
- Open
Tlamatini/agent/static/agent/js/acp-agent-connectors.js. - Add
async function update<Pascal>Connection(agentId, targetAgentId, action, type = 'target') { ... }modeled on the sibling connector. - Inside it,
fetch('/agent/update_<lower>_connection/${agentId}/', { method:'POST', headers:{'Content-Type':'application/json', ...getHeaders()}, credentials:'same-origin', body: JSON.stringify({ target_agent: targetAgentId, action, type }) }). - Log a
console.erroron a non-ok response and on a thrown error (sibling pattern). - Open
Tlamatini/agent/static/agent/js/acp-canvas-core.js. - Location 1 — classMap (
applyAgentTypeClass(), ~line 32): add'<dash>': '<css>-agent',(KEY is the hyphenated form, VALUE is the CSS class). - Location 2 —
AGENTS_NEVER_START_OTHERS(~line 94): add'<dash>'ONLY if the agent does NOT start downstream (Terminal/Monitoring). Skip for Active agents. - Location 3 —
populateAgentsList()(~line 830): confirm it uses the sharedapplyAgentToolIconStyle(iconDiv, description)— do NOT add a per-agent gradient branch. - Location 4 —
removeConnection()(~line 600): add SPACED-form branchesif (targetAgentName.toLowerCase() === '<space>') update<Pascal>Connection(targetId, sourceId, 'remove', 'source');and the symmetricsourceAgentNamebranch with'remove','target'. - Location 5 —
removeConnectionsFor()(~line 740): add SPACED-form branches with the deletion guards (!targetBeingDeleted/!sourceBeingDeleted). - Location 6 — mouseup handler (~line 1200): add SPACED-form branches with
'add'instead of'remove'. - Use the HYPHENATED form ONLY in the classMap (Location 1) and
AGENTS_NEVER_START_OTHERS(Location 2). - Use the SPACED form (
name.toLowerCase()) in Locations 4, 5, 6 (connection handlers). - Confirm the connector symbol
update<Pascal>Connectionis referenced identically in all locations (case-exact identifier). - If the agent has a special connection shape, mirror the sibling that shares that shape (Forker for A/B, Counter for L/G, etc.) at every location.
- Do NOT forget any of the 6 locations — missing one silently breaks creation, removal, undo, redo, or
.flwload. - Confirm
applyAgentTypeClassis what the canvas calls to set the node's CSS class (so the gradient applies). - Confirm the new branches do not shadow an existing agent whose name is a substring (use exact
===, notincludes). - Re-read the 6 edits as a group to confirm name-form correctness per location.
- Note the connector symbol for the
/* global */declarations in Phase 11.
PHASE 11 — Frontend JS: undo/redo, .flw load, globals
- Open
Tlamatini/agent/static/agent/js/acp-canvas-undo.js. - Find an existing
update<Sibling>Connectionreference and mirror it in the UNDO section (SPACED form,'add'action). - Mirror it again in the REDO section (SPACED form,
'remove'action). - Open
Tlamatini/agent/static/agent/js/acp-file-io.js. - In
restoreAgentConnection's SOURCE-side switch, addcase '<space>': await update<Pascal>Connection(sourceId, targetId, 'add', 'target'); break;. - In
restoreAgentConnection's TARGET-side switch, addcase '<space>': await update<Pascal>Connection(targetId, sourceId, 'add', 'source'); break;. - If the agent persists Parametrizer mappings or other artifacts, confirm
acp-file-io.jsre-hydrates them on.flwload (only relevant if the agent is a Parametrizer node — normal agents need nothing extra). - Add
update<Pascal>Connectionto the/* global ... */declaration at the top ofacp-canvas-core.js. - Add it to the
/* global ... */declaration at the top ofacp-canvas-undo.js. - Add it to the
/* global ... */declaration at the top ofacp-file-io.js. - Open
Tlamatini/eslint.config.mjsand addupdate<Pascal>Connectionto theglobalsblock so the linter knows it. - If the agent introduces any other new global JS symbol, add it to
eslint.config.mjstoo. - Confirm the
.flwload path callsupdateCanvasContentSize()after restoring positions (it does generically — just don't break it). - Confirm no JS edit appends a node to
#submonitor-containerinstead of#canvas-content(coordinate-frame contract). - Re-read all undo/redo/file-io edits as a group for name-form (all SPACED) and action correctness.
PHASE 12 — agentic_control_panel.html: the inputs/outputs connector contract
- Open
Tlamatini/agent/templates/agent/agentic_control_panel.htmland read how agent nodes render (the palette is server-injected from theAgentrows; the label comes fromagentDescriptionverbatim viaconsumers.agent_establishment). - Confirm the sidebar label will render exactly
<Display>(it reads the DB row — no HTML edit needed for the label). - Confirm the node's hover tooltip + canvas Description dialog come from
agents_descriptions.md(the## Workflow Agentstable) parsed intoagent_purpose_map— you will add that row in Phase 20. - Understand the inputs/outputs model: a node's INPUT connectors accept arrows FROM upstream agents (writing into
source_agents), its OUTPUT connectors start arrows TO downstream agents (writing intotarget_agents). - Decide the agent's input/output cardinality and ensure it matches the connection-field shape from Phase 0/6: most agents = 1 input + 1 output; OR/AND = 2 inputs + 1 output; Asker/Forker = 1 input + 2 outputs; Counter = 1 input + 2 outputs (L/G); Starter = 0 input + N outputs; Ender = N inputs + output_agents.
- Confirm the canvas DOM contract: every
.canvas-item, the SVG#connections-layer, and#selection-boxlive inside#canvas-content(the content layer), NOT#submonitor-container(the viewport). - Confirm coordinate math for the node uses
canvasContent.getBoundingClientRect()(already generic — do not special-case the new agent). - Confirm the node's connector dots/handles are produced by the generic canvas-item renderer keyed off the CSS class — a normal agent needs NO bespoke HTML.
- If the agent needs a NON-standard connector layout (e.g. a third output), study how Forker/Counter render their A/B/L/G handles and mirror that EXACTLY (this is the only case that touches connector rendering JS/HTML).
- Verify the node is draggable from the sidebar palette after the migration runs (the palette is populated from
Agentrows at page load). - Verify the node's output connector, when dragged to a target, fires the
update<Pascal>Connection(..., 'add', ...)path (Phase 10, Location 6). - Verify the node's input connector, when receiving an arrow, writes into the correct list (
source_agents) via the view. - Confirm
AGENTS_NEVER_START_OTHERScorrectly suppresses the OUTPUT-starts-downstream behavior for a Terminal agent (the canvas still draws the wire as metadata). - Confirm the node renders inside the scrollable canvas and the canvas grows (no upper clamp) when the node is placed far right/bottom.
- Confirm right-click on the node opens the contextual menu (generic
contextual_menus.js) and the Description entry shows theagent_purpose_maptext. - Confirm double-click / the config entry opens the configuration dialog (Phase 13).
- Do NOT add the agent to any hardcoded HTML list — the palette is dynamic from the DB; only MCP checkboxes are hardcoded (irrelevant here).
- If the new agent must appear in a specific palette CATEGORY/section grouping in the sidebar, check whether
agentic_control_panel.html/its JS groups by category and add the mapping if such grouping exists; otherwise it lists generically.
PHASE 13 — The configuration dialog (canvas node settings)
- Open
Tlamatini/agent/static/agent/js/canvas_item_dialog.jsand read how a node's config dialog is built. - Confirm the dialog is GENERIC: it reads the node's
config.yaml(via the save/load endpoints) and renders a field per key — most agents need NO bespoke dialog code. - Confirm each
config.yamlkey from Phase 1 appears as an editable field with its default pre-filled. - Confirm nested config (e.g.
llm.model) renders with dotted-key fields the dialog understands. - Confirm boolean fields render as checkboxes / true-false controls (match the sibling).
- Confirm the dialog's Save posts to
save_agent_config_view, which DEEP-MERGES the posted JSON over the templateconfig.yaml— so empty fields must be omitted, not written as''(or they destroy defaults). - If the agent needs a SPECIAL dialog widget (a dropdown of enum actions, a file picker, a Parametrizer mapping UI), find the sibling that has it and mirror it; otherwise rely on the generic renderer.
- If the agent is a Parametrizer, wire
acp-parametrizer-dialog.js(only for Parametrizer itself — not a normal new agent). - Confirm the dialog shows the connection fields as read-only/managed (connections are set by dragging wires, not typed in the dialog).
- Confirm credential fields render as empty inputs (never pre-filled with a secret).
- Confirm the dialog title shows
<Display>(it reads the node label). - Verify the dialog round-trips: open → edit a value → Save → reopen shows the new value (Phase 25 live check).
- Confirm Save does not clobber a connection field that was set by wiring (deep-merge + omit-empty protects this).
- If you added a bespoke dialog control, add any new JS global it introduces to
eslint.config.mjsand the/* global */header.
PHASE 14 — Multi-Turn enablement (the wrapped chat-agent tool)
- Decide YES (recommended) to make the agent LLM-callable in Multi-Turn — this is "enable the agent to be Multi-turn".
- Open
Tlamatini/agent/chat_agent_registry.py. - Append a
ChatWrappedAgentSpec(...)toWRAPPED_CHAT_AGENT_SPECS. - Set
key="<lower>". - Set
template_dir="<lower>"(MUST matchagent/agents/<lower>). - Set
tool_name="chat_agent_<lower>"(MUST start withchat_agent_). - Set
tool_description="Chat-Agent-<Display>". - Set
display_name="<Display>"(MUST equal the DBagentDescription). - Set
purpose="..."— a crisp sentence telling the LLM WHEN to use it. - Set
example_request="Run <Display> with param1='...', param2='...'"using the EXACTconfig.yamlkey names (the wrapped parser mapskey=value→ config overrides). - Set
aliases=("<lower>", "<space>", ...)for natural-language matching. - Set
security_hints=(...)with keywords that help capability scoring select it. - Set
poll_window_seconds=Nonly if the default 8 is wrong (short agents can lower it). - Set
long_running=Trueonly for watch-loop agents. - Confirm the spec is picked up via
WRAPPED_CHAT_AGENT_BY_TOOL_NAME— no edits needed inmcp_agent.py/tools.pyfor the launcher itself. - Create a SECOND migration `Tlamatini/agent/migrat
…(truncated)