AISA v2.0 / Technical documentation / agent_map_svg.py
agent_map_svg.py
Python · 496 lines · 41,950 bytes · wiki path 10 Architecture/ui/agent_map_svg.py · download the raw file · cited from Stage Agents and Tools · Decision register
Same folder: agent-catalog.json · agent_catalog.py · build_agent_map.py · build_case_content_docs.py · build_case_journeys.py · build_case_traces.py · build_operations_examples.py · case-content.json · case-journeys.json · case-runtime-traces.json · case-ui-specs.json · case_content.py · case_ui_model.py · frequent-operations.json · memory-capabilities.json · validate_agent_map.py · validate_case_content.py
"""Linked responsibility map: module roots, stage agents and their variants, utility agents, shared common agents, tools, decision gates and precipitation."""
from html import escape
from pathlib import Path
import json,math,textwrap
from case_ui_model import MANIFESTS,LABELS
from build_operations_examples import LINKS
from agent_catalog import ENTITIES,PROFILE_IDS,by_profile,stage_entities,entity_url,stage_brief,node_class,runs_code
X0=60 # left margin
CW=660 # module column pitch
SW=600 # node width — two thirds of the former 880
W=X0+3*CW+SW+X0
ORDER={'support':['S1','route','closure'],'data':['S3','S2','S4','S5','S6','S7'],'configuration':['S1','S2','S3','S4','S5','joint'],'source':['S1','S2','S3','S4'],'commercial':['O1','O2','O3','O4']}
GATES={'H1':'Scope','H2':'Plan','H3':'Result','H4':'Script set','H5':'Execution','H6':'Human action','H7':'Recovery','Hd':'Handover','H5-send':'Customer message','CG-11':'Customer acceptance','HW-*':'Effect','HW-ddl':'DDL','publish':'Publish'}
GATE_LONG={'H1':'Confirm scope','H2':'Approve plan','H3':'Accept result','H4':'Approve release packet','H5':'Permit target change','H6':'Immediate human action or dead end','H7':'Approve recovery','Hd':'Accept handover','H5-send':'Permit customer message','CG-11':'Customer acceptance','HW-*':'Effect-specific permission','HW-ddl':'Permit DDL','publish':'Publish evaluated capability'}
# One look per class, and the class is what the reader is being taught: agent (blue, runs a model), skill (green, followed), tool (slate, code).
# One row per class, and nothing else may invent a colour: fill, border, ink, corner.
# The legend keys carry the same values and a check compares them (validate_agent_map.py).
STYLE={'agent':('#fff','#aac5e7','#245799',12),'skill':('#e7f5ef','#a0cbb9','#24634e',14),
'tool':('#eef2f7','#40546e','#25374d',6)}
COLOURS={k:v[:3] for k,v in STYLE.items()}
# An agent's ROLE is a tone of the same blue, not another class: 'root' is the orchestration agent (the module root, and the
# Support root that owns Stage 1 — owns whether and who), 'agent' is the stage-agent tone (the stage box), and STYLE['agent']
# (white) is every called agent: a utility agent, a declared variant of the stage agent (under a caption), a shared common. agent.
TONE={'agent':('#f0f4f9','#b9cde5','#293e5b'),'root':('#e1eaf6','#7fa4d1','#1d4a7f'),'tool':('#eef2f7','#40546e','#25374d')}
NOTE=('#eef0f3','#c5cfdc','#40546e') # a caption or a procedure note: no profile id, so not an actor
VARIANT_CAPTION={('data','S2'):'Variants of the stage agent · one is selected by symptom domain',('source','S3'):'Variants of the stage agent · one per declared source the change touches'}
MODULE_NAMES={'data':'Data & Manipulation','configuration':'Configuration','source':'Customer-specific development','commercial':'Assessment & Offers'}
MODULE_SUB={'data':'Embedded in this Support case','configuration':'S3 → S4 and S5 may prepare in parallel','source':'Plug-ins, extensions and stored code on the customer lineage','commercial':'Everything else · Core Development is one offer type'}
MODULE_ROOT={'data':'support.root','configuration':'configuration.root','source':'source.root','commercial':'commercial.coordinator'}
MODULE_PAGE={'data':'40-support/data-and-information-module','configuration':'20-configuration/configuration-module','source':'30-source/source-module','commercial':'45-commercial/assessment-and-offers'}
SOURCES=[["Jira ServiceDesk", "ticket system · swept by query", "The customer's Jira Service Management project. Tickets are swept by query and fetched with their comments and attachments; a public comment, internal note or transition is a gated send. Connector seed: the aisa-jira CLI.", "../technical-documentation/10-architecture/40-support/components.html#6-connector-operations"], ["Mailbox", "shared mailbox · Inbox scanned", "The shared service mailbox. The Inbox is scanned on a schedule, a thread is fetched as one arrival and deduplicated against its ticket mirror; a reply is a gated send. Connector seed: the aisa-mail CLI.", "../technical-documentation/10-architecture/40-support/components.html#6-connector-operations"], ["Serdica UI", "client surface · authenticated push", "A request started in place on the client surface. Its message box is a door of that surface, not a separate channel: the arrival is pushed with the authenticated identity and the initiation right is checked before a case opens.", "../technical-documentation/10-architecture/50-agents/agents.html#5c-who-routes-an-arrival-before-any-module-owns-it"], ["Other declared connectors", "declared per customer plug-in", "Any other channel the customer plug-in declares — another ticket system, a portal, a queue — arrives through the same mechanism under its own connector row and grants. Channels are configuration, not architecture.", "../technical-documentation/10-architecture/whitelabel-catalogue.html#2-channels-and-customer-visible-behaviour-customer-plug-in"]]
NUM={'joint':'6','release':'7'}
EDGE={'flow':'#7e97b5','return':'#967aaf','precip-flow':'#8a6bb0'} # the three edge classes; the legend's flow and return keys carry the same two colours
PRECIP_PAGE='../technical-documentation/10-architecture/40-support/stage-precipitation.html'
svg=[]
BOXES=[]
OPERATIONS={o['id']:o for o in json.loads((Path(__file__).parent/'frequent-operations.json').read_text(encoding='utf-8'))['operations']}
CAPS=json.loads((Path(__file__).parent/'memory-capabilities.json').read_text(encoding='utf-8'))
SAMPLES={'support.investigator.transfers':'op.transfer.stuck','support.investigator.pricing':'op.pricing.policy-components','support.investigator.printing':'op.printing.source-data'}
def add(s):svg.append(s)
def rect(x,y,w,h,fill,stroke='none',rx=12,extra=''):
add(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{rx}" fill="{fill}" stroke="{stroke}" {extra}/>')
def txt(x,y,s,size=18,fill='#40546f',weight=400):
add(f'<text x="{x}" y="{y}" font-size="{size}" fill="{fill}" font-weight="{weight}">{escape(str(s))}</text>')
def wrapped(x,y,s,w,size=17,limit=None,fill='#40546f',leading=23,factor=.56):
parts=textwrap.wrap(s,max(12,int(w/(size*factor))))
if limit and len(parts)>limit:parts=parts[:limit];parts[-1]=parts[-1].rstrip(' .,;')+'…'
for i,line in enumerate(parts):txt(x,y+i*leading,line,size,fill)
return len(parts)*leading
def path(d,kind='flow'):
add(f'<path d="{d}" class="am-{kind}"/>')
def link(href,hint,kind,entity=None,title=None,extra=''):
attr=f' data-entity="{entity["id"]}"' if entity else ''
t=title or (entity['label'] if entity else hint.split('.')[0])
add(f'<g data-href="{escape(href,quote=True)}" data-title="{escape(t,quote=True)}" class="am-node am-{kind}" role="button" tabindex="0" data-map-hint="{escape(hint,quote=True)}" aria-label="{escape(t,quote=True)}"{attr}{extra}><title>{escape(hint)}</title>')
def end():add('</g>')
def cap_label(e):return e['id'] # the `common.` namespace already says a definition is shared
def stage_no(module,sid):
d=''.join(ch for ch in sid if ch.isdigit());return d or NUM.get(sid,'')
def stage_title(module,sid):
if module=='support' and sid in ('S1','route'):return 'Stage 1: Triage, classification and route'
n=stage_no(module,sid);return ('Stage '+n+': ' if n else 'Stage: ')+LABELS[module][sid]
def consult_line(e):
cs=e.get('consults',[])
return '↔ may consult · '+' · '.join(c['domain']+(' →Hd' if c.get('handover') else '') for c in cs) if cs else ''
def runs_text(e):
"""The one line that says what this box is; the badges count what it runs and the board lists the names."""
if node_class(e)=='tool':return 'tool · code with a contract · no model'
bits=['agent · runs a model']
if e.get('consults'):bits.append(consult_line(e).replace('↔ may consult · ','↔ '))
return ' · '.join(bits)
def runs_wrap(e,w,size=14):return textwrap.wrap(runs_text(e),max(12,int(w/(size*.56))))[:2]
def runs_width(e,w):return w-36-(badge_w(e)+12 if badge_w(e) else 0) # the runs line stops short of the badges
def tool_set(e):
"""The tools an agent reaches: its own, and those of the skills it runs."""
s=set(e.get('tools') or [])
for k in e.get('skills') or []:s.update(ENTITIES[k].get('tools') or [])
return s
def badge_w(e):
if node_class(e)!='agent':return 0
n_s,n_t=len(e.get('skills') or []),len(tool_set(e))
return (22+4+7*len(str(n_s))+(10 if n_t else 0) if n_s else 0)+(14+4+7*len(str(n_t)) if n_t else 0)
def badges(e,x_right,y):
"""Top right of every agent node: the legend's own miniatures — a green pill with the count of skills it runs,
a slate square with the count of tools it reaches. The names read in the board."""
if node_class(e)!='agent':return 0
n_s,n_t=len(e.get('skills') or []),len(tool_set(e));x=x_right-badge_w(e)
if n_s:
rect(x,y,22,14,STYLE['skill'][0],STYLE['skill'][1],7,extra='class="am-badge am-badge-skill"');txt(x+26,y+12,n_s,12,STYLE['skill'][2],600);x+=22+4+7*len(str(n_s))+10
if n_t:
rect(x,y,14,14,STYLE['tool'][0],STYLE['tool'][1],2,extra='stroke-width="2" class="am-badge am-badge-tool"');txt(x+18,y+12,n_t,12,STYLE['tool'][2],600)
return badge_w(e)
def shared_caption(shared):
"""The shared (common.) definitions this stage also calls, and what class each of them is."""
n={}
for e in shared:n[node_class(e)]=n.get(node_class(e),0)+1
return 'Shared (common.) it also calls · '+' · '.join(str(v)+' '+k+('s' if v>1 else '') for k,v in n.items())
def stage_decl(module,sid):return next(s for s in MANIFESTS[module]['stages'] if s['id']==sid)
def visible_agents(module,sid):
"""owner · variants (the stage agent under another selection) · utility agents (module-owned) · shared agents (common.)"""
choices=stage_entities(module,sid)
owner=choices[0]
variant_ids={PROFILE_IDS[p] for p in (stage_decl(module,sid).get('profile_variants') or {}).values()}-{owner['id']}
variants=[e for e in choices[1:] if e['id'] in variant_ids]
utility=[e for e in choices[1:] if e['id'] not in variant_ids and not e['id'].startswith('common.')]
shared=[e for e in choices[1:] if e['id'].startswith('common.')]
return owner,variants,utility,shared
def stage_url(module,sid):
return '../technical-documentation/10-architecture/'+LINKS[module][sid]+'.html'+('#stage-'+sid.lower() if module=='commercial' else '#stage-'+module+'-'+sid.lower())
# ---------------------------------------------------------------- utility agents and chips
def compact_height(e,sample=None):return 78+(28 if sample else 0)
def compact(e,x,y,w,sample=None,sub=None,role='utility',stage_id=None):
"""One actor box in the look of its class and role: a called agent (white — utility, declared variant or shared),
an orchestration agent (deep blue) or a tool (slate, 2 px border). Skills and tools: badges top right, names in the board."""
h=compact_height(e,sample)
cls=node_class(e)
if cls=='agent' and role=='root':kind,(fill,stroke,ink),rx='root',TONE['root'],16
else:kind,(fill,stroke,ink,rx)=cls,STYLE[cls]
link(entity_url(e['id']),stage_brief(e),kind,e,extra=f' data-stage="{stage_id}" data-variant="1"' if role=='variant' else '')
rect(x,y,w,h,fill,stroke,rx,extra='stroke-width="2"' if cls=='tool' else '')
txt(x+16,y+25,cap_label(e),17,ink,600)
txt(x+16,y+47,sub or e['label'],14,'#55769b')
wrapped(x+16,y+67,runs_text(e),w-32,13,limit=1,leading=16,fill='#6b7d92')
badges(e,x+w-16,y+10)
end()
if sample:
op=OPERATIONS[sample]
href='../technical-documentation/10-architecture/40-support/frequent-operations.html#'+sample.replace('.','-')
link(href,op['trigger']+' Required result: '+op['outcome'],'note')
txt(x+16,y+h-9,'Example: '+op['name']+' →',14,'#47776e');end()
BOXES.append({'kind':'agent','id':e['id'],'x':x,'y':y,'w':w,'h':h})
return h
def cap_height(e,w):return max(32,18*len(textwrap.wrap(cap_label(e),max(12,int((w-30-badge_w(e))/8.4)))))+10
def capability(e,x,y,w):
"""A shared (common.) agent the stage also calls: the same white box, id only, badges top right."""
cls=node_class(e);fill,stroke,ink,rx=STYLE[cls];h=cap_height(e,w)
link(entity_url(e['id']),stage_brief(e),cls,e)
rect(x,y,w,h,fill,stroke,rx)
wrapped(x+12,y+23,cap_label(e),w-30-badge_w(e),15,fill=ink,leading=18)
badges(e,x+w-10,y+9)
end();return h
# ---------------------------------------------------------------- intake
def sources_row(x,y,w):
"""The declared sources: each a node of its own, feeding the one intake mechanism."""
n=len(SOURCES);gap=30;bw=(w-gap*(n-1))/n;h=76
for i,(name,sub,hint,href) in enumerate(SOURCES):
bx=x+i*(bw+gap)
link(href,name+'. '+hint,'note')
rect(bx,y,bw,h,'#f5f7fa','#98a6b7',12)
txt(bx+18,y+30,name,19,'#34445a',600)
txt(bx+18,y+55,sub,14,'#5c6e85');end()
path(f'M{bx+bw/2} {y+h} V{y+h+36}')
return h+36
def intake_node(e,x,y,w):
link(entity_url(e['id']),stage_brief(e),node_class(e),e)
rect(x,y,w,142,*STYLE['tool'][:2],STYLE['tool'][3],extra='stroke-width="2"')
txt(x+24,y+35,'Connector intake · poll or push',26,STYLE['tool'][2],600)
wrapped(x+24,y+72,'Validate source → deduplicate revision → persist arrival → start or resume the case → wake Support root',w-48,18,leading=24)
txt(x+24,y+122,'Tool · code with a contract: normalisation and case start or resume; no model, no business classification',15,'#5c6e85');end();return 142
# ---------------------------------------------------------------- gates
def gates_for(module,sid):
d=next(s for s in MANIFESTS[module]['stages'] if s['id']==sid)
values=[(g['kind'],g['when'].replace('_',' '),False) for g in d['gates']]
extra=[]
if module=='data' and sid=='S5':extra=['HW-*','H5-send']
if module=='configuration' and sid in ('S2','S3','S4','S5'):extra=['HW-*']
if module=='source' and sid=='S7':extra=['HW-ddl']
if clarifies(module,sid):extra+=['H6','H5-send']
reason={'H6':'the stage cannot start — missing material or authority — or the incident cannot wait','H5-send':'the clarification message is customer-visible'}
return values+[(key,reason.get(key,'only when this effect is selected'),True) for key in extra if key not in [v[0] for v in values]]
def gate_url(key):return '../technical-documentation/10-architecture/gating.html#gate-'+key.lower().replace('*','all')
def gate_node(key,event,x,y,w,conditional=False,h=56,hint=None):
hint=hint or key+': '+GATE_LONG.get(key,key)+'. '+event+'. Recorded decision, answered by the assigned role or an explicitly eligible policy; never by task content. A person can intervene at every H gate.'
link(gate_url(key),hint,'gate',title=key+' · '+GATE_LONG.get(key,key))
rect(x,y,w,h,'#fff5dc','#dbb464',7,extra='stroke-dasharray="5 4"' if conditional else '')
add(f'<path d="M{x+16} {y+11} l9 9 -9 9 -9 -9 Z" fill="#f7d886" stroke="#ae8130"/>')
txt(x+32,y+25,key,15,'#725016',600)
wrapped(x+10,y+45,GATES.get(key,key),w-16,12.5,limit=1,leading=16,fill='#88652a',factor=.6)
end()
def clarifies(module,sid):
return bool(next(s for s in MANIFESTS[module]['stages'] if s['id']==sid).get('clarification'))
def precipitates(module,sid):
return bool(next(s for s in MANIFESTS[module]['stages'] if s['id']==sid).get('precipitates'))
def marked_stages():
return [(m,s['id']) for m in MODULE_NAMES for s in MANIFESTS[m]['stages'] if s.get('precipitates')]
def marked_summary():
by={}
for m,sid in marked_stages():by.setdefault(m,[]).append(stage_no(m,sid) or sid)
return ' · '.join(MODULE_NAMES[m]+' '+' and '.join(v) for m,v in by.items())
def precip_chip_w():return 82
def precip_chip(x,y,h=48):
"""The precipitation mark: same place as a decision checkpoint, purple, and asynchronous."""
link(PRECIP_PAGE+'#3-the-closing-walk','Precipitation event: this stage files its experience entry and what was missing as a proposal, then moves on. Asynchronous — the case never waits for it — and it feeds the precipitation workflow at the foot of this map.','precip',title='Precipitation event · asynchronous',extra=' data-mark="event"')
rect(x,y,precip_chip_w(),h,'#f8f3fe','#a98cc9',7,extra='stroke-dasharray="5 4"')
add(f'<path d="M{x+13} {y+8} q6 7 6 11 a6 6 0 0 1 -12 0 q0 -4 6 -11 Z" fill="#e5d8f4" stroke="#7d5da8"/>')
txt(x+24,y+21,'Precip',13.5,'#5b3f7a',600)
txt(x+8,y+39,'learning',11,'#7a63a0')
end();return precip_chip_w()
def gate_chip_w(key):return max(50,math.ceil(len(GATES.get(key,key))*11*.58)+14,int(len(key)*8.6)+30)
def marks_block_w(gs,precip=False):
w=sum(gate_chip_w(k) for k,_,_ in gs)+6*(len(gs)-1) if gs else 0
return w+(precip_chip_w()+(6 if gs else 0)) if precip else w
def title_w(title,size=20):return int(len(title)*size*.56)+8
def marks_fit(title,gs,w,fqdn='',precip=False):
"""The marks sit beside the title and fqdn lines when they all fit the node width; otherwise they take their own row."""
return (bool(gs) or precip) and max(title_w(title),title_w(fqdn,16))+marks_block_w(gs,precip)+16<=w-36
def gate_chip(key,event,x,y,conditional=False,hint=None):
w=gate_chip_w(key);h=48
hint=hint or key+': '+GATE_LONG.get(key,key)+'. '+event+'. Recorded decision, answered by the assigned role or an explicitly eligible policy; never by task content. A person can intervene at every H gate.'
link(gate_url(key),hint,'gate',title=key+' · '+GATE_LONG.get(key,key))
rect(x,y,w,h,'#fff5dc','#dbb464',7,extra='stroke-dasharray="5 4"' if conditional else '')
add(f'<path d="M{x+13} {y+10} l6 6 -6 6 -6 -6 Z" fill="#f7d886" stroke="#ae8130"/>')
txt(x+24,y+21,key,13.5,'#725016',600)
wrapped(x+8,y+39,GATES.get(key,key),w-12,11,limit=1,leading=14,fill='#88652a',factor=.58)
end();return w
def marks_right(gs,x_right,y,precip=False):
"""Right-aligned checkpoint chips, the precipitation mark last, ending at x_right."""
x=x_right-marks_block_w(gs,precip)
for key,event,cond in gs:x+=gate_chip(key,event,x,y,cond)+6
if precip:precip_chip(x,y)
def gate_row(gs,x,y,w):
"""Every gate of a stage on one lane."""
n=len(gs);gap=8;gw=(w-gap*(n-1))/n
for i,(key,event,cond) in enumerate(gs):gate_node(key,event,x+i*(gw+gap),y,gw,cond)
return 56
# ---------------------------------------------------------------- the stage agent
def operation_inventory(x,y,w):
href='../technical-documentation/10-architecture/40-support/frequent-operations.html#operation-inventory'
link(href,'Full source-grounded list: request triggers, fresh evidence, selected domain profiles, skill variants, tool candidates and sources. An operation does not automatically become a new agent.','note',title='Recurring-operation inventory')
rect(x,y,w,64,*NOTE[:2],10)
txt(x+16,y+26,'Browse the recurring-operation inventory →',17,'#28675c',600)
txt(x+16,y+50,'Examples here; full evidence and automation candidates on the technical page.',14,'#47776e');end()
def caption(s,x,y):
"""A group caption inside a stage: it says what the boxes below it are."""
txt(x,y+14,s,14,'#426891',600);return 26
def variant_height(variants):return 26+sum(compact_height(e,SAMPLES.get(e['id']))+12 for e in variants)
def variant_block(module,sid,variants,x,y,w):
"""The declared profile variants of the stage agent: white boxes like any called agent, under a caption that says the case selects one."""
yy=y+caption(VARIANT_CAPTION.get((module,sid),'Stage agent variants · selected per case'),x,y)
for e in variants:yy+=compact(e,x,yy,w,SAMPLES.get(e['id']),role='variant',stage_id=module+'/'+sid)+12
return yy-y
def investigation_height(variants,utility):
return 132+sum(compact_height(e)+12 for e in utility)+variant_height(variants)+76
def investigation_samples(x,y,w,variants,utility):
yy=y
link(stage_url('data','S2'),'No usable experience is a normal investigation mode. Establish the actual failure from current evidence and source, compare a valid peer, test separating hypotheses, and request missing evidence or a human-owned action through root.','note',title='Fresh investigation')
rect(x,yy,w,118,*NOTE[:2],10)
txt(x+16,yy+28,'No match, stale match, or contradicted experience',19,'#295c73',600)
wrapped(x+16,yy+56,'Fresh investigation: request → failure evidence → source/data path → hypotheses → independent mechanism verdict.',w-32,15,leading=20)
txt(x+16,yy+104,'Unknown domain starts here; a known domain uses its own investigator.',14,'#496f82');end();yy+=132
for e in utility:yy+=compact(e,x,yy,w)+12
yy+=variant_block('data','S2',variants,x,yy,w)
operation_inventory(x,yy,w);return yy+76-y
def stage_header_height(owner,w):
"""title, fqdn, two description lines, then the one line that says what it is (badges to its right count what it runs)"""
return 118+18*len(runs_wrap(owner,runs_width(owner,w)))
def stage_height(module,sid,w):
owner,variants,utility,shared=visible_agents(module,sid)
h=stage_header_height(owner,w)
if module=='data' and sid=='S2':h+=investigation_height(variants,utility)+12
else:
h+=sum(compact_height(e)+12 for e in utility)
if variants:h+=variant_height(variants)
if module=='commercial' and sid=='O1':h+=58
if module=='configuration' and sid in ('S2','S3','S4','S5'):h+=58
if shared:h+=30+((len(shared)+1)//2)*46
gs=gates_for(module,sid);pr=precipitates(module,sid)
if (gs or pr) and not marks_fit(stage_title(module,sid),gs,w,cap_label(owner),pr):h+=28+56
return h+14
def stage(module,sid,x,y,w,ids=None,tone=None):
"""One stage box = the stage agent: its identity and description, then its utility agents, shared services and gates."""
h=stage_height(module,sid,w);d=stage_decl(module,sid)
href=stage_url(module,sid);owner,variants,utility,shared=visible_agents(module,sid)
title=stage_title(module,sid);fill,stroke,ink=tone or TONE[node_class(owner)]
link(href,d['done'],'stage',owner,title=title,extra=f' data-stage="{module}/{sid}" data-done="{escape(d["done"],quote=True)}"')
rect(x,y,w,h,fill,stroke,16)
txt(x+18,y+32,title,20,ink,600)
txt(x+18,y+57,cap_label(owner),16,COLOURS[node_class(owner)][2],600)
wrapped(x+18,y+80,owner['purpose'],w-36,15,limit=2,leading=19,fill='#40546f')
yy=y+118
gs=gates_for(module,sid);pr=precipitates(module,sid);on_title=marks_fit(title,gs,w,cap_label(owner),pr)
for i,ln in enumerate(runs_wrap(owner,runs_width(owner,w))):txt(x+18,yy+2+i*18,ln,14,'#2f6f7e' if node_class(owner)=='agent' else '#4a5c70') # teal for a consult line, slate for code
yy+=18*len(runs_wrap(owner,runs_width(owner,w)))
badges(owner,x+w-18,y+108)
end()
if module=='data' and sid=='S2':yy+=investigation_samples(x+18,yy,w-36,variants,utility)+12
else:
for e in utility:yy+=compact(e,x+18,yy,w-36)+12
if variants:yy+=variant_block(module,sid,variants,x+18,yy,w-36)
if module=='commercial' and sid=='O1':
# the same commercial.assessor task from clarification to the complete assessment: a phase note, not a second actor
link(href,'Clarification, evidence inspection, domain consults and the effort estimate are phases of one commercial.assessor task; the complete assessment returns to the offer preparer.','note',title='One assessor task')
rect(x+18,yy,w-36,46,*NOTE[:2],10)
txt(x+30,yy+20,'One assessor task: clarify → inspect → consult → estimate',16,'#40546f',600)
txt(x+30,yy+38,'Same task throughout; the complete assessment goes to the offer preparer.',13);end();yy+=58
if module=='configuration' and sid in ('S2','S3','S4','S5'):
link(href,'Clarification and plan synthesis are phases of the same domain task. Immediate parent confirms preparation; H2 and effect gates precede action; independent domain verdict precedes H3.','note',title='Stage phases')
rect(x+18,yy,w-36,46,*NOTE[:2],10)
txt(x+30,yy+20,'Clarify / plan → confirm → coordinate → domain verdict',16,'#40546f',600)
txt(x+30,yy+38,'A revisitable stage, with selected skills and tools and preserved context.',13);end();yy+=58
if shared:
txt(x+18,yy+8,shared_caption(shared),14,'#426891',600);yy+=22
for i,e in enumerate(shared):capability(e,x+18+(i%2)*((w-36)/2+8),yy+(i//2)*46,(w-36)/2-4)
yy+=((len(shared)+1)//2)*46+8
if on_title:marks_right(gs,x+w-18,y+10,pr)
elif gs or pr:
txt(x+18,yy+8,'Decision checkpoints'+(' and precipitation' if pr else ''),14,'#826229',600);yy+=20
rw=w-36-(precip_chip_w()+8 if pr else 0)
if gs:gate_row(gs,x+18,yy,rw)
if pr:precip_chip(x+18+rw+(8 if gs else 0),yy,56)
for bid in (ids or [module+'/'+sid]):BOXES.append({'kind':'stage','id':bid,'x':x,'y':y,'w':w,'h':h})
return h
def root_stage(x,y,w):
"""Stage 1 of the desk: triage, classification and the route are one node, owned by support.root — the orchestration agent, so it wears the root tone."""
root=ENTITIES['support.root'];s1=next(s for s in MANIFESTS['support']['stages'] if s['id']=='S1');rt=next(s for s in MANIFESTS['support']['stages'] if s['id']=='route')
done=s1['done']+' '+rt['done']
gs=gates_for('support','S1')+[g for g in gates_for('support','route') if g[0] not in [v[0] for v in gates_for('support','S1')]]
on_title=marks_fit(stage_title('support','S1'),gs,w,cap_label(root))
h=118+(22 if root.get('consults') else 0)+40+(0 if on_title else 28+56)+14
link(stage_url('support','route'),done+' A plan for a different scope is redirected at H2; a person can intervene at every H gate.','root',root,title=stage_title('support','S1'),extra=f' data-stage="support/S1+route" data-done="{escape(done,quote=True)}"')
rect(x,y,w,h,*TONE['root'][:2],16)
txt(x+18,y+32,stage_title('support','S1'),20,TONE['root'][2],600)
txt(x+18,y+57,cap_label(root),16,'#245799',600)
wrapped(x+18,y+80,'Triage: stamps the request type, the contract class and priority (§ 1 Standard Sev 1–4 or § 2 Personalization P1–3), the SLA clocks and the scope; one bounded classification question may go to the adviser on the right.',w-36,15,limit=2,leading=19)
yy=y+118
if root.get('consults'):wrapped(x+18,yy+2,consult_line(root),runs_width(root,w),14,limit=1,leading=18,fill='#2f6f7e');yy+=22
wrapped(x+18,yy+16,'Route: Data or Assessment work here, Configuration or Customer-specific development as an accepted sub-case, or an explicit wait. Redirected at H2 when the scope differs; missing information or an urgent incident is raised by the stage that meets it, through its optional clarification agent.',w-36,13,limit=2,leading=17);yy+=40
badges(root,x+w-18,y+108)
end()
if on_title:marks_right(gs,x+w-18,y+10)
else:
txt(x+18,yy+8,'Decision checkpoints',14,'#826229',600);yy+=20
gate_row(gs,x+18,yy,w-36)
for bid in ('support/S1','support/route'):BOXES.append({'kind':'stage','id':bid,'x':x,'y':y,'w':w,'h':h})
return h
def adviser_node(x,y,w):
"""The classification adviser: an internal sub-agent call from Stage 1, never another exit."""
e=ENTITIES['support.triage']
h=118
link(entity_url(e['id']),stage_brief(e),'agent',e,title='Contract and priority classifier')
rect(x,y,w,h,*STYLE['agent'][:2],STYLE['agent'][3])
txt(x+18,y+32,'Contract and priority classifier',20,STYLE['agent'][2],600)
txt(x+18,y+57,cap_label(e),15,'#55769b',600)
wrapped(x+18,y+80,'Answers one bounded question from Stage 1 — contract class, severity or priority, scope — with the evidence for and against, and returns a recommendation. It never owns the case, its clocks or the route.',w-36,14,limit=2,leading=18)
badges(e,x+w-18,y+10)
end()
BOXES.append({'kind':'agent','id':e['id'],'x':x,'y':y,'w':w,'h':h})
return h
def closure_strip(x,y,w):
"""The common end of every lane: the verified result returns to the root, is sent through the gate and accepted by the customer."""
d=next(s for s in MANIFESTS['support']['stages'] if s['id']=='closure');e=ENTITIES['common.communicator']
h=126
link(stage_url('support','closure'),d['done'],'stage',e,title='Stage: Closure — the common end of every lane',extra=f' data-stage="support/closure" data-done="{escape(d["done"],quote=True)}"')
rect(x,y,w,h,*TONE['agent'][:2],16)
txt(x+20,y+34,'Stage: Closure — the common end of every lane',24,TONE['agent'][2],600)
txt(x+20,y+60,cap_label(e)+' drafts; the root sends; the customer accepts',16,'#245799',600)
wrapped(x+20,y+84,'Every verified result — a repaired policy, a previewed product, a deployed change, a communicated offer — returns to the controlling root. The reply is a gated send; the ticket moves to Pending; the customer\'s acceptance completes the case. Delivering a reply is not acceptance.',w-720,14,limit=2,leading=18)
badges(e,x+w-20,y+72)
end()
marks_right(gates_for('support','closure'),x+w-20,y+14)
BOXES.append({'kind':'stage','id':'support/closure','x':x,'y':y,'w':w,'h':h})
return h
def envelope(module,x,y,w,stages,positions):
heights=[stage_height(module,sid,w-36) for sid in stages]
header=172
h=header+sum(heights)+max(0,len(stages)-1)*48+24
href=stage_url(module,stages[0])
title='Delivery follow-through'
hint='After authoring and working proof, prepare the deployment scripts/API/SQL packet, confirm it, execute within current authority and verify the target. Required delivery remains open until these obligations are met.'
link(href,hint,'note',title=title);rect(x,y,w,h,'#eef6f5','#93beb5',18,extra='stroke-width="2" stroke-dasharray="10 6"');end()
txt(x+20,y+34,title,20,'#2f6b60',600)
wrapped(x+20,y+62,hint,w-40,15,leading=20,limit=4)
txt(x+20,y+152,'API requests · SQL · artefacts · confirmations · proof',15,'#376f65',600)
yy=y+header
for sid,sh in zip(stages,heights):
stage(module,sid,x+18,yy,w-36);positions[(module,sid)]=(x+18,yy,sh)
yy+=sh+48
BOXES.append({'kind':'envelope','id':module+'/delivery','x':x,'y':y,'w':w,'h':h})
return y+h
def precip_node(x,y,w,title,tag,body,href,hint,h=132,gate=None,chip=False):
"""One step of the asynchronous precipitation workflow."""
link(href,hint,'precip',title=title)
rect(x,y,w,h,'#faf7fd','#a98cc9',14,extra='stroke-width="1.6"')
txt(x+18,y+32,title,20,'#5b3f7a',600)
txt(x+18,y+56,tag,15,'#7d63a0',600)
wrapped(x+18,y+80,body,w-36,14,limit=3,leading=18,fill='#5c5170')
end()
if chip:precip_chip(x+w-18-precip_chip_w(),y+10)
if gate:marks_right([gate],x+w-18,y+10)
return h
def precipitation_workflow(y):
"""The marked stages raise events; a scheduled pass summarises; the curator creates; a person seeds it for everyone."""
cols=[X0+i*CW for i in range(4)];ny=y+92;h=326
link(PRECIP_PAGE+'#purpose','Precipitation is the case paying for itself: the marked stages file what they learned, a scheduled pass summarises it, the curator turns it into facts, skills and tested tools, and a person seeds it for everyone. All of it runs after the workflow; none of it holds a case open.','precip',title='Precipitation · asynchronous')
rect(X0-18,y,W-2*X0+36,h,'#f7f3fc','#a98cc9',18,extra='stroke-width="2" stroke-dasharray="10 6"');end()
txt(X0,y+40,'Precipitation · asynchronous · one workflow for every lane',24,'#5b3f7a',600)
wrapped(X0,y+68,'Nothing here holds a case open, and nothing here is active until a person seeds it.',W-2*X0,15,limit=1,leading=20,fill='#6f5c8f')
precip_node(cols[0],ny,SW,'Precipitation events','raised by the marked stages, case-locally',
marked_summary()+' · each files its experience entry, and what was missing, as a proposal and moves on.',
PRECIP_PAGE+'#3-the-closing-walk','The closing walk of a marked stage, reconciled like a plan: the known-issue register, the living checklists and the precipitation decision. The entry is written where the context still exists, and filed as a proposal.',chip=True)
precip_node(cols[1],ny,SW,'Scheduled summarisation','the dreaming pass · a person sets the schedule',
'Separate from any case: merge duplicates, re-read each entry against the would-this-help-a-stranger test, prune what the running system has outgrown and mark contradictions.',
PRECIP_PAGE+'#6-consolidation-the-dreaming-pass','The consolidation pass runs on a schedule, separate from case closing. It catches the entry that only made sense to someone who was there and rewrites it for a reader who was not. Measured by the same triage hit-rate as migration.')
stage('data','S8',cols[2],ny,SW)
precip_node(cols[3],ny,SW,'Seed it for everyone','publish · always a person',
'The curator proposes; the role holder publishes. Only then is the fact, skill or tool active for the next case, with its version pinned and its dependent evaluations re-run.',
'../technical-documentation/10-architecture/50-agents/agents-memory.html#74b-publication-memory-is-governed-not-just-written','Publication is governed, not just written: proposed → active → retired, provenance on every write, version pinning per case and dependency-scoped evaluation re-runs. Until a person publishes it, a proposal changes nothing.',
gate=('publish','the curator proposes a fact, a skill or a tested tool for every case',False))
for i in range(3):path(f'M{cols[i]+SW} {ny+56} H{cols[i+1]}','precip-flow')
runtime=[c for c in CAPS['capabilities'] if c['kind'] in ('tool','skill','agent') and c['domain']!='workspace']
n_run=len(runtime);n_cand=len([c for c in runtime if c['status']!='existing'])
cy=ny+146
link('../technical-documentation/10-architecture/50-agents/memory-derived-capabilities.html#0-census',f'{n_run} runtime capabilities the v1 memory could become — tools, skills and one agent — inventoried one line each with seeds, owner and evidence; {n_cand} are still candidates. Each agent\'s board lists its own.','note',title='Memory-derived capabilities')
rect(cols[3],cy,SW,64,'#fff','#a98cc9',10,extra='stroke-dasharray="6 4"')
txt(cols[3]+16,cy+26,f'{n_run} sub-agents, skills and tools from v1 memory →',16,'#5b3f7a',600)
txt(cols[3]+16,cy+48,f'{n_cand} candidates, one line each; per agent in the board',13.5,'#7a63a0');end()
return y+h
def build_svg():
svg.clear();BOXES.clear()
add('<defs><marker id="am-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10" fill="none" stroke="#8195ae" stroke-width="1.4"/></marker><marker id="am-arrow-p" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10" fill="none" stroke="#8a6bb0" stroke-width="1.4"/></marker></defs>')
# three edge classes, all drawn: flow (solid), return or rework (dashed), precipitation flow (dashed purple). A consult is a line of node text, never an edge.
add('<style>.agent-map-svg{font-family:"Segoe UI",Arial,sans-serif}.am-flow,.am-return{fill:none;stroke:'+EDGE['flow']+';stroke-width:2;marker-end:url(#am-arrow)}.am-return{stroke:'+EDGE['return']+';stroke-dasharray:8 6}.am-precip-flow{fill:none;stroke:'+EDGE['precip-flow']+';stroke-width:2;stroke-dasharray:8 6;marker-end:url(#am-arrow-p)}.am-node{text-decoration:none;cursor:pointer}.am-node:hover>rect,.am-node:focus>rect{stroke:#0b57d0;stroke-width:3}.am-node:focus{outline:none}</style>')
top=30
mid_x=X0+CW;mid_w=2*CW # the root column spans the two middle lanes
sh_=sources_row(mid_x,top,mid_w)
ih=intake_node(by_profile('platform.intake'),mid_x,top+sh_,mid_w);root_y=top+sh_+ih+50
path(f'M{mid_x+mid_w/2} {top+sh_+ih} V{root_y}')
txt(mid_x+14,root_y-14,'Support root · one task and one evolving case brief',20,'#24486b',600)
rx,rw=mid_x+120,mid_w-240
# the clarification agent is not here: the stage that cannot start is the one that asks (D186)
rh=root_stage(rx,root_y,rw)
# right: the classification adviser — an internal sub-agent call, like Stage 3 ↔ Stage 2
tx=X0+3*CW
th=adviser_node(tx,root_y,SW)
path(f'M{rx+rw} {root_y+44} H{tx}','return');txt(rx+rw+8,root_y+36,'bounded question',11,'#7d6298')
path(f'M{tx} {root_y+96} H{rx+rw}','return');txt(rx+rw+8,root_y+112,'recommendation',11,'#7d6298')
branch_y=max(root_y+rh,root_y+th)+56
start=branch_y+96
module_x={m:X0+i*CW for i,m in enumerate(MODULE_NAMES)}
owner_heights=[]
for module,x in module_x.items():
path(f'M{rx+rw/2} {root_y+rh} V{branch_y} H{x+SW/2} V{start}')
link('../technical-documentation/10-architecture/'+MODULE_PAGE[module]+'.html#agents-in-this-module',MODULE_NAMES[module]+' module overview, full expandable agent descriptions and owned technical subpages.','module',title=MODULE_NAMES[module])
txt(x+12,start-44,MODULE_NAMES[module],26,'#24486b',600)
txt(x+12,start-16,MODULE_SUB[module],15,'#5c7490');end()
owner_heights.append(compact(by_profile(MODULE_ROOT[module]),x,start,SW,sub='orchestration agent · owns whether and who; a stage owns how' if module!='data' else 'the same Support root, orchestration agent; Data runs inside the case',role='root'))
consult_y=start+max(owner_heights)+30
link('../technical-documentation/10-architecture/50-agents/agents.html#5-how-the-modules-reach-each-other','Domain-agent consults use the runtime protocol. Return evidence and open questions with source revisions. Consulting another module does not grant its write permissions or start implementation; a consult that needs a change becomes a handover (Hd) raised by the root.','note',title='Domain-agent consults')
rect(X0,consult_y,W-2*X0,92,'#ecf7f8','#b3d3db',14)
txt(X0+24,consult_y+32,'↔ Domain-agent consults · runtime protocol',21,'#275f70',600)
wrapped(X0+24,consult_y+60,'A stage agent may ask another domain\'s owning agent for knowledge — its "↔ may consult" line names the domains; the answer is evidence, never authority. A consult that needs a changed system becomes a handover (Hd) that the root raises to the other module\'s root.',W-2*X0-48,15,limit=2,leading=20,fill='#477684');end()
stage_top=consult_y+92+92
positions={};ends={}
for module,x in module_x.items():
yy=stage_top
txt(x+12,yy-18,'Selected work only · a stage can resume or request rework',14,'#68809b')
if module=='data':wrapped(x+12,yy-58,'Execution order, not id order: Solution take (S3) is entered first; Investigation (S2) runs only when no ready solution applies (D83).',SW,13,limit=2,leading=15,fill='#68809b')
for sid in ORDER[module]:
h=stage(module,sid,x,yy,SW);positions[(module,sid)]=(x,yy,h);yy+=h+60
if module=='configuration':ends[module]=envelope(module,x-18,yy,SW+36,['release'],positions)
elif module=='source':ends[module]=envelope(module,x-18,yy,SW+36,['S5','S6','S7','S8'],positions)
else:ends[module]=yy
# Only manifest edges are drawn. Envelopes classify work without inventing another workflow.
for module in MODULE_NAMES:
for edge in MANIFESTS[module]['edges']:
if (module,edge['from']) not in positions or (module,edge['to']) not in positions:continue
x,y,h=positions[(module,edge['from'])];tx_,ty,th_=positions[(module,edge['to'])]
b=next(b for b in BOXES if b['kind']=='stage' and b['id']==module+'/'+edge['from']);bw=b['w']
if abs(ty-(y+h))<=250 and ty>y and edge['kind']=='dependency':
path(f'M{x+bw/2} {y+h} V{ty}')
else:
rail=module_x[module]-16 if edge['kind'] in ('rework','asynchronous') else module_x[module]+SW+16
side=x if edge['kind'] in ('rework','asynchronous') else x+bw
to_side=tx_ if edge['kind'] in ('rework','asynchronous') else tx_+next(b['w'] for b in BOXES if b['kind']=='stage' and b['id']==module+'/'+edge['to'])
add('<g><title>'+escape(edge['from']+' → '+edge['to']+': '+edge['on'])+'</title>')
path(f'M{side} {y+40} H{rail} V{ty+40} H{to_side}','return' if edge['kind']!='dependency' else 'flow');add('</g>')
# the common end of every lane
close_y=max(ends.values())+70
for module,x in module_x.items():path(f'M{x+SW/2} {ends[module]} V{close_y}','flow')
clh=closure_strip(X0,close_y,W-2*X0)
H=precipitation_workflow(close_y+clh+80)
content='\n'.join(svg)
return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}" role="group" aria-labelledby="am-title am-desc" class="agent-map-svg"><title id="am-title">AISA detailed agents, capabilities and gates</title><desc id="am-desc">Four declared sources feed the connector intake, which precedes the persistent Support root; classification and the route are one stage of that root, with H6 and a clarification node beside it. Four working lanes, each headed by its orchestration agent (the module root), show stage agents with their declared variants, their utility agents, the shared common agents they also call, and gates; the legend miniatures at the top right of every agent count the skills it runs and the tools it reaches, and the brief names them; closure is the common end of every lane. Configuration and Customer-specific development carry their own delivery follow-through. Seven stages across the four lanes carry a precipitation mark; one asynchronous purple workflow at the foot summarises those events, creates the facts, skills and tools, and asks a person to seed them for everyone. Core Development is an offer type.</desc><rect width="{W}" height="{H}" fill="#f8fafc"/>\n'+content+'\n</svg>'