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).
⚠️ MODIFYING an existing agent rather than creating one?
Most phases below still apply, but four of them are the ones that get skipped and cause silent damage. Run at least these:
- Phase 14 (257b/257c) — if behaviour or a default changed, the wrapped spec's
purpose/example_requestis the ONLY place the model learns it. Leave it stale and you ship a feature that never fires while appearing to work.- Phase 18 (325b) — if the agent's runtime stopped being predictable from its config, the FlowHypervisor will start raising false alarms about it.
- Phase 17 — FlowCreator's
agentic_skill.mdentry carries the config defaults verbatim; a changed default makes that entry a lie, and it is what the flow designer reads when it builds a.flw.- Phase 20 — the doc sweep. Grep a SIBLING AGENT'S NAME, not the old value.
And two habits worth keeping: report what HAPPENED rather than what was REQUESTED (a field that can differ from its input must be computed from the artefact), and APPEND new
INI_SECTIONfields — never rename or reorder the existing ones — keepingagent_contracts._PARAMETRIZER_OUTPUT_FIELDSin step in the same commit.
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
…(truncated)