@theoremjs/studio 0.3.4-next.484.g20d8b9d → 0.3.4-next.522.g9e684cd

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +3 -3
  2. package/compile.d.ts +1 -1
  3. package/compile.js +44 -2
  4. package/draft.d.ts +55 -2
  5. package/draft.js +14 -0
  6. package/example.js +5 -2
  7. package/map-line.d.ts +12 -0
  8. package/map-line.js +98 -0
  9. package/map.d.ts +1 -1
  10. package/map.js +0 -1
  11. package/mod.d.ts +1 -1
  12. package/mod.js +1 -1
  13. package/package.json +4 -4
  14. package/page/assets/{Banner-G4u0gVOw.js → Banner-UZW6WFTG.js} +2 -2
  15. package/page/assets/{ConsoleFrame-BZqxUndX.js → ConsoleFrame-C6ZxuLse.js} +3 -3
  16. package/page/assets/{ContextMenu-DEnGmVYR.js → ContextMenu-ClIUDzvK.js} +2 -2
  17. package/page/assets/{ToolResultCharts-BWBYDar4.js → ToolResultCharts-COgqvWEu.js} +1 -1
  18. package/page/assets/{TraceInspector-B0cUdNg5.js → TraceInspector-D2unePaa.js} +1 -1
  19. package/page/assets/{TraceOverview-Bjqg7laD.js → TraceOverview-CCA1iUNg.js} +2 -2
  20. package/page/assets/{_virtual_studio-type-sources-DBS0HldZ.js → _virtual_studio-type-sources-DfyVGpe8.js} +93 -61
  21. package/page/assets/browser-engine-CRGnAh9x.js +11 -0
  22. package/page/assets/browser-transport-CgmN-3Ia.js +47 -0
  23. package/page/assets/{compile-egress-BSxOdX4K.js → compile-egress-BRGW9qfO.js} +2 -2
  24. package/page/assets/{crypto-iT_LZ8Et.js → crypto-BoqeX_71.js} +1 -1
  25. package/page/assets/{egress-automata-B5ZGE1mT.js → egress-automata-SiHQTP1i.js} +1 -1
  26. package/page/assets/{encode-files-8AFMw7Er.js → encode-files-DFrbgXms.js} +1 -1
  27. package/page/assets/{export-agent-Cxamh6fg.js → export-agent-U8qRBuhg.js} +3 -3
  28. package/page/assets/{guardrail-tester-Lubrp5dY.js → guardrail-tester-DASanQyo.js} +2 -2
  29. package/page/assets/host-patterns-cSf1U25G.js +1 -0
  30. package/page/assets/index-C9rhrCGT.css +1 -0
  31. package/page/assets/index-CGN6iWF2.js +101 -0
  32. package/page/assets/{labels-provider-BSiD6Lrb.js → labels-provider-Gus3heEH.js} +1 -1
  33. package/page/assets/{live-ingress-lDUGPwth.js → live-ingress-qi-y5uvH.js} +1 -1
  34. package/page/assets/{live-messages-BwMauTVi.js → live-messages-cl-CI8gZ.js} +1 -1
  35. package/page/assets/reader-IkImpzZF.js +7 -0
  36. package/page/assets/shaped-data-CITXsEfw.js +21 -0
  37. package/page/assets/{stub-_DAX3TnJ.js → stub-Ccyi1ZiM.js} +2 -2
  38. package/page/assets/studio-code-pane-AoMu0cGy.js +8 -0
  39. package/page/assets/{studio-console-C-rWlbSY.js → studio-console-JNYkyQ0E.js} +1 -1
  40. package/page/assets/{studio-decision-CCpYdeOV.js → studio-decision-BRJuvgS3.js} +1 -1
  41. package/page/assets/{studio-embedding-BM1yTK8B.js → studio-embedding-D-r2kuz3.js} +1 -1
  42. package/page/assets/studio-host-console-HoLSpRVk.js +2 -0
  43. package/page/assets/{studio-labels-CBBymwHa.js → studio-labels-CBcjlfH4.js} +1 -1
  44. package/page/assets/studio-live-DtywUIK2.js +5 -0
  45. package/page/assets/studio-map-BIJ-S7br.js +1 -0
  46. package/page/assets/studio-trace-replay-BuOKQwlj.js +2 -0
  47. package/page/assets/{studio-type-checker-B4qvn-WC.js → studio-type-checker-C8qG2W9D.js} +1 -1
  48. package/page/assets/tool-schema-DDkOMTMz.js +17 -0
  49. package/page/assets/{trace-record-D4xAUHLZ.js → trace-record-Dqvj9bhx.js} +1 -1
  50. package/page/assets/tree-jmLWUWxQ.js +3 -0
  51. package/page/assets/{turn-client-D_bUUpER.js → turn-client-DSatougR.js} +2 -2
  52. package/page/assets/types-4jMDDudJ.js +1 -0
  53. package/page/assets/{use-one-call-DWpPEFdQ.js → use-one-call-BcTU9gJ9.js} +1 -1
  54. package/page/assets/workspace-B2bnm5qv.js +1 -0
  55. package/page/index.html +19 -19
  56. package/read-source.js +10 -1
  57. package/registrations.d.ts +6 -0
  58. package/run-payload.d.ts +1 -1
  59. package/runtime.d.ts +1 -1
  60. package/runtime.js +1 -1
  61. package/server/draft-decide.d.ts +1 -4
  62. package/server/draft-decide.js +1 -1
  63. package/server/draft-replies.js +2 -2
  64. package/source.js +1 -1
  65. package/tools.js +3 -0
  66. package/types.d.ts +6 -0
  67. package/ui/brand-icons.d.ts +2 -2
  68. package/ui/brand-icons.js +6 -4
  69. package/ui/lib/tree-map-layout.d.ts +120 -18
  70. package/ui/lib/tree-map-layout.js +1292 -190
  71. package/ui/map-token.css +25 -0
  72. package/ui/node-icon.js +1 -4
  73. package/ui/profile-editor.js +22 -6
  74. package/ui/read-map.css +1 -24
  75. package/ui/read-map.js +52 -4
  76. package/ui/studio-map.js +1 -3
  77. package/ui/tree-map.css +9 -0
  78. package/ui/tree-map.d.ts +27 -6
  79. package/ui/tree-map.js +215 -75
  80. package/page/assets/browser-engine-CaPsakbj.js +0 -11
  81. package/page/assets/browser-transport-BEha0XY8.js +0 -47
  82. package/page/assets/host-patterns-C9RMP16Q.js +0 -1
  83. package/page/assets/index-C3n9ogf7.js +0 -101
  84. package/page/assets/index-Cbs7B46A.css +0 -1
  85. package/page/assets/reader-oVJF5Iqk.js +0 -7
  86. package/page/assets/shaped-data-Bc6u4pow.js +0 -16
  87. package/page/assets/studio-code-pane-BqHsqd7P.js +0 -8
  88. package/page/assets/studio-host-console-D8_OCbZG.js +0 -2
  89. package/page/assets/studio-live-CgYci_ts.js +0 -5
  90. package/page/assets/studio-map-QDBMJ6x3.js +0 -1
  91. package/page/assets/studio-trace-replay-BIRuiVjZ.js +0 -2
  92. package/page/assets/tool-schema-CvHbJyis.js +0 -18
  93. package/page/assets/tree-DtF83caH.js +0 -3
  94. package/page/assets/types-Bc3MMW0P.js +0 -1
  95. package/page/assets/workspace-ILHOuESH.js +0 -1
@@ -9,6 +9,8 @@
9
9
  import { workspaceTree } from "../../mod.js";
10
10
  /** A row's height closed; grown, the least and the most it stretches to, to fill the box it fits. */
11
11
  const ROW = 32;
12
+ /** Closed, the room before each agent after the first. */
13
+ const AGENT_GAP = 16;
12
14
  const GROWN_ROW = 28;
13
15
  const TALLEST_ROW = 40;
14
16
  /** How far in each level of the tree starts. */
@@ -16,53 +18,116 @@ export const INDENT = 28;
16
18
  /** A token's half height. */
17
19
  export const HALF = 11;
18
20
  export const PAD = 16;
21
+ /** Grown, how much further in from the box's edges the map stands. */
22
+ const INSET = 16;
23
+ /** Grown, the room over the columns their headings take: a title, the line under it, and a gap. */
24
+ export const HEADING = 56;
25
+ /** Grown, each column's heading, in order: the tree, the registries, the runner. */
26
+ export const HEADINGS = [
27
+ { label: 'Profile', note: 'The contract you define' },
28
+ { label: 'Registry', note: 'The pieces you provide' },
29
+ { label: 'Runner', note: 'What runs your contract' },
30
+ ];
19
31
  /** Closed, between the tree's widest row and the feeds; grown, the least and most between columns. */
20
32
  const GAP = 72;
21
33
  const LEAST_GAP = 56;
22
- const MOST_GAP = 160;
23
- /** The tools beside a Tools row: the first of each type. */
34
+ const MOST_GAP = 200;
35
+ /** The least room between two lanes. */
36
+ const LANE = 9;
37
+ /** Closed, how far left of the feeds the lines from a row run down to them, as one. */
38
+ const TRUNK_GAP = 24;
39
+ /** The types of tool the map draws, in the order it draws them. */
24
40
  const TOOL_TYPES = ['function', 'http', 'mcp', 'agent'];
25
- /** Grown, the runner's steps two across, down one column and then the next. */
26
- const STEPS_DOWN = 4;
27
- const STEP_GAP = 24;
28
- /** By how far grown every node has reached its zone; the rest is for drawing in. */
29
- const MOVE_TO = 0.7;
30
- /** Over what stretch of growing the added nodes draw, and how long each takes of it. */
31
- const DRAW_FROM = 0.2;
32
- const DRAW_TO = 0.95;
33
- const DRAW_SPAN = 0.1;
34
- /** How finely growing is walked to find where an added node first stands clear. */
35
- const SAMPLES = 100;
41
+ /** The tools a remote call reaches the network for. */
42
+ const REMOTE_TYPES = ['http', 'mcp'];
43
+ /** By how far grown the map's frame has eased to its grown size and place. */
44
+ const MOVE_TO = 0.2;
45
+ /** What the still shows and growing takes away is gone by this far grown. */
46
+ export const LEAVE_TO = 0.2;
47
+ /** How far grown the drawing starts from the tree's agents: once the frame has settled. */
48
+ const DRAW_FROM = MOVE_TO;
49
+ /**
50
+ * How long nodes of the still take to glide into place, as a length of line: they go as the
51
+ * drawing comes to the node they are reached from, and its lines to them wait for them.
52
+ */
53
+ const GLIDE = 280;
54
+ /** How long a node growing adds takes to show once its line has reached it, as a length of line. */
55
+ const SHOW = 180;
56
+ /** How far clear of a node of the still what shows near it stands. */
57
+ const ROOM = 12;
58
+ /** How much of its glide a node of the still has done when what would land on it starts to show. */
59
+ const AHEAD = 0.35;
60
+ /** How far a node of the still that made room glides on once its line reaches it, as a share of a glide. */
61
+ const LAND = 0.3;
62
+ /** How much of its showing a node has done as its line reaches it, so the line meets it there. */
63
+ const MET = 0.35;
64
+ /** How much of its showing a node has done when its lines start to leave it. */
65
+ const SHOWN = 0.4;
66
+ /** How far a node growing adds drifts in along its line as it shows. */
67
+ const DRIFT = 14;
68
+ /** How long the drawing rests at each node before its lines leave it, as a length of line. */
69
+ const PAUSE = 40;
36
70
  /** Space kept clear round a token. */
37
71
  const CLEAR = 2;
38
- export const RUNNER = 'runner';
39
- export const TOOL_RUNNER = 'tool-runner';
72
+ /**
73
+ * Of a still node's glide into a slot, the share of it the node goes across before it starts down
74
+ * or up, and the share it has left to go across once it has.
75
+ */
76
+ const SINK = 0.3;
77
+ export const TOOL_RUNTIME = 'tool-runtime';
78
+ export const GUARDRAIL_RUNTIME = 'guardrail-runtime';
79
+ export const TRACE_DESTINATIONS = 'trace-destinations';
80
+ export const PROVIDER_STREAM = 'provider-stream';
81
+ export const KEY_VAULT = 'key-vault';
82
+ export const TURN_LOOP = 'turn-loop';
40
83
  /**
41
84
  * What the runner does in a turn, in order: the steps of the kernel contract's turn lifecycle
42
- * (docs/contracts/kernel.md, "Turn lifecycle") that do work.
85
+ * (docs/contracts/kernel.md, "Turn lifecycle") that do work, and the profile sections each reads.
43
86
  * why: The canary bind runs only while a canary detector is on, and pre_turn, before_end and
44
- * post_turn are hooks for the host's stages, so the map leaves those four out.
87
+ * post_turn are hooks for the host's stages, so the map leaves those four out; the structured
88
+ * reply's validation is the outputs' own, not a story of the runner's, so it is left out too.
89
+ * Sanitize and egress are named for what they guard, the input and the reply, and drawn from them.
45
90
  */
46
91
  export const TURN_STEPS = [
47
- { id: 'resolve', label: 'Resolve' },
48
- { id: 'sanitize', label: 'Sanitize' },
49
- { id: 'compaction', label: 'Compaction' },
50
- { id: 'stream', label: 'Provider stream' },
51
- { id: 'tools', label: 'Tool loop' },
52
- { id: 'validation', label: 'Validation' },
53
- { id: 'egress', label: 'Egress' },
54
- { id: 'trace', label: 'Trace' },
92
+ { id: 'sanitize', label: 'Input guard', reads: ['inputs'] },
93
+ { id: 'egress', label: 'Reply guard', reads: ['outputs'] },
94
+ { id: 'trace', label: 'Trace exporter', reads: ['observability'] },
55
95
  ];
56
96
  /**
57
- * What the tool runner checks of every call, in order (`executeRegisteredTool`): its permission,
58
- * its sign-in, the network a remote call reaches, and the guard on its result.
97
+ * What the tool runner does with every call (`executeRegisteredTool`): its permission, the MCP
98
+ * session an MCP call runs in, and its sign-in.
99
+ * why: The sessions stand before the sign-in, level with the third tool, so an MCP tool there has a
100
+ * straight line to them.
59
101
  */
60
102
  export const TOOL_CHECKS = [
61
103
  { id: 'permission', label: 'Permission gate' },
62
- { id: 'sign-in', label: 'Sign-in' },
63
- { id: 'network', label: 'Network check' },
64
- { id: 'result', label: 'Result guard' },
104
+ { id: 'sessions', label: 'MCP sessions' },
105
+ { id: 'sign-in', label: 'Authentication' },
106
+ ];
107
+ /** What the guard runner does with a call: the network a remote call reaches, and the guard on its result. */
108
+ const GUARDRAIL_CHECKS = [
109
+ { id: 'network', label: 'Network guard' },
110
+ { id: 'result', label: 'Tool result guard' },
111
+ ];
112
+ /**
113
+ * The order of an agent's rows: what it runs on, how it turns, its tools, its guardrails, then what
114
+ * goes in and out of it, level with the guards on them; any other section after, observability last.
115
+ */
116
+ const ROW_ORDER = [
117
+ 'identity',
118
+ 'models',
119
+ 'decision',
120
+ 'image',
121
+ 'speech',
122
+ 'live',
123
+ 'turnBehaviour',
124
+ 'tools',
125
+ 'guardrails',
126
+ 'inputs',
127
+ 'outputs',
65
128
  ];
129
+ /** Closed, the map shows only these beside the tree: each agent's models and its tools. */
130
+ const CLOSED = ['model', 'tool'];
66
131
  /** A row's token id: the tree's ids repeat from one agent to the next. */
67
132
  export function rowId(row) {
68
133
  return `${row.agent}/${row.node.id}`;
@@ -78,8 +143,18 @@ function rowsOf(workspace, only) {
78
143
  ...(parent ? { parent: rowId(parent) } : {}),
79
144
  };
80
145
  rows.push(row);
81
- for (const child of node.children)
146
+ // why: The models are named once, under their providers beside the tree; the Models row links to
147
+ // them. Observability comes last, level with the trace destinations and the trace exporter at
148
+ // the foot of the map.
149
+ const children = node.children.filter((child) => child.ref.facet !== 'modelBinding');
150
+ const rank = (child) => child.ref.facet === 'observability'
151
+ ? ROW_ORDER.length + 1
152
+ : ROW_ORDER.includes(child.ref.facet)
153
+ ? ROW_ORDER.indexOf(child.ref.facet)
154
+ : ROW_ORDER.length;
155
+ for (const child of children.toSorted((a, b) => rank(a) - rank(b))) {
82
156
  walk(child, agent, depth + 1, row);
157
+ }
83
158
  };
84
159
  workspaceTree(workspace).agents.forEach((root, index) => {
85
160
  const key = workspace.agents[index]?.key ?? '';
@@ -88,127 +163,287 @@ function rowsOf(workspace, only) {
88
163
  });
89
164
  return rows;
90
165
  }
166
+ /** A tool's feed, with a line from each row in `from`, and an agent tool's agent under it. */
167
+ function toolFeeds(workspace, id, spec, from) {
168
+ const tool = {
169
+ id,
170
+ label: spec.toolName,
171
+ kind: 'tool',
172
+ of: spec.toolType,
173
+ from,
174
+ column: 1,
175
+ group: 'tools',
176
+ };
177
+ const agent = workspace.agents.find((each) => each.key === spec.agentKey);
178
+ if (spec.toolType !== 'agent' || !agent)
179
+ return [tool];
180
+ return [
181
+ tool,
182
+ {
183
+ id: `${id}/agent`,
184
+ label: agent.identity.agentId || agent.identity.handle,
185
+ kind: 'subagent',
186
+ of: agent.identity.profileType,
187
+ from: [],
188
+ under: id,
189
+ column: 1,
190
+ group: 'tools',
191
+ },
192
+ ];
193
+ }
91
194
  /**
92
- * Beside the tree, shared by every agent drawn: the providers, each with the protocols the models
93
- * it serves speak, and a line into each of those models; the first tool of each type the agents
94
- * allow, with a line into each Tools row that allows it, an MCP tool's sessions under it and an
95
- * agent tool's agent under it, joined to the runner; and the one runner, with a line into each
96
- * agent but a host (whose calls the tool runner runs) and its steps under it. Past them, the tool
97
- * runner the tool loop calls, its checks under it.
195
+ * Each provider with the sources of each of its protocols, and each model once under its provider,
196
+ * with a line from each source that runs on it.
197
+ */
198
+ function modelsOf(modelled) {
199
+ const providers = new Map();
200
+ // why: Agents that run on the same model share it, as they share a tool: one model, with a line
201
+ // from each Models row.
202
+ const models = new Map();
203
+ for (const { agent, source } of modelled) {
204
+ for (const binding of agent.modelBindings) {
205
+ const protocols = providers.get(binding.provider) ?? new Map();
206
+ protocols.set(binding.protocol, [...(protocols.get(binding.protocol) ?? []), source]);
207
+ providers.set(binding.provider, protocols);
208
+ const id = `model:${binding.provider}:${binding.apiId}:${binding.modelId}`;
209
+ const shared = models.get(id);
210
+ if (shared) {
211
+ if (!shared.from.includes(source))
212
+ shared.from.push(source);
213
+ continue;
214
+ }
215
+ models.set(id, {
216
+ id,
217
+ label: binding.modelId,
218
+ kind: 'model',
219
+ of: binding.apiId,
220
+ from: [source],
221
+ under: `provider:${binding.provider}`,
222
+ column: 1,
223
+ group: 'providers',
224
+ });
225
+ }
226
+ }
227
+ return { providers, models };
228
+ }
229
+ /**
230
+ * Beside the tree, shared by every agent drawn, two columns. First, the registries: each tool the
231
+ * agents allow, by type, with a line from each Tools row that allows it, and an agent tool's
232
+ * agent under it; each provider, with its models under it, each with a line from its Models row; and the trace
233
+ * destinations, with a line from the observability rows. Then the runner: the provider protocols, a
234
+ * head over each provider's protocols, each with a line from its provider; the tool runner, with a line from
235
+ * each tool, and what it does with each call under it; the guard runner, with a line from the
236
+ * guardrails, and what it does under it; and the trace exporter, with a line from the trace
237
+ * destinations.
98
238
  */
99
239
  function feedsOf(workspace, rows) {
100
240
  const drawn = workspace.agents.filter((agent) => rows.some((row) => row.agent === agent.key));
101
241
  const rowOf = (agent, facet, key) => rows.find((row) => row.agent === agent &&
102
242
  row.node.ref.facet === facet &&
103
243
  (key === undefined || ('key' in row.node.ref && row.node.ref.key === key)));
244
+ const sections = (facets) => rows.filter((row) => facets.includes(row.node.ref.facet)).map(rowId);
104
245
  const feeds = [];
105
- const providers = new Map();
106
- for (const agent of drawn) {
107
- for (const binding of agent.modelBindings) {
108
- const row = rowOf(agent.key, 'modelBinding', binding.key);
109
- if (!row)
110
- continue;
111
- const provider = providers.get(binding.provider) ?? { into: [], protocols: new Set() };
112
- provider.into.push(rowId(row));
113
- provider.protocols.add(binding.protocol);
114
- providers.set(binding.provider, provider);
115
- }
116
- }
117
- for (const [provider, { into, protocols }] of providers) {
118
- const id = `provider:${provider}`;
119
- feeds.push({ id, label: provider, kind: 'provider', into, column: 1, group: 'providers' });
120
- for (const protocol of protocols) {
121
- const nested = { id: `${id}/${protocol}`, label: protocol, kind: 'protocol' };
122
- feeds.push({ ...nested, into: [], under: id, column: 1, group: 'providers' });
123
- }
124
- }
125
246
  const allowing = (key) => drawn.flatMap((agent) => {
126
247
  const row = agent.tools.allow.includes(key) ? rowOf(agent.key, 'tools') : undefined;
127
248
  return row ? [rowId(row)] : [];
128
249
  });
129
- for (const type of TOOL_TYPES) {
130
- const tool = workspace.toolSpecs.find((each) => each.toolType === type && allowing(each.key).length > 0);
131
- if (!tool)
132
- continue;
133
- const id = `tool:${tool.key}`;
134
- const into = allowing(tool.key);
250
+ // The tools the agents allow, by type, each type's in the workspace's order.
251
+ const allowed = TOOL_TYPES.flatMap((type) => workspace.toolSpecs.filter((each) => each.toolType === type && allowing(each.key).length > 0));
252
+ // why: An agent tool's agent runs on a model of its own, so its models stand under their
253
+ // providers too, each with a line from the agent under the tool, when the tree does not draw it.
254
+ const asked = allowed.flatMap((tool) => {
255
+ const agent = drawn.some((each) => each.key === tool.agentKey)
256
+ ? undefined
257
+ : workspace.agents.find((each) => each.key === tool.agentKey);
258
+ return agent ? [{ agent, source: `tool:${tool.key}/agent` }] : [];
259
+ });
260
+ const modelled = [
261
+ ...drawn.flatMap((agent) => {
262
+ const section = rowOf(agent.key, 'models');
263
+ return section ? [{ agent, source: rowId(section) }] : [];
264
+ }),
265
+ ...asked,
266
+ ];
267
+ const { providers, models } = modelsOf(modelled);
268
+ const protocols = [];
269
+ for (const [provider, byProtocol] of providers) {
270
+ const id = `provider:${provider}`;
135
271
  feeds.push({
136
272
  id,
137
- label: tool.toolName,
138
- kind: 'tool',
139
- of: type,
140
- into,
273
+ label: provider,
274
+ kind: 'provider',
275
+ // why: The Models rows link to the models under it, not to the provider.
276
+ from: [],
141
277
  column: 1,
142
- group: 'tools',
278
+ group: 'providers',
143
279
  });
144
- const nested = { into: [], under: id, column: 1, group: 'tools' };
145
- if (type === 'mcp') {
146
- feeds.push({ ...nested, id: `${id}/sessions`, label: 'MCP sessions', kind: 'branch' });
147
- }
148
- const agent = workspace.agents.find((each) => each.key === tool.agentKey);
149
- if (type === 'agent' && agent) {
150
- const label = agent.identity.agentId || agent.identity.handle;
151
- feeds.push({
152
- ...nested,
153
- id: `${id}/agent`,
154
- label,
155
- kind: 'subagent',
156
- of: agent.identity.profileType,
157
- joins: RUNNER,
280
+ for (const protocol of byProtocol.keys()) {
281
+ protocols.push({
282
+ id: `${id}/${protocol}`,
283
+ label: protocol,
284
+ kind: 'protocol',
285
+ from: [id],
286
+ under: PROVIDER_STREAM,
287
+ column: 2,
288
+ group: 'protocols',
158
289
  });
159
290
  }
160
291
  }
161
- const runs = drawn.filter((agent) => agent.identity.profileType !== 'host');
162
- const into = runs.flatMap((agent) => {
163
- const row = rowOf(agent.key, 'identity');
164
- return row ? [rowId(row)] : [];
292
+ // why: The key vault holds the keys the protocols call providers with and the tools sign in
293
+ // with, so it stands between the providers and the tools.
294
+ if (protocols.length > 0 || allowed.length > 0) {
295
+ feeds.push({
296
+ id: KEY_VAULT,
297
+ label: 'Key vault',
298
+ kind: 'vault',
299
+ from: [],
300
+ column: 1,
301
+ group: 'vault',
302
+ });
303
+ }
304
+ const tools = allowed.map((spec) => ({ id: `tool:${spec.key}`, spec }));
305
+ for (const { id, spec } of tools)
306
+ feeds.push(...toolFeeds(workspace, id, spec, allowing(spec.key)));
307
+ const stepOf = (id) => TURN_STEPS.find((step) => step.id === id) ?? { id, label: id, reads: [] };
308
+ const guardrails = sections(['guardrails']);
309
+ const observability = sections(['observability']);
310
+ // Registries: each provider, its models under it, and the trace destinations.
311
+ feeds.push(...models.values());
312
+ feeds.push({
313
+ id: TRACE_DESTINATIONS,
314
+ label: 'Trace destinations',
315
+ kind: 'destinations',
316
+ from: observability,
317
+ column: 1,
318
+ group: 'destinations',
165
319
  });
166
- feeds.push({ id: RUNNER, label: 'Runner', kind: 'runner', into, column: 1, group: 'runner' });
167
- for (const step of TURN_STEPS) {
320
+ // The runner: the provider protocols, one under another, then the turn loop, the tool runner and
321
+ // the guard runner with what each does under it, then the trace exporter.
322
+ if (protocols.length > 0) {
168
323
  feeds.push({
169
- id: `${RUNNER}/${step.id}`,
170
- label: step.label,
324
+ id: PROVIDER_STREAM,
325
+ label: 'Provider protocols',
326
+ kind: 'stream',
327
+ // why: The protocols call each provider with the key the vault holds for it.
328
+ from: [KEY_VAULT],
329
+ column: 2,
330
+ group: 'protocols',
331
+ });
332
+ feeds.push(...protocols);
333
+ }
334
+ // why: The turn loop runs the steps of a turn as the turn behaviour sets them: where the step
335
+ // limit stops it, and whether a cut-off reply continues or the user steers it.
336
+ const turnBehaviour = sections(['turnBehaviour']);
337
+ if (turnBehaviour.length > 0) {
338
+ feeds.push({
339
+ id: TURN_LOOP,
340
+ label: 'Turn loop',
171
341
  kind: 'step',
172
- of: step.id,
173
- into: [],
174
- under: RUNNER,
175
- column: 1,
176
- group: 'runner',
177
- ...(step.id === 'tools' ? { joins: TOOL_RUNNER } : {}),
342
+ of: 'loop',
343
+ from: turnBehaviour,
344
+ column: 2,
345
+ group: 'turn-loop',
178
346
  });
179
347
  }
180
- const runner = { id: TOOL_RUNNER, label: 'Tool runner', kind: 'tool-runner' };
181
- feeds.push({ ...runner, into: [], column: 2, group: 'tool-runner' });
348
+ const toolsOf = (types) => tools.filter(({ spec }) => types.includes(spec.toolType)).map(({ id }) => id);
349
+ const checkFrom = {
350
+ 'sign-in': [
351
+ ...tools.filter(({ spec }) => (spec.authType ?? 'none') !== 'none').map(({ id }) => id),
352
+ // why: A tool's sign-in reads its key or its resolver from the vault.
353
+ ...(tools.length > 0 ? [KEY_VAULT] : []),
354
+ ],
355
+ network: toolsOf(REMOTE_TYPES),
356
+ sessions: toolsOf(['mcp']),
357
+ // why: The tool runner hands each result to its guard before it goes back to the model.
358
+ result: tools.length > 0 ? [TOOL_RUNTIME] : [],
359
+ };
360
+ feeds.push({
361
+ id: TOOL_RUNTIME,
362
+ label: 'Tool runner',
363
+ kind: 'tool-runtime',
364
+ from: tools.map(({ id }) => id),
365
+ column: 2,
366
+ group: 'tool-runtime',
367
+ });
182
368
  for (const check of TOOL_CHECKS) {
183
- const nested = { id: `${TOOL_RUNNER}/${check.id}`, label: check.label, of: check.id };
184
369
  feeds.push({
185
- ...nested,
370
+ id: `${TOOL_RUNTIME}/${check.id}`,
371
+ label: check.label,
372
+ kind: check.id === 'sessions' ? 'branch' : 'check',
373
+ of: check.id,
374
+ from: checkFrom[check.id] ?? [],
375
+ under: TOOL_RUNTIME,
376
+ column: 2,
377
+ group: 'tool-runtime',
378
+ });
379
+ }
380
+ feeds.push({
381
+ id: GUARDRAIL_RUNTIME,
382
+ label: 'Guard runner',
383
+ kind: 'guardrail-runtime',
384
+ from: guardrails,
385
+ column: 2,
386
+ group: 'guardrail-runtime',
387
+ });
388
+ for (const id of ['sanitize', 'egress']) {
389
+ const step = stepOf(id);
390
+ feeds.push({
391
+ id: `${GUARDRAIL_RUNTIME}/${id}`,
392
+ label: step.label,
393
+ kind: 'step',
394
+ of: id,
395
+ from: sections(step.reads),
396
+ under: GUARDRAIL_RUNTIME,
397
+ column: 2,
398
+ group: 'guardrail-runtime',
399
+ });
400
+ }
401
+ for (const check of GUARDRAIL_CHECKS) {
402
+ feeds.push({
403
+ id: `${GUARDRAIL_RUNTIME}/${check.id}`,
404
+ label: check.label,
186
405
  kind: 'check',
187
- into: [],
188
- under: TOOL_RUNNER,
406
+ of: check.id,
407
+ from: checkFrom[check.id] ?? [],
408
+ under: GUARDRAIL_RUNTIME,
189
409
  column: 2,
190
- group: 'tool-runner',
410
+ group: 'guardrail-runtime',
191
411
  });
192
412
  }
413
+ feeds.push({
414
+ id: 'telemetry',
415
+ label: stepOf('trace').label,
416
+ kind: 'step',
417
+ of: 'trace',
418
+ from: [TRACE_DESTINATIONS],
419
+ column: 2,
420
+ group: 'telemetry',
421
+ });
193
422
  return feeds;
194
423
  }
195
424
  const clamp = (value) => Math.min(Math.max(value, 0), 1);
425
+ /** Slow out, slow in: how far a move or a draw has gone at `at` of its time, both from 0 to 1. */
426
+ /** A gentle ease in and out, for what moves far: it never rushes through the middle. */
427
+ export const soft = (at) => (1 - Math.cos(Math.PI * clamp(at))) / 2;
428
+ /** An ease that comes in quickly and settles slowly, for what drifts the last of its way. */
429
+ export const settling = (at) => 1 - (1 - clamp(at)) ** 3;
430
+ /** How far the map's frame has settled this far grown, 0 to 1: its size and place, and what holds it. */
431
+ export const settledAt = (grow) => soft(grow / MOVE_TO);
432
+ export const ease = (at) => {
433
+ const t = clamp(at);
434
+ return t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;
435
+ };
196
436
  const between = (from, to, at) => from + (to - from) * at;
197
- /** A step under the runner two across, down one column and then the next; any other a level in. */
198
- function slotsOf(feeds, width) {
437
+ /** A node under a feed a level in, a row down for each before it. */
438
+ function slotsOf(feeds) {
199
439
  const slots = new Map();
200
- const steps = feeds.filter((feed) => feed.kind === 'step');
201
- const stepColumn = Math.max(0, ...steps.map((step) => width(step.id))) + STEP_GAP;
202
440
  const counts = new Map();
203
441
  for (const feed of feeds) {
204
442
  if (feed.under === undefined)
205
443
  continue;
206
444
  const index = counts.get(feed.under) ?? 0;
207
445
  counts.set(feed.under, index + 1);
208
- const slot = feed.kind === 'step'
209
- ? { across: Math.floor(index / STEPS_DOWN) * stepColumn, down: (index % STEPS_DOWN) + 1 }
210
- : { across: 0, down: index + 1 };
211
- slots.set(feed.id, { holder: feed.under, across: INDENT + slot.across, down: slot.down });
446
+ slots.set(feed.id, { holder: feed.under, across: INDENT, down: index + 1 });
212
447
  }
213
448
  return slots;
214
449
  }
@@ -237,18 +472,48 @@ function stacksOf(rows, feeds, slots) {
237
472
  });
238
473
  return { held, columns: [stack(tree), ...columns] };
239
474
  }
475
+ /** The tallest row a grown map can take that `fits`, else the least. */
476
+ function fittingRow(fits) {
477
+ for (let step = TALLEST_ROW; step >= GROWN_ROW; step -= 1) {
478
+ if (fits(step))
479
+ return step;
480
+ }
481
+ return GROWN_ROW;
482
+ }
240
483
  /**
241
- * The map grown: the tree, each agent a zone of its own; beside it the providers, the tools and the
242
- * runner; past them the tool runner. Rows stretch, up to a most, and the gaps between columns and
243
- * between groups widen, so the map fills `box`; without one, rows and gaps are at their least.
484
+ * Moves the last row and `foot` down by `shift`, and the rows before it with no line of their own
485
+ * to stand a `step` apart over it.
244
486
  */
245
- function grownPlaces(rows, feeds, slots, width, box) {
246
- const { held, columns } = stacksOf(rows, feeds, slots);
247
- // In rows, from the top node's middle to the bottom one's, each gap at its least of a row.
248
- const span = Math.max(...columns.map((each) => each.rows + each.ids.length - 2));
249
- const room = box ? box.height - 2 * (PAD + HALF) : 0;
250
- const row = Math.min(Math.max(room / Math.max(span, 1), GROWN_ROW), TALLEST_ROW);
251
- const height = Math.max(room, span * row);
487
+ function liftTail(rows, lined, lastRow, ys, foot, shift, step) {
488
+ const tail = [lastRow];
489
+ for (const each of rows.toReversed().slice(1)) {
490
+ if (lined.has(rowId(each)) || each.agent !== lastRow.agent)
491
+ break;
492
+ tail.push(each);
493
+ }
494
+ const last = (ys.get(rowId(lastRow)) ?? 0) + Math.max(0, shift);
495
+ for (const [index, each] of tail.entries())
496
+ ys.set(rowId(each), last - index * step);
497
+ if (shift > 0)
498
+ for (const block of foot)
499
+ block.top += shift;
500
+ }
501
+ /** Opens `spare` room between the agents, each a zone of its own. */
502
+ function spreadAgents(rows, tree, ys, blocks, spare) {
503
+ const agents = [...new Set(rows.map((each) => each.agent))];
504
+ if (agents.length <= 1 || spare <= 0)
505
+ return;
506
+ const by = (agent) => (spare * agents.indexOf(agent)) / (agents.length - 1);
507
+ for (const each of rows)
508
+ ys.set(rowId(each), (ys.get(rowId(each)) ?? 0) + by(each.agent));
509
+ for (const block of blocks) {
510
+ const anchor = block.anchor && tree.get(block.anchor.row);
511
+ if (anchor)
512
+ block.top += by(anchor.agent);
513
+ }
514
+ }
515
+ /** Where each column starts: the tree, the registries, the runner. */
516
+ function columnLefts(rows, feeds, slots, width, box) {
252
517
  const right = (id, x) => x + width(id);
253
518
  const treeRight = Math.max(0, ...rows.map((each) => right(rowId(each), each.x)));
254
519
  const columnWidth = (column) => Math.max(0, ...feeds
@@ -258,84 +523,913 @@ function grownPlaces(rows, feeds, slots, width, box) {
258
523
  return right(feed.id, slot ? slot.across : 0);
259
524
  }));
260
525
  const [first, second] = [columnWidth(1), columnWidth(2)];
261
- const spare = box ? (box.width - 2 * PAD - (treeRight - PAD) - first - second) / 2 : LEAST_GAP;
262
- const gap = Math.min(Math.max(spare, LEAST_GAP), MOST_GAP);
263
- const lefts = [0, treeRight + gap, treeRight + gap + first + gap];
264
- const places = new Map();
526
+ const taken = treeRight - PAD + first + second;
527
+ // A gap is wide enough for a lane into each feed of the column after it, and the spare is shared.
528
+ const gaps = [1, 2].map((column) => {
529
+ const lanes = feeds.filter((feed) => feed.column === column && feed.from.length + (feed.late?.length ?? 0) > 0).length;
530
+ return Math.max(LEAST_GAP, (lanes + 1) * LANE);
531
+ });
532
+ const [one = 0, two = 0] = gaps;
533
+ const spare = box ? Math.max(0, box.width - 2 * (PAD + INSET) - taken - one - two) / 2 : 0;
534
+ const widen = (gap) => Math.max(gap, Math.min(gap + spare, MOST_GAP));
535
+ return [0, treeRight + widen(one), treeRight + widen(one) + first + widen(two)];
536
+ }
537
+ /** Stands each block as near its `want` as it can, the blocks in order a row apart, inside the map. */
538
+ function keepInside(steps, start, end, row) {
539
+ let floor = start;
540
+ for (const block of steps) {
541
+ block.top = Math.max(block.want, floor);
542
+ floor = block.top + block.size + 2 * row;
543
+ }
544
+ let ceiling = end;
545
+ for (const block of steps.toReversed()) {
546
+ block.top = Math.max(Math.min(block.top, ceiling - block.size), start);
547
+ ceiling = block.top - 2 * row;
548
+ }
549
+ }
550
+ /**
551
+ * The map grown: the tree, each row with lines into a registry level with it; beside it the
552
+ * registries, then the runner, each block centred on its sources. Rows stretch, up to a most, and
553
+ * the gaps between columns and between groups widen, so the map fills `box`; without one, rows and
554
+ * gaps are at their least.
555
+ */
556
+ function grownPlaces(rows, feeds, slots, width, box) {
557
+ const { held, columns } = stacksOf(rows, feeds, slots);
558
+ const [, registries, runner] = columns;
559
+ const start = PAD + INSET + HEADING + HALF;
560
+ const room = box ? box.height - 2 * (PAD + INSET + HALF) - HEADING : 0;
561
+ const byId = new Map(feeds.map((feed) => [feed.id, feed]));
562
+ const sourcesOf = (id) => {
563
+ const feed = byId.get(id);
564
+ return feed ? [...feed.from, ...(feed.late ?? [])] : [];
565
+ };
265
566
  const tree = new Map(rows.map((each) => [rowId(each), each]));
266
- columns.forEach((stack, column) => {
267
- const free = height - (stack.rows - 1) * row;
268
- // Past a group, the next stands a row and its share of what is free further down.
269
- const extra = stack.ids.length > 1 ? free / (stack.ids.length - 1) : 0;
270
- // why: A column of one group has no gap to widen, so it stands in the middle of the map.
271
- let y = PAD + HALF + (stack.ids.length > 1 ? 0 : free / 2);
272
- for (const group of stack.ids) {
273
- for (const id of group) {
274
- places.set(id, { x: tree.get(id)?.x ?? lefts[column] ?? 0, y });
275
- y += row * (1 + (held.get(id) ?? 0));
567
+ const lined = new Set(feeds.flatMap((feed) => sourcesOf(feed.id)));
568
+ // The rows with lines straight into the runner, each run of them level with the block they feed.
569
+ const toRunner = new Set(feeds.filter((feed) => feed.column === 2).flatMap((feed) => sourcesOf(feed.id)));
570
+ const opens = new Set(rows
571
+ .filter((each, index) => {
572
+ const before = rows[index - 1];
573
+ return toRunner.has(rowId(each)) && !(before && toRunner.has(rowId(before)));
574
+ })
575
+ .map(rowId));
576
+ /** A column's groups, each a block of rows `step` apart, its heads' nested feeds under them. */
577
+ const blocksOf = (stack, step) => (stack?.ids ?? []).map((group) => {
578
+ const offsets = new Map();
579
+ let size = 0;
580
+ for (const id of group) {
581
+ offsets.set(id, size);
582
+ for (const [nested, slot] of slots) {
583
+ if (slot.holder === id)
584
+ offsets.set(nested, size + slot.down * step);
276
585
  }
277
- y += extra;
586
+ size += step * (1 + (held.get(id) ?? 0));
278
587
  }
588
+ // A registry block stands level with the first tree row any of its feeds has a line from.
589
+ const anchor = [...offsets]
590
+ .toSorted((a, b) => a[1] - b[1])
591
+ .flatMap(([id, offset]) => {
592
+ const row = sourcesOf(id).find((source) => tree.has(source));
593
+ return row ? [{ row, offset }] : [];
594
+ })[0];
595
+ return { group, offsets, size: size - step, top: start, anchor };
279
596
  });
280
- return { places, row };
597
+ /**
598
+ * The tree and the registries, together down the map: a row with lines into a registry block is
599
+ * level with the first feed it has a line to, and the row after it stands a row below that block.
600
+ * `extra` opens above each run of rows with lines into the runner; `reserving`, the foot keeps
601
+ * clear of the runner block each run feeds.
602
+ */
603
+ const lay = (step, extra = 0, reserving = true) => {
604
+ const ys = new Map();
605
+ const blocks = blocksOf(registries, step);
606
+ const runs = blocksOf(runner, step);
607
+ let y = start;
608
+ let floor = start;
609
+ // A row after a run of rows into the runner stands below the runner block the run feeds, in
610
+ // its agent.
611
+ let reserve = start;
612
+ let last;
613
+ const placed = new Set();
614
+ for (const row of rows) {
615
+ const id = rowId(row);
616
+ if (last && last.agent !== row.agent) {
617
+ y += 2 * step;
618
+ reserve = start;
619
+ }
620
+ if (opens.has(id))
621
+ y += extra;
622
+ if (reserving && lined.has(id) && !toRunner.has(id))
623
+ y = Math.max(y, reserve);
624
+ if (opens.has(id)) {
625
+ for (const run of runs) {
626
+ const fed = [...run.offsets].find(([feed]) => sourcesOf(feed).includes(id));
627
+ if (fed)
628
+ reserve = Math.max(reserve, y - fed[1] + run.size + 2 * step);
629
+ }
630
+ }
631
+ let below = y;
632
+ for (const block of blocks.filter((each) => each.anchor?.row === id)) {
633
+ const offset = block.anchor?.offset ?? 0;
634
+ block.top = Math.max(floor, y - offset);
635
+ y = block.top + offset;
636
+ floor = block.top + block.size + 2 * step;
637
+ below = Math.max(below, block.top + block.size + step);
638
+ // A block with no line from a row stands after the block before it.
639
+ for (const next of blocks.slice(blocks.indexOf(block) + 1)) {
640
+ if (next.anchor || placed.has(next))
641
+ break;
642
+ next.top = floor;
643
+ floor = next.top + next.size + 2 * step;
644
+ below = Math.max(below, next.top + next.size + step);
645
+ placed.add(next);
646
+ }
647
+ }
648
+ ys.set(id, y);
649
+ y = Math.max(y + step, below);
650
+ last = row;
651
+ }
652
+ for (const block of blocks.filter((each) => !each.anchor && !placed.has(each))) {
653
+ block.top = floor;
654
+ floor = block.top + block.size + 2 * step;
655
+ }
656
+ const bottom = Math.max(...ys.values(), ...blocks.map((block) => block.top + block.size));
657
+ return { ys, blocks, bottom };
658
+ };
659
+ const runnerRows = (runner?.rows ?? 1) + 2 * ((runner?.ids.length ?? 1) - 1) - 1;
660
+ const fits = (step) => {
661
+ const laid = lay(step);
662
+ return laid.bottom - start <= room && runnerRows * step <= room;
663
+ };
664
+ const row = box ? fittingRow(fits) : GROWN_ROW;
665
+ const plain = lay(row);
666
+ const height = Math.max(room, plain.bottom - start, runnerRows * row);
667
+ // why: The last row's registry stands at the foot of the map, with the rows before it that have
668
+ // no line of their own; the map's spare room is shared between the gap above them and the gap
669
+ // above each run of rows into the runner, so the rows into the registries stay close. A run goes
670
+ // no lower than keeps its runner block clear of the foot's.
671
+ const lastRow = rows.at(-1);
672
+ const atFoot = (block) => lastRow !== undefined && block.anchor?.row === rowId(lastRow);
673
+ const breaks = opens.size + 1;
674
+ const footOf = (blocks) => Math.max(...blocks.filter(atFoot).map((block) => block.top + block.size));
675
+ const share = Math.min(Math.max(0, start + height - footOf(lay(row, 0, false).blocks)) / breaks, Math.max(0, start + height - footOf(plain.blocks)) / Math.max(1, opens.size));
676
+ const { ys, blocks, bottom } = plain.blocks.some(atFoot) ? lay(row, share) : plain;
677
+ const end = start + height;
678
+ const foot = blocks.filter(atFoot);
679
+ if (lastRow && foot.length > 0) {
680
+ liftTail(rows, lined, lastRow, ys, foot, end - footOf(foot), row);
681
+ }
682
+ else {
683
+ spreadAgents(rows, tree, ys, blocks, end - bottom);
684
+ }
685
+ const lefts = columnLefts(rows, feeds, slots, width, box);
686
+ const places = new Map();
687
+ for (const [id, y] of ys)
688
+ places.set(id, { x: INSET + (tree.get(id)?.x ?? 0), y });
689
+ for (const block of blocks) {
690
+ for (const id of block.group) {
691
+ places.set(id, { x: INSET + (lefts[1] ?? 0), y: block.top + (block.offsets.get(id) ?? 0) });
692
+ }
693
+ }
694
+ const yOf = (id) => {
695
+ const slot = slots.get(id);
696
+ if (!slot)
697
+ return places.get(id)?.y;
698
+ const holder = yOf(slot.holder);
699
+ return holder === undefined ? undefined : holder + slot.down * row;
700
+ };
701
+ // The runner: each block's head level with the first row it has a line from, so what stands under
702
+ // it is level with the rows after; a head without lines, its block centred on the rows its feeds
703
+ // have lines from.
704
+ const ysOf = (sources) => sources.flatMap((source) => {
705
+ const y = yOf(source);
706
+ return y === undefined ? [] : [y];
707
+ });
708
+ const steps = blocksOf(runner, row).map((block) => {
709
+ const [head] = block.group;
710
+ // why: A source with no line of its own, the key vault, does not place a head; its line finds
711
+ // the head where it stands.
712
+ const heads = ysOf((head === undefined ? [] : sourcesOf(head)).filter((source) => tree.has(source) || sourcesOf(source).length > 0));
713
+ const at = ysOf([...block.offsets.keys()].flatMap(sourcesOf));
714
+ const want = heads.length > 0
715
+ ? Math.min(...heads)
716
+ : at.length > 0
717
+ ? (Math.min(...at) + Math.max(...at)) / 2 - block.size / 2
718
+ : start;
719
+ return { ...block, want };
720
+ });
721
+ keepInside(steps, start, end, row);
722
+ for (const block of steps) {
723
+ for (const id of block.group) {
724
+ places.set(id, { x: INSET + (lefts[2] ?? 0), y: block.top + (block.offsets.get(id) ?? 0) });
725
+ }
726
+ }
727
+ const headings = HEADINGS.map((heading, index) => ({
728
+ ...heading,
729
+ x: INSET + (index === 0 ? PAD : (lefts[index] ?? 0)),
730
+ y: PAD + INSET,
731
+ }));
732
+ return { places, row, headings };
733
+ }
734
+ /** How far what the still shows and growing takes away has gone, 0 to 1. */
735
+ export function goneAt(grow) {
736
+ return ease(grow / LEAVE_TO);
281
737
  }
282
- /** Where a node stands this far grown: a node under a feed keeps its slot under it on the way. */
738
+ /** The key under which the line into a feed under another draws, for a feed that was a row. */
739
+ export const underKey = (feed) => `${feed}^`;
740
+ /** Where a node stands grown: a node under a feed, in its slot under it. */
741
+ function grownPlace(layout, id) {
742
+ const slot = layout.slots.get(id);
743
+ if (!slot)
744
+ return layout.grown.get(id) ?? { x: 0, y: 0 };
745
+ const holder = grownPlace(layout, slot.holder);
746
+ return { x: holder.x + slot.across, y: holder.y + slot.down * layout.row };
747
+ }
748
+ /**
749
+ * Where a node stands this far grown. A node of the still glides to where it stands grown in one
750
+ * eased move, landing as the drawing reaches it. A node growing adds stands where it stands from
751
+ * the node it grows from, and moves with it, drifting in along its line as it shows.
752
+ */
283
753
  export function placeAt(layout, id, grow) {
284
- const moved = clamp(grow / MOVE_TO);
754
+ return standAt(layout, id, grow, true);
755
+ }
756
+ /**
757
+ * Where a node stands this far grown, drifting in or not. What grows from a node keeps to where it
758
+ * stands, not to its drift, so a node's drift moves nothing else.
759
+ */
760
+ function standAt(layout, id, grow, drifts) {
761
+ const moved = glidedAt(layout, id, grow);
762
+ const glide = (from, to, at) => ({
763
+ x: between(from.x, to.x, at),
764
+ y: between(from.y, to.y, at),
765
+ });
285
766
  const slot = layout.slots.get(id);
286
- if (slot) {
287
- const holder = placeAt(layout, slot.holder, grow);
288
- const row = between(ROW, layout.row, moved);
289
- return { x: holder.x + slot.across, y: holder.y + slot.down * row };
767
+ const anchor = layout.anchors.get(id);
768
+ if (anchor !== undefined) {
769
+ // why: It keeps to the node it grows from, which may still be on its way, and comes the last
770
+ // little way along the line that reached it, as a pen's mark settles.
771
+ const from = standAt(layout, anchor, grow, false);
772
+ const [own, its] = [grownPlace(layout, id), grownPlace(layout, anchor)];
773
+ const source = layout.reachedFrom.get(id);
774
+ const back = source === undefined ? { x: 0, y: 0 } : grownPlace(layout, source);
775
+ const [dx, dy] = [own.x - back.x, own.y - back.y];
776
+ const drift = drifts ? DRIFT * (1 - settling(drawnAt(layout, id, grow) ?? 1)) : 0;
777
+ const away = drift / (Math.hypot(dx, dy) || 1);
778
+ return { x: from.x + own.x - its.x - dx * away, y: from.y + own.y - its.y - dy * away };
779
+ }
780
+ // A row of the still that growing takes away folds into its agent as it goes, and an agent going
781
+ // is carried down with the tree's last row above it, so no row passes through it.
782
+ const leaving = layout.leaving.rows.find((row) => rowId(row) === id);
783
+ if (leaving?.parent !== undefined) {
784
+ const parent = placeAt(layout, layout.becomes.get(leaving.parent) ?? leaving.parent, grow);
785
+ return glide(leaving, parent, goneAt(grow));
786
+ }
787
+ if (leaving) {
788
+ const above = layout.rows
789
+ .filter((row) => (layout.closed.get(rowId(row))?.y ?? Infinity) < leaving.y)
790
+ .at(-1);
791
+ const from = above && layout.closed.get(rowId(above));
792
+ const by = above && from ? placeAt(layout, rowId(above), grow).y - from.y : 0;
793
+ return { x: leaving.x, y: leaving.y + by };
794
+ }
795
+ const from = layout.closed.get(id);
796
+ if (slot && from) {
797
+ // why: A node of the still that moves into a slot goes across first, out of the tree's way,
798
+ // and turns down or up to its slot on the way, in one curve.
799
+ const to = grownPlace(layout, id);
800
+ const t = glideAt(layout, id, grow);
801
+ return {
802
+ x: between(from.x, to.x, soft(t / (1 - SINK))),
803
+ y: between(from.y, to.y, soft((t - SINK) / (1 - SINK))),
804
+ };
805
+ }
806
+ const to = grownPlace(layout, id);
807
+ return glide(from ?? to, to, moved);
808
+ }
809
+ /**
810
+ * Whether a node stands still this far grown: closed, what the still shows; then each node once it
811
+ * is out and in its zone, and what it comes out from too. What growing takes away is on its way.
812
+ */
813
+ export function atRest(layout, id, grow) {
814
+ if (layout.leaving.rows.some((row) => rowId(row) === id))
815
+ return grow <= 0;
816
+ if (layout.leaving.feeds.some((feed) => feed.id === id))
817
+ return grow <= 0;
818
+ const anchor = layout.anchors.get(id);
819
+ if (anchor === undefined) {
820
+ const glided = glideAt(layout, id, grow);
821
+ return glided === 0 || glided === 1;
290
822
  }
291
- const to = layout.grown.get(id) ?? { x: 0, y: 0 };
292
- const from = layout.closed.get(id) ?? to;
293
- return { x: between(from.x, to.x, moved), y: between(from.y, to.y, moved) };
823
+ return (drawnAt(layout, id, grow) ?? 1) >= 1 && atRest(layout, anchor, grow);
294
824
  }
295
- /** How far a node growing adds has drawn, 0 to 1; undefined for one the map shows closed. */
825
+ /**
826
+ * How far a node of the still has gone on its glide into place, 0 to 1: it goes once the drawing
827
+ * reaches it, or with the frame where the drawing does not reach it.
828
+ */
829
+ function glideAt(layout, id, grow) {
830
+ const start = layout.gathers.get(id);
831
+ if (start === undefined)
832
+ return clamp(grow / MOVE_TO);
833
+ const land = layout.lands.get(id) ?? start + GLIDE * layout.pace;
834
+ return clamp((grow - start) / (land - start));
835
+ }
836
+ /** How far a node of the still has glided, eased. */
837
+ function glidedAt(layout, id, grow) {
838
+ return soft(glideAt(layout, id, grow));
839
+ }
840
+ /**
841
+ * How far a node growing adds has shown, 0 to 1, once the drawing has reached it; undefined for
842
+ * what the map shows closed.
843
+ */
296
844
  export function drawnAt(layout, id, grow) {
297
- const start = layout.starts.get(id);
298
- return start === undefined ? undefined : clamp((grow - start) / DRAW_SPAN);
299
- }
300
- /** Whether two tokens, at their left sides and middles, touch. */
301
- function touching(a, aWidth, b, bWidth) {
302
- return (a.x < b.x + bWidth + CLEAR &&
303
- b.x < a.x + aWidth + CLEAR &&
304
- Math.abs(a.y - b.y) < 2 * HALF + CLEAR);
305
- }
306
- /** The least growing from which a node stands clear of every other, all the way grown. */
307
- function clearFrom(layout, id, ids, width) {
308
- for (let step = SAMPLES; step >= 0; step -= 1) {
309
- const grow = step / SAMPLES;
310
- const place = placeAt(layout, id, grow);
311
- const hit = ids.some((other) => other !== id && touching(place, width(id), placeAt(layout, other, grow), width(other)));
312
- if (hit)
313
- return (step + 1) / SAMPLES;
314
- }
315
- return 0;
845
+ if (!layout.anchors.has(id))
846
+ return undefined;
847
+ const reach = layout.reach.get(id) ?? DRAW_FROM;
848
+ return clamp((grow - reach) / (SHOW * layout.pace) + MET);
849
+ }
850
+ /**
851
+ * A line's length as the drawing runs it: across, then down, from where its source stands grown to
852
+ * where its target stands when the pen comes, which for a node of the still is where the still holds it.
853
+ */
854
+ function lineLength(layout, source, target) {
855
+ const from = grownPlace(layout, source);
856
+ const to = (glides(layout, target) && layout.closed.get(target)) || grownPlace(layout, target);
857
+ return Math.abs(to.x - from.x) + Math.abs(to.y - from.y);
858
+ }
859
+ /**
860
+ * How far the line from `source` to `target` has drawn, 0 to 1: it runs out from its source at the
861
+ * drawing's pace once its source stands shown and still, easing out of it and into its end. What the still draws closed is drawn.
862
+ */
863
+ export function lineAt(layout, source, target, grow) {
864
+ if (layout.drawnClosed.has(lineKey(source, target)))
865
+ return 1;
866
+ const start = layout.leaves.get(source);
867
+ if (start === undefined)
868
+ return 1;
869
+ const span = lineLength(layout, source, target) * layout.pace;
870
+ return span > 0 ? soft((grow - start) / span) : grow >= start ? 1 : 0;
871
+ }
872
+ /** At most this many lanes are tried in every order. */
873
+ const EVERY_ORDER = 7;
874
+ /** Every order of `items`, the given one first. */
875
+ function* orders(items) {
876
+ if (items.length <= 1) {
877
+ yield items;
878
+ return;
879
+ }
880
+ for (const [index, first] of items.entries()) {
881
+ const rest = items.filter((_, other) => other !== index);
882
+ for (const tail of orders(rest))
883
+ yield [first, ...tail];
884
+ }
885
+ }
886
+ /**
887
+ * Where two lines' runs cross, one's level runs over the other's lane. A line whose source has a
888
+ * line down the other lane meets it there, where its own line joins, so it does not cross it.
889
+ */
890
+ function crossings(a, b) {
891
+ const across = (level, down) => level.reduce((count, line) => count +
892
+ down.filter((other) => !down.some((each) => each.source === line.source) &&
893
+ [
894
+ [line.from.y, line.from.x, line.lane],
895
+ [line.to.y, line.lane, line.to.x],
896
+ ].some(([y = 0, left = 0, right = 0]) => other.lane > Math.min(left, right) &&
897
+ other.lane < Math.max(left, right) &&
898
+ y > Math.min(other.from.y, other.to.y) &&
899
+ y < Math.max(other.from.y, other.to.y))).length, 0);
900
+ return across(a, b) + across(b, a);
901
+ }
902
+ /**
903
+ * Each feed's lane, in the gap left of its column: lines into one feed share it, and the lanes go
904
+ * in the order that crosses fewest.
905
+ */
906
+ function lanesOf(layout, width) {
907
+ const lanes = new Map();
908
+ /** Per column, where a line steps aside in the gap before it: left of every lane there. */
909
+ const steps = new Map();
910
+ const grown = (id) => placeAt(layout, id, 1);
911
+ const root = (id) => {
912
+ const slot = layout.slots.get(id);
913
+ return slot ? root(slot.holder) : id;
914
+ };
915
+ const rights = (ids) => Math.max(0, ...ids.map((id) => grown(id).x + width(id)));
916
+ [1, 2].forEach((column) => {
917
+ const inColumn = layout.feeds.filter((feed) => feed.column === column);
918
+ const head = inColumn.find((feed) => !layout.slots.has(feed.id));
919
+ if (!head)
920
+ return;
921
+ const left = grown(head.id).x;
922
+ const before = column === 1
923
+ ? layout.rows.map(rowId)
924
+ : layout.feeds.filter((feed) => feed.column === column - 1).map((feed) => feed.id);
925
+ const space = left - rights(before);
926
+ // why: As in the studio's map, the lines that share an end run as one: the end most lines share
927
+ // takes them as its hub, then the end most of the rest share, a feed before a source when even.
928
+ const links = inColumn.flatMap((feed) => [...feed.from, ...(feed.late ?? [])]
929
+ .filter((source) => !beside(layout, source, feed))
930
+ // why: A line level with its feed runs straight across and needs no lane of its own.
931
+ .filter((source) => Math.abs(grown(source).y - grown(feed.id).y) >= 1)
932
+ .map((source) => ({ source, feed: feed.id })));
933
+ const hubs = new Map();
934
+ let open = links;
935
+ while (open.length > 0) {
936
+ const ends = new Map();
937
+ for (const link of open) {
938
+ for (const end of [`>${link.feed}`, `${link.source}>`]) {
939
+ ends.set(end, [...(ends.get(end) ?? []), link]);
940
+ }
941
+ }
942
+ const feedEnd = (end) => end.startsWith('>');
943
+ const [hub, taken] = [...ends].reduce((most, each) => each[1].length > most[1].length ||
944
+ (each[1].length === most[1].length && feedEnd(each[0]) && !feedEnd(most[0]))
945
+ ? each
946
+ : most);
947
+ hubs.set(hub, taken);
948
+ open = open.filter((link) => !taken.includes(link));
949
+ }
950
+ const middle = (group) => group.reduce((sum, link) => sum + grown(link.feed).y, 0) / group.length;
951
+ const runs = (group, lane) => group.map(({ source, feed }) => ({
952
+ source,
953
+ from: { x: grown(source).x + width(source), y: grown(source).y },
954
+ to: grown(feed),
955
+ lane,
956
+ }));
957
+ // The first lane is the nearest its column; to start, the top hub's.
958
+ const order = [...hubs.values()].toSorted((a, b) => middle(a) - middle(b));
959
+ const laneAt = (index) => left - (space * (index + 1)) / (order.length + 1);
960
+ const cost = () => order.reduce((sum, feed, index) => sum +
961
+ order
962
+ .slice(index + 1)
963
+ .reduce((more, other, offset) => more +
964
+ crossings(runs(feed, laneAt(index)), runs(other, laneAt(index + 1 + offset))), 0), 0);
965
+ let best = cost();
966
+ // why: Few lanes are tried in every order, since swapping neighbours stops where a lane must
967
+ // pass two others to cross fewer; many, by swapping neighbours.
968
+ if (order.length <= EVERY_ORDER) {
969
+ const start = [...order];
970
+ for (const each of orders(start)) {
971
+ order.splice(0, order.length, ...each);
972
+ const next = cost();
973
+ if (next < best) {
974
+ best = next;
975
+ start.splice(0, start.length, ...each);
976
+ }
977
+ }
978
+ order.splice(0, order.length, ...start);
979
+ }
980
+ for (let pass = 0, swapped = true; swapped && pass < order.length; pass += 1) {
981
+ swapped = false;
982
+ for (let index = 0; index + 1 < order.length; index += 1) {
983
+ const [a, b] = [order[index], order[index + 1]];
984
+ if (!a || !b)
985
+ continue;
986
+ order[index] = b;
987
+ order[index + 1] = a;
988
+ const next = cost();
989
+ if (next < best) {
990
+ best = next;
991
+ swapped = true;
992
+ }
993
+ else {
994
+ order[index] = a;
995
+ order[index + 1] = b;
996
+ }
997
+ }
998
+ }
999
+ order.forEach((group, index) => {
1000
+ const of = root(group[0]?.feed ?? head.id);
1001
+ for (const { source, feed } of group) {
1002
+ lanes.set(lineKey(source, feed), { of, by: grown(of).x - laneAt(index) });
1003
+ }
1004
+ });
1005
+ steps.set(column, { of: head.id, by: space - space / (order.length + 1) / 2 });
1006
+ });
1007
+ return { lanes, jogs: jogsOf(layout, width, lanes, steps) };
1008
+ }
1009
+ /** Whether `source` stands in `feed`'s own column: its line runs out to the right of the column and back. */
1010
+ export function beside(layout, source, feed) {
1011
+ return layout.feeds.some((each) => each.id === source && each.column === feed.column);
1012
+ }
1013
+ /** The key of the line from `source` into `feed`. */
1014
+ export const lineKey = (source, feed) => `${source}>${feed}`;
1015
+ /**
1016
+ * Where a line would run level behind a token, or along another feed's last run, it steps half a
1017
+ * row aside in the gap before it and runs on between the rows.
1018
+ */
1019
+ function jogsOf(layout, width, lanes, steps) {
1020
+ const grown = (id) => placeAt(layout, id, 1);
1021
+ const laneX = (lane) => grown(lane.of).x - lane.by;
1022
+ const columnOf = new Map(layout.feeds.map((feed) => [feed.id, feed.column]));
1023
+ const band = HALF + CLEAR;
1024
+ const nodes = [...layout.rows.map(rowId), ...layout.feeds.map((feed) => feed.id)];
1025
+ const jogs = new Map();
1026
+ for (const feed of layout.feeds) {
1027
+ const end = grown(feed.id);
1028
+ // What a level run can't pass: other tokens, and the last runs into the feeds beside this one,
1029
+ // but a run from its own source, which it shares.
1030
+ const blocks = [
1031
+ ...nodes.map((id) => ({
1032
+ id,
1033
+ from: undefined,
1034
+ left: grown(id).x,
1035
+ right: grown(id).x + width(id),
1036
+ reach: band,
1037
+ })),
1038
+ ...layout.feeds
1039
+ .filter((other) => other.column === feed.column && other.id !== feed.id)
1040
+ .flatMap((other) => [...other.from, ...(other.late ?? [])].flatMap((source) => {
1041
+ const its = lanes.get(lineKey(source, other.id));
1042
+ return its
1043
+ ? [
1044
+ {
1045
+ id: other.id,
1046
+ from: source,
1047
+ left: laneX(its),
1048
+ right: grown(other.id).x,
1049
+ reach: HALF,
1050
+ },
1051
+ ]
1052
+ : [];
1053
+ })),
1054
+ ];
1055
+ for (const source of [...feed.from, ...(feed.late ?? [])]) {
1056
+ if (beside(layout, source, feed))
1057
+ continue;
1058
+ const lane = lanes.get(lineKey(source, feed.id));
1059
+ if (!lane)
1060
+ continue;
1061
+ const rail = laneX(lane);
1062
+ const aside = [];
1063
+ let x = grown(source).x + width(source);
1064
+ let y = grown(source).y;
1065
+ for (let tries = 0; tries < 3; tries += 1) {
1066
+ const hit = blocks
1067
+ .filter((block) => block.id !== source &&
1068
+ block.id !== feed.id &&
1069
+ block.from !== source &&
1070
+ block.right > x &&
1071
+ block.left < rail &&
1072
+ Math.abs(grown(block.id).y - y) < block.reach)
1073
+ .toSorted((a, b) => a.left - b.left)[0];
1074
+ if (!hit)
1075
+ break;
1076
+ const step = steps.get(columnOf.get(hit.id) ?? 0);
1077
+ if (!step || laneX(step) <= x)
1078
+ break;
1079
+ const side = end.y >= grown(hit.id).y ? 1 : -1;
1080
+ aside.push({ lane: step, near: hit.id, side });
1081
+ x = laneX(step);
1082
+ y = grown(hit.id).y + (side * layout.row) / 2;
1083
+ }
1084
+ if (aside.length > 0)
1085
+ jogs.set(lineKey(source, feed.id), aside);
1086
+ }
1087
+ }
1088
+ return jogs;
1089
+ }
1090
+ /**
1091
+ * What each node growing adds comes out from under: a row, the row above it; a feed under another,
1092
+ * the one above it there, else that one; any other feed, the first it has a line from, else the
1093
+ * first under it (or, when that comes out too, the first that one has a line from), else the feed
1094
+ * before it.
1095
+ */
1096
+ function anchorsOf(rows, feeds, slots, unfolds, few) {
1097
+ const anchors = new Map();
1098
+ const ids = rows.map(rowId);
1099
+ for (const id of unfolds) {
1100
+ const above = ids[ids.indexOf(id) - 1];
1101
+ if (above !== undefined)
1102
+ anchors.set(id, above);
1103
+ }
1104
+ for (const [index, feed] of feeds.entries()) {
1105
+ if (few.has(feed.id))
1106
+ continue;
1107
+ const under = [...slots].find(([, slot]) => slot.holder === feed.id)?.[0];
1108
+ const slot = slots.get(feed.id);
1109
+ const above = slot &&
1110
+ [...slots].find(([, each]) => each.holder === slot.holder && each.down === slot.down - 1)?.[0];
1111
+ const first = under === undefined || few.has(under)
1112
+ ? under
1113
+ : feeds.find((each) => each.id === under)?.from[0];
1114
+ const anchor = above ?? slot?.holder ?? feed.from[0] ?? first ?? feeds[index - 1]?.id ?? ids[0];
1115
+ if (anchor !== undefined)
1116
+ anchors.set(feed.id, anchor);
1117
+ }
1118
+ return anchors;
1119
+ }
1120
+ /**
1121
+ * Closed, the still: the rows down the map, a gap before each agent after the first, and beside
1122
+ * them, at `x`, the feeds it shows: the runs one under another, a row's gap between them, centred
1123
+ * on the rows.
1124
+ */
1125
+ function stillPlaces(rows, few, x) {
1126
+ const ids = new Set(rows.map(rowId));
1127
+ const runs = new Map();
1128
+ for (const feed of few) {
1129
+ const source = feed.from.find((id) => ids.has(id));
1130
+ if (source !== undefined)
1131
+ runs.set(source, [...(runs.get(source) ?? []), feed]);
1132
+ }
1133
+ let y = PAD + HALF;
1134
+ const placed = rows.map((row, index) => {
1135
+ if (index > 0 && row.parent === undefined)
1136
+ y += AGENT_GAP;
1137
+ const at = { ...row, y };
1138
+ y += ROW;
1139
+ return at;
1140
+ });
1141
+ const own = placed.flatMap((row) => {
1142
+ const run = runs.get(rowId(row)) ?? [];
1143
+ return run.length > 0 ? [run] : [];
1144
+ });
1145
+ // The runs one under another, a row's gap between them, their middle level with the rows' middle.
1146
+ const slots = own.reduce((sum, run) => sum + run.length + 1, -1);
1147
+ const [first, last] = [placed[0]?.y ?? 0, placed.at(-1)?.y ?? 0];
1148
+ let top = (first + last) / 2 - ((slots - 1) * ROW) / 2;
1149
+ const feeds = new Map();
1150
+ for (const run of own) {
1151
+ for (const [step, feed] of run.entries())
1152
+ feeds.set(feed.id, { x, y: top + step * ROW });
1153
+ top += (run.length + 1) * ROW;
1154
+ }
1155
+ return { rows: placed, feeds };
1156
+ }
1157
+ /** Closed, the rows and feeds given a place, each where it is given. */
1158
+ function givenPlaces(rows, feeds, given) {
1159
+ return {
1160
+ rows: rows.map((row) => ({ ...row, ...given.get(rowId(row)) })),
1161
+ feeds: new Map(feeds.flatMap((feed) => {
1162
+ const place = given.get(feed.id);
1163
+ return place ? [[feed.id, place]] : [];
1164
+ })),
1165
+ };
1166
+ }
1167
+ /**
1168
+ * Closed, the x each row's lines run down to its feeds, together: left of the feeds, a lane
1169
+ * further left for each trunk from a row above it that it would run beside, so two rows' lines meet
1170
+ * only at a feed both have.
1171
+ */
1172
+ function trunksOf(lines, closed, x) {
1173
+ const spans = new Map();
1174
+ for (const { source, feed } of lines) {
1175
+ const ys = [closed.get(source)?.y, closed.get(feed)?.y].filter((y) => y !== undefined);
1176
+ const [top, bottom] = spans.get(source) ?? [Infinity, -Infinity];
1177
+ spans.set(source, [Math.min(top, ...ys), Math.max(bottom, ...ys)]);
1178
+ }
1179
+ const taken = [];
1180
+ const trunks = new Map();
1181
+ // why: A lower row's trunk stands further left, so its line in runs short of the trunks above.
1182
+ const fromTop = [...spans].sort(([, [a]], [, [b]]) => a - b);
1183
+ for (const [source, [top, bottom]] of fromTop) {
1184
+ let lane = 0;
1185
+ while (taken.some((each) => each.lane === lane && each.top <= bottom && top <= each.bottom))
1186
+ lane += 1;
1187
+ taken.push({ lane, top, bottom });
1188
+ trunks.set(source, x - TRUNK_GAP - lane * LANE);
1189
+ }
1190
+ return trunks;
1191
+ }
1192
+ /** Every line the map draws, from the node it leaves to the node at its end. */
1193
+ function edgesOf(rows, feeds) {
1194
+ return [
1195
+ ...rows.flatMap((each) => (each.parent === undefined ? [] : [[each.parent, rowId(each)]])),
1196
+ ...feeds.flatMap((feed) => [
1197
+ ...[...feed.from, ...(feed.late ?? [])].map((source) => [source, feed.id]),
1198
+ ...(feed.under === undefined ? [] : [[feed.under, feed.id]]),
1199
+ ]),
1200
+ ];
1201
+ }
1202
+ /** Whether a node is of the still and goes somewhere growing, so it glides when the drawing comes. */
1203
+ function glides(layout, id) {
1204
+ const closed = layout.closed.get(id);
1205
+ if (!closed || layout.anchors.has(id))
1206
+ return false;
1207
+ const grown = grownPlace(layout, id);
1208
+ return Math.hypot(grown.x - closed.x, grown.y - closed.y) > 1;
1209
+ }
1210
+ /**
1211
+ * Settles a node the drawing has reached: when it stands ready, and when its lines leave. A node
1212
+ * growing adds is well on its way to shown; a node of the still that moves glides into place once
1213
+ * its line has reached it, the line coming with it.
1214
+ */
1215
+ function settle(layout, runs, id) {
1216
+ const ran = runs.ran.get(id) ?? 0;
1217
+ let ready = ran + (layout.anchors.has(id) ? SHOW * (SHOWN - MET) : 0);
1218
+ if (runs.from.has(id) && glides(layout, id)) {
1219
+ runs.gather.set(id, ran);
1220
+ ready = ran + GLIDE;
1221
+ }
1222
+ runs.ready.set(id, ready);
1223
+ runs.leave.set(id, ready + PAUSE);
1224
+ }
1225
+ /**
1226
+ * The pen's runs to every node, by the shortest run of line from the tree's agents.
1227
+ */
1228
+ function runsOf(layout, edges) {
1229
+ const runs = {
1230
+ ran: new Map(),
1231
+ from: new Map(),
1232
+ ready: new Map(),
1233
+ leave: new Map(),
1234
+ gather: new Map(),
1235
+ land: new Map(),
1236
+ };
1237
+ const queue = layout.rows.filter((each) => each.parent === undefined).map(rowId);
1238
+ for (const id of queue)
1239
+ runs.ran.set(id, 0);
1240
+ while (queue.length > 0) {
1241
+ queue.sort((a, b) => (runs.ran.get(a) ?? 0) - (runs.ran.get(b) ?? 0));
1242
+ const id = queue.shift();
1243
+ if (runs.leave.has(id))
1244
+ continue;
1245
+ settle(layout, runs, id);
1246
+ for (const [source, target] of edges.filter(([each]) => each === id)) {
1247
+ const to = (runs.leave.get(id) ?? 0) + lineLength(layout, source, target);
1248
+ if (to >= (runs.ran.get(target) ?? Infinity))
1249
+ continue;
1250
+ runs.ran.set(target, to);
1251
+ runs.from.set(target, source);
1252
+ queue.push(target);
1253
+ }
1254
+ }
1255
+ reachUnlined(layout, runs);
1256
+ return runs;
1257
+ }
1258
+ /** What the pen still has not reached is reached from what it grows from, once that node's lines leave. */
1259
+ function reachUnlined(layout, runs) {
1260
+ for (const id of layout.anchors.keys()) {
1261
+ if (runs.ran.has(id))
1262
+ continue;
1263
+ let anchor = layout.anchors.get(id);
1264
+ while (anchor !== undefined && !runs.ran.has(anchor))
1265
+ anchor = layout.anchors.get(anchor);
1266
+ if (anchor !== undefined)
1267
+ runs.from.set(id, anchor);
1268
+ const leaves = anchor === undefined ? 0 : (runs.leave.get(anchor) ?? 0);
1269
+ runs.ran.set(id, leaves + (anchor === undefined ? 0 : lineLength(layout, anchor, id)));
1270
+ settle(layout, runs, id);
1271
+ }
1272
+ }
1273
+ /**
1274
+ * Nodes of the still a line reaches together glide as one, once their line reaches them, or sooner
1275
+ * where something would show where one of them stands: they are on their way as it comes, and their
1276
+ * line catches them up.
1277
+ */
1278
+ function makeRoom(layout, runs, width) {
1279
+ const box = (id, place) => ({
1280
+ left: place.x - ROOM,
1281
+ right: place.x + width(id) + ROOM,
1282
+ top: place.y - HALF - CLEAR,
1283
+ bottom: place.y + HALF + CLEAR,
1284
+ });
1285
+ const meet = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
1286
+ const comes = (id) => runs.gather.get(id) ?? runs.ran.get(id) ?? 0;
1287
+ const coming = [...new Set([...layout.anchors.keys(), ...layout.closed.keys()])].map((id) => ({
1288
+ id,
1289
+ at: comes(id) - (layout.anchors.has(id) ? SHOW * MET : 0),
1290
+ edges: box(id, grownPlace(layout, id)),
1291
+ }));
1292
+ const groups = Map.groupBy(runs.gather.keys(), (id) => runs.from.get(id) ?? id);
1293
+ for (const members of groups.values()) {
1294
+ const own = Math.min(...members.map(comes));
1295
+ const before = members.flatMap((id) => {
1296
+ const stands = box(id, layout.closed.get(id));
1297
+ return coming
1298
+ .filter((each) => !members.includes(each.id) && each.at < own && meet(each.edges, stands))
1299
+ .map((each) => each.at - GLIDE * AHEAD);
1300
+ });
1301
+ const start = Math.min(own, ...before);
1302
+ // why: What would land on it with the frame is made room for with the frame. Gone sooner, it
1303
+ // goes slower, and lands as its line reaches it.
1304
+ for (const id of members) {
1305
+ const reached = runs.ran.get(id) ?? 0;
1306
+ if (start <= 0)
1307
+ runs.gather.delete(id);
1308
+ else
1309
+ runs.gather.set(id, start);
1310
+ if (start < own)
1311
+ runs.land.set(id, Math.max(start + GLIDE, reached + GLIDE * LAND));
1312
+ }
1313
+ }
1314
+ }
1315
+ /**
1316
+ * When the drawing reaches each node, when each node's lines leave it, and when each node of the
1317
+ * still glides into place.
1318
+ *
1319
+ * why: The map draws out from the tree's agents as one pen would, at an even pace, and nothing
1320
+ * moves before the pen comes to it: down the tree to each row, and along each line to what it
1321
+ * feeds, so a node is reached by the shortest run of line to it. A node shows as its line reaches
1322
+ * it; what the still holds stands until its line reaches it, then glides into place with it.
1323
+ */
1324
+ function drawingOf(layout, width) {
1325
+ const edges = edgesOf(layout.rows, layout.feeds);
1326
+ // why: What no line runs into is reached from what it grows from, as the pen goes, so the lines
1327
+ // it sends out run in their turn with the rest.
1328
+ const lined = new Set(edges.map(([, target]) => target));
1329
+ const jumps = [...layout.anchors]
1330
+ .filter(([id]) => !lined.has(id))
1331
+ .map(([id, anchor]) => [anchor, id]);
1332
+ const runs = runsOf(layout, [...edges, ...jumps]);
1333
+ makeRoom(layout, runs, width);
1334
+ const ends = edges
1335
+ .filter(([source]) => runs.leave.has(source))
1336
+ .map(([source, target]) => (runs.leave.get(source) ?? 0) + lineLength(layout, source, target));
1337
+ const farthest = Math.max(1, ...[...runs.ran.values()].map((at) => at + SHOW), ...ends);
1338
+ const pace = (1 - DRAW_FROM) / Math.max(farthest, ...runs.ready.values());
1339
+ const grown = (runs) => new Map([...runs].map(([id, run]) => [id, DRAW_FROM + run * pace]));
1340
+ return {
1341
+ reach: grown(runs.ran),
1342
+ reachedFrom: runs.from,
1343
+ leaves: grown(runs.leave),
1344
+ gathers: grown(runs.gather),
1345
+ lands: grown(runs.land),
1346
+ pace,
1347
+ };
316
1348
  }
317
1349
  /**
318
1350
  * The tree and what feeds it, closed and grown, for tokens this wide: grown, it fills `box` when
319
1351
  * one is given.
320
1352
  */
321
- export function treeMapLayout(workspace, agents, width, box) {
1353
+ export function treeMapLayout(workspace, agents, width, box,
1354
+ /** The agents the map shows closed, when not the tree's. */
1355
+ still,
1356
+ /** Where the tokens the map shows closed stand, by id, when not where the still puts them. */
1357
+ given) {
322
1358
  const rows = rowsOf(workspace, agents);
323
1359
  const feeds = feedsOf(workspace, rows);
324
- // Closed, the first column's heads stand beside the tree, each level with the first row it
325
- // joins or under the one before.
326
- const fewFeeds = feeds.filter((feed) => feed.column === 1 && feed.under === undefined);
327
- const few = new Set(fewFeeds.map((feed) => feed.id));
328
- const feedX = Math.max(0, ...rows.map((row) => row.x + width(rowId(row)))) + GAP;
329
- const closed = new Map(rows.map((row) => [rowId(row), row]));
330
- const rowY = new Map(rows.map((row) => [rowId(row), row.y]));
331
- let below = Number.NEGATIVE_INFINITY;
332
- for (const feed of fewFeeds) {
333
- const y = Math.max(rowY.get(feed.into[0] ?? '') ?? below, below);
334
- closed.set(feed.id, { x: feedX, y });
335
- below = y + ROW;
336
- }
337
- const slots = slotsOf(feeds, width);
338
- const { places, row } = grownPlaces(rows, feeds, slots, width, box);
1360
+ // Closed, the still: what is given, else its own agents and rows, or the tree's, and what feeds them.
1361
+ const stillRows = given
1362
+ ? rows.filter((row) => given.has(rowId(row)))
1363
+ : still
1364
+ ? rowsOf(workspace, still)
1365
+ : rows;
1366
+ const stillFeeds = given
1367
+ ? feeds.filter((feed) => given.has(feed.id))
1368
+ : still
1369
+ ? feedsOf(workspace, stillRows)
1370
+ : feeds;
1371
+ const grownRows = new Set(rows.map(rowId));
1372
+ const grownFeeds = new Set(feeds.map((feed) => feed.id));
1373
+ // why: An agent of the still that the tree draws as an agent tool's agent glides into it.
1374
+ const becomes = new Map();
1375
+ for (const row of stillRows) {
1376
+ if (row.parent !== undefined || grownRows.has(rowId(row)))
1377
+ continue;
1378
+ const spec = workspace.toolSpecs.find((each) => each.agentKey === row.agent);
1379
+ const feed = spec ? `tool:${spec.key}/agent` : undefined;
1380
+ if (feed && grownFeeds.has(feed))
1381
+ becomes.set(rowId(row), feed);
1382
+ }
1383
+ // Closed, each agent's models and tools stand beside the tree, those with a line from a row of it.
1384
+ const feedX = Math.max(0, ...stillRows.map((row) => row.x + width(rowId(row)))) + GAP;
1385
+ const placed = given
1386
+ ? givenPlaces(stillRows, stillFeeds, given)
1387
+ : stillPlaces(stillRows, CLOSED.flatMap((kind) => stillFeeds.filter((feed) => feed.kind === kind)), feedX);
1388
+ const few = new Set([...placed.feeds.keys(), ...becomes.values()]);
1389
+ const closed = new Map(placed.rows.map((row) => [rowId(row), row]));
1390
+ for (const [row, feed] of becomes) {
1391
+ const place = closed.get(row);
1392
+ if (place)
1393
+ closed.set(feed, place);
1394
+ }
1395
+ // why: A row the still does not show unfolds from under the row above it, so it moves with the
1396
+ // tree and shows as it comes apart, not into a gap opened for it.
1397
+ const unfolds = rows.map(rowId).filter((id) => !closed.has(id));
1398
+ rows.forEach((row, index) => {
1399
+ if (closed.has(rowId(row)))
1400
+ return;
1401
+ const above = rows
1402
+ .slice(0, index)
1403
+ .map((each) => closed.get(rowId(each)))
1404
+ .findLast((place) => place !== undefined);
1405
+ if (above)
1406
+ closed.set(rowId(row), { x: above.x, y: above.y });
1407
+ });
1408
+ for (const [id, place] of placed.feeds)
1409
+ closed.set(id, place);
1410
+ const slots = slotsOf(feeds);
1411
+ const { places, row, headings } = grownPlaces(rows, feeds, slots, width, box);
1412
+ // What the still shows and the tree does not: it stays where it stood closed as it goes.
1413
+ const shownClosed = (feed) => few.has(feed.id);
1414
+ const stillIds = new Set(stillRows.map(rowId));
1415
+ const stillLines = stillFeeds
1416
+ .filter(shownClosed)
1417
+ .flatMap((feed) => feed.from
1418
+ .filter((source) => stillIds.has(source))
1419
+ .map((source) => ({ source, feed: feed.id })));
1420
+ const grownLines = new Set(feeds.flatMap((feed) => feed.from.map((source) => lineKey(source, feed.id))));
1421
+ const leaving = {
1422
+ rows: placed.rows.filter((each) => !grownRows.has(rowId(each)) && !becomes.has(rowId(each))),
1423
+ feeds: stillFeeds.filter((feed) => few.has(feed.id) && !grownFeeds.has(feed.id)),
1424
+ lines: stillLines.filter((line) => !grownLines.has(lineKey(line.source, line.feed))),
1425
+ };
1426
+ for (const each of leaving.rows)
1427
+ places.set(rowId(each), closed.get(rowId(each)) ?? each);
1428
+ for (const feed of leaving.feeds) {
1429
+ const place = closed.get(feed.id);
1430
+ if (place)
1431
+ places.set(feed.id, place);
1432
+ }
339
1433
  const layout = {
340
1434
  rows,
341
1435
  feeds,
@@ -343,15 +1437,23 @@ export function treeMapLayout(workspace, agents, width, box) {
343
1437
  closed,
344
1438
  grown: places,
345
1439
  row,
1440
+ headings,
346
1441
  slots,
347
- starts: new Map(),
1442
+ reach: new Map(),
1443
+ reachedFrom: new Map(),
1444
+ leaves: new Map(),
1445
+ gathers: new Map(),
1446
+ lands: new Map(),
1447
+ pace: 0,
1448
+ drawnClosed: new Set(stillLines.map((line) => lineKey(line.source, line.feed))),
1449
+ anchors: anchorsOf(rows, feeds, slots, unfolds, few),
1450
+ lanes: new Map(),
1451
+ jogs: new Map(),
1452
+ leaving,
1453
+ becomes,
1454
+ trunks: trunksOf(stillLines, closed, feedX),
348
1455
  };
349
- // What growing adds draws in its columns' order, each once it stands clear.
350
- const order = feeds.filter((feed) => !few.has(feed.id));
351
- const ids = [...rows.map(rowId), ...feeds.map((feed) => feed.id)];
352
- order.forEach((feed, index) => {
353
- const turn = DRAW_FROM + ((DRAW_TO - DRAW_SPAN - DRAW_FROM) * index) / Math.max(order.length - 1, 1);
354
- layout.starts.set(feed.id, Math.max(turn, clearFrom(layout, feed.id, ids, width)));
355
- });
1456
+ Object.assign(layout, lanesOf(layout, width));
1457
+ Object.assign(layout, drawingOf(layout, width));
356
1458
  return layout;
357
1459
  }