roboticus-api 0.11.3

HTTP routes, WebSocket, auth, rate limiting, and dashboard for the Roboticus agent runtime
Documentation
App._ctxSession = null;
App._ctxTurns = [];
App._ctxActiveTurn = null;
App._liveStreamTurn = null;
App.renderContext = function() {
      var self = this;
      return api('/api/sessions').then(function(data) {
        var sessions = (data.sessions || []).filter(function(s) { return s.status === 'active'; });
        var liveBanner = '';
        if (self._liveStreamTurn && self._liveStreamTurn.turn_id) {
          liveBanner = '<div class="card" style="margin-bottom:0.75rem;padding:0.625rem 0.75rem;background:rgba(193,128,255,0.08);border-color:rgba(193,128,255,0.3)">'
            + '<div style="display:flex;align-items:center;justify-content:space-between;gap:0.75rem;flex-wrap:wrap">'
            + '<div style="font-size:0.75rem;color:var(--muted)">'
            + '<span class="badge" style="background:var(--accent);color:#fff;font-size:0.55rem;margin-right:0.35rem">LIVE</span>'
            + 'Active stream turn <span class="card-mono" style="font-size:0.7rem">' + esc(truncate(self._liveStreamTurn.turn_id, 16)) + '</span>'
            + (self._liveStreamTurn.model ? ' · model <span class="card-mono" style="font-size:0.7rem">' + esc(self._liveStreamTurn.model) + '</span>' : '')
            + '</div>'
            + '<div style="display:flex;align-items:center;gap:0.375rem">'
            + '<button class="btn secondary" id="ctx-copy-live-turn" style="font-size:0.6875rem;padding:0.25rem 0.6rem" data-turn-id="' + esc(self._liveStreamTurn.turn_id) + '">Copy turn ID</button>'
            + '<button class="btn secondary" id="ctx-open-live-turn" style="font-size:0.6875rem;padding:0.25rem 0.6rem" data-turn-id="' + esc(self._liveStreamTurn.turn_id) + '" data-session-id="' + esc(self._liveStreamTurn.session_id || '') + '">Open forensic detail</button>'
            + '</div>'
            + '</div></div>';
        }

        if (self._ctxSession && self._ctxActiveTurn) {
          return Promise.all([
            api('/api/turns/' + encodeURIComponent(self._ctxActiveTurn.id)).catch(function() { return null; }),
            api('/api/turns/' + encodeURIComponent(self._ctxActiveTurn.id) + '/context').catch(function() { return null; }),
            api('/api/turns/' + encodeURIComponent(self._ctxActiveTurn.id) + '/tools').catch(function() { return { tool_calls: [] }; }),
            api('/api/turns/' + encodeURIComponent(self._ctxActiveTurn.id) + '/tips').catch(function() { return { tips: [] }; }),
            api('/api/turns/' + encodeURIComponent(self._ctxActiveTurn.id) + '/model-selection').catch(function() { return null; })
          ]).then(function(results) {
            var turn = results[0], ctx = results[1], tools = results[2], tipsData = results[3], modelSel = results[4];
            var detailHtml = '';
            if (ctx && ctx.snapshot !== false) {
              var budget = ctx.token_budget || 1;
              var sysPct = Math.round(((ctx.system_prompt_tokens || 0) / budget) * 100);
              var memPct = Math.round(((ctx.memory_tokens || 0) / budget) * 100);
              var histPct = Math.round(((ctx.history_tokens || 0) / budget) * 100);
              var freePct = Math.max(0, 100 - sysPct - memPct - histPct);
              detailHtml += '<div class="ctx-stats">';
              detailHtml += '<div class="ctx-stat"><div class="val">' + esc(ctx.complexity_level) + '</div><div class="lbl">Level</div></div>';
              detailHtml += '<div class="ctx-stat"><div class="val">' + (ctx.token_budget || 0).toLocaleString() + '</div><div class="lbl">Budget</div></div>';
              detailHtml += '<div class="ctx-stat"><div class="val">' + (ctx.history_depth || 0) + '</div><div class="lbl">History Depth</div></div>';
              if (turn) {
                detailHtml += '<div class="ctx-stat"><div class="val">' + ((turn.tokens_in || 0) + (turn.tokens_out || 0)).toLocaleString() + '</div><div class="lbl">Tokens</div></div>';
                detailHtml += '<div class="ctx-stat"><div class="val">$' + (turn.cost || 0).toFixed(4) + '</div><div class="lbl">Cost</div></div>';
              }
              detailHtml += '</div>';
              detailHtml += '<h4 style="margin:0 0 0.25rem;font-size:0.75rem;color:var(--muted)">TOKEN ALLOCATION</h4>';
              detailHtml += '<div class="ctx-bar"><span class="sys" style="width:' + sysPct + '%"></span><span class="mem" style="width:' + memPct + '%"></span><span class="hist" style="width:' + histPct + '%"></span><span class="free" style="width:' + freePct + '%"></span></div>';
              detailHtml += '<div class="ctx-legend"><span class="l-sys">System ' + (ctx.system_prompt_tokens || 0) + '</span><span class="l-mem">Memory ' + (ctx.memory_tokens || 0) + '</span><span class="l-hist">History ' + (ctx.history_tokens || 0) + '</span></div>';
              if (ctx.model) detailHtml += '<p style="margin:0.75rem 0 0;font-size:0.75rem;color:var(--muted)">Model: <span style="color:var(--text)">' + esc(ctx.model) + '</span></p>';
            } else {
              detailHtml += '<p style="color:var(--muted)">No context snapshot recorded for this turn.</p>';
            }
            if (turn && turn.thinking) {
              detailHtml += '<details class="ctx-section"><summary>Reasoning Trace</summary><div>' + renderSafeMarkdown(turn.thinking) + '</div></details>';
            }
            var toolCalls = (tools && tools.tool_calls) || [];
            if (toolCalls.length > 0) {
              detailHtml += '<details class="ctx-section"><summary>Tool Calls (' + toolCalls.length + ')</summary>';
              toolCalls.forEach(function(tc) {
                detailHtml += '<div style="margin:0.375rem 0;padding:0.375rem;background:rgba(0,0,0,0.15);border-radius:3px;font-size:0.75rem">';
                detailHtml += '<span class="badge ' + (tc.status === 'success' ? 'success' : 'error') + '" style="font-size:0.5625rem;margin-right:0.25rem">' + esc(tc.status) + '</span>';
                detailHtml += '<strong>' + esc(tc.tool_name) + '</strong>';
                if (tc.duration_ms != null) detailHtml += ' <span style="color:var(--muted)">' + tc.duration_ms + 'ms</span>';
                detailHtml += '</div>';
              });
              detailHtml += '</details>';
            }
            var turnTips = (tipsData && tipsData.tips) || [];
            if (turnTips.length > 0) {
              detailHtml += '<h4 style="margin:0.75rem 0 0.25rem;font-size:0.75rem;color:var(--muted)">ANALYSIS TIPS</h4>';
              detailHtml += '<div class="ctx-tips">';
              turnTips.forEach(function(tip) {
                detailHtml += '<span class="ctx-tip ' + esc(tip.severity) + '" title="' + esc(tip.suggestion) + '"><span class="tip-dot"></span>' + esc(tip.message) + '</span>';
              });
              detailHtml += '</div>';
            }
            if (modelSel) {
              var candidates = modelSel.candidates || [];
              detailHtml += '<details class="ctx-section" open><summary>Model Selection Forensics</summary>';
              detailHtml += '<div style="font-size:0.75rem;color:var(--muted);margin:0.35rem 0 0.5rem">'
                + 'Selected <span style="color:var(--text);font-family:var(--mono)">' + esc(modelSel.selected_model || '—') + '</span>'
                + ' via <span class="badge muted" style="font-size:0.6rem">' + esc(modelSel.strategy || 'unknown') + '</span>'
                + (modelSel.complexity ? ' · complexity ' + esc(modelSel.complexity) : '')
                + '</div>';
              if (modelSel.user_excerpt) {
                detailHtml += '<div style="font-size:0.7rem;color:var(--muted);margin-bottom:0.5rem">Task excerpt: <span style="color:var(--text)">' + esc(modelSel.user_excerpt) + '</span></div>';
              }
              if (candidates.length > 0) {
                detailHtml += '<div class="table-wrap"><table><thead><tr><th>Candidate</th><th>Source</th><th>Provider</th><th>Breaker</th><th>Usable</th><th>Reason</th></tr></thead><tbody>';
                candidates.forEach(function(c) {
                  detailHtml += '<tr>'
                    + '<td class="card-mono">' + esc(c.model || '') + '</td>'
                    + '<td>' + esc(c.source || '') + '</td>'
                    + '<td>' + (c.provider_available ? '<span class="badge success">yes</span>' : '<span class="badge error">no</span>') + '</td>'
                    + '<td>' + (c.breaker_blocked ? '<span class="badge error">open</span>' : '<span class="badge success">closed</span>') + '</td>'
                    + '<td>' + (c.usable ? '<span class="badge success">yes</span>' : '<span class="badge error">no</span>') + '</td>'
                    + '<td>' + esc(c.note || '') + '</td>'
                    + '</tr>';
                });
                detailHtml += '</tbody></table></div>';
              }
              detailHtml += '</details>';
            }
            detailHtml += '<button class="btn secondary ctx-analyze-btn" data-analyze-turn="' + esc(self._ctxActiveTurn.id) + '" style="margin-top:0.75rem">Analyze with AI</button>';

            var timelineHtml = self._ctxTurns.map(function(t) {
              var isActive = t.id === self._ctxActiveTurn.id;
              var cost = t.cost != null ? '$' + t.cost.toFixed(4) : '';
              var turnLabel = sessionAssistantLabel(self._ctxSession, 'assistant');
              return '<div class="ctx-timeline-item' + (isActive ? ' active' : '') + '" data-turn-id="' + esc(t.id) + '"><div style="font-size:0.6875rem;color:var(--muted)">' + esc(t.created_at || '') + '</div><div style="display:flex;justify-content:space-between;align-items:center;gap:0.25rem"><span class="badge muted" style="font-size:0.5625rem">' + esc(turnLabel) + '</span><span class="badge muted" style="font-size:0.5625rem">' + esc(t.model || '') + '</span><span style="font-size:0.6875rem;color:var(--success);margin-left:auto">' + cost + '</span><span class="badge muted" style="font-size:0.5625rem">View details \u203a</span></div></div>';
            }).join('');

            return liveBanner + '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem"><button class="btn secondary" style="font-size:0.75rem;padding:0.3rem 0.75rem" id="ctx-back-btn">\u25c0 Back</button><h3 style="margin:0">Turn Detail</h3></div><div class="ctx-explorer-grid"><div class="ctx-timeline">' + timelineHtml + '</div><div class="ctx-turn-detail">' + detailHtml + '</div></div>';
          });
        }

        if (self._ctxSession) {
          return Promise.all([
            api('/api/sessions/' + encodeURIComponent(self._ctxSession.id) + '/turns').catch(function() { return { turns: [] }; }),
            api('/api/sessions/' + encodeURIComponent(self._ctxSession.id) + '/insights').catch(function() { return { insights: [] }; }),
            api('/api/sessions/' + encodeURIComponent(self._ctxSession.id) + '/feedback').catch(function() { return { feedback: [] }; })
          ]).then(function(results) {
            var r = results[0], insightsData = results[1], fbData = results[2];
            self._ctxTurns = r.turns || [];
            var ctxFbMap = {};
            (fbData.feedback || []).forEach(function(fb) { ctxFbMap[fb.turn_id] = fb; });
            if (self._ctxTurns.length === 0) {
              return liveBanner + '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem"><button class="btn secondary" style="font-size:0.75rem;padding:0.3rem 0.75rem" id="ctx-back-btn">\u25c0 Back</button><h3 style="margin:0">' + esc(self._ctxSession.nickname || self._ctxSession.id) + '</h3></div><div class="card"><p style="color:var(--muted)">No turns recorded for this session yet.</p><button class="btn secondary" data-page-nav="sessions">Open Sessions</button></div>';
            }
            var totalCost = 0, totalTokens = 0;
            self._ctxTurns.forEach(function(t) { totalCost += t.cost || 0; totalTokens += (t.tokens_in || 0) + (t.tokens_out || 0); });
            var gradedTurns = Object.keys(ctxFbMap).length;
            var avgGrade = gradedTurns > 0 ? (Object.values(ctxFbMap).reduce(function(s, f) { return s + f.grade; }, 0) / gradedTurns).toFixed(1) : '\u2014';
            var summaryHtml = '<div class="ctx-stats"><div class="ctx-stat"><div class="val">' + self._ctxTurns.length + '</div><div class="lbl">Turns</div></div><div class="ctx-stat"><div class="val">' + totalTokens.toLocaleString() + '</div><div class="lbl">Total Tokens</div></div><div class="ctx-stat"><div class="val">$' + totalCost.toFixed(4) + '</div><div class="lbl">Total Cost</div></div><div class="ctx-stat"><div class="val">' + avgGrade + '</div><div class="lbl">Avg Grade</div></div></div>';
            var insights = (insightsData && insightsData.insights) || [];
            var insightsHtml = '';
            if (insights.length > 0) {
              insightsHtml += '<div class="ctx-insights"><h4 style="margin:0 0 0.5rem;font-size:0.75rem;color:var(--muted)">SESSION INSIGHTS</h4><div class="ctx-tips">';
              insights.forEach(function(tip) {
                insightsHtml += '<span class="ctx-tip ' + esc(tip.severity) + '" title="' + esc(tip.suggestion) + '"><span class="tip-dot"></span>' + esc(tip.message) + '</span>';
              });
              insightsHtml += '</div></div>';
            }
            var timelineHtml = self._ctxTurns.map(function(t) {
              var cost = t.cost != null ? '$' + t.cost.toFixed(4) : '';
              var tokens = ((t.tokens_in || 0) + (t.tokens_out || 0)).toLocaleString();
              var fb = ctxFbMap[t.id];
              var gradeBadge = fb ? '<span class="grade-badge">' + '\u2605'.repeat(fb.grade) + '</span>' : '';
              var turnLabel = sessionAssistantLabel(self._ctxSession, 'assistant');
              return '<div class="ctx-timeline-item" data-turn-id="' + esc(t.id) + '" style="cursor:pointer"><div style="font-size:0.6875rem;color:var(--muted)">' + esc(t.created_at || '') + '</div><div style="display:flex;justify-content:space-between;align-items:center;gap:0.25rem"><span class="badge muted" style="font-size:0.5625rem">' + esc(turnLabel) + '</span><span class="badge muted" style="font-size:0.5625rem">' + esc(t.model || '') + '</span>' + gradeBadge + '<span style="font-size:0.6875rem">' + tokens + ' tokens</span><span style="font-size:0.6875rem;color:var(--success)">' + cost + '</span><span class="badge muted" style="font-size:0.5625rem">View details \u203a</span></div></div>';
            }).join('');
            return liveBanner + '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem"><button class="btn secondary" style="font-size:0.75rem;padding:0.3rem 0.75rem" id="ctx-back-btn">\u25c0 Back</button><h3 style="margin:0">' + esc(self._ctxSession.nickname || self._ctxSession.id) + '</h3></div>' + summaryHtml + insightsHtml + '<div class="ctx-timeline">' + timelineHtml + '</div>';
          });
        }

        var visibleSessions = sessions.filter(function(s) { return (s.turn_count || 0) > 0; });
        var rows = visibleSessions.map(function(s) {
          var nick = s.nickname || truncate(s.id, 16);
          var agentLabel = sessionAssistantLabel(s, s.agent_id || 'default');
          return '<tr data-ctx-session=\'' + esc(JSON.stringify(s)) + '\' style="cursor:pointer"><td>' + esc(nick) + '</td><td><span class="badge muted" style="font-size:0.5625rem">' + esc(agentLabel) + '</span></td><td style="font-size:0.75rem;color:var(--muted)">' + esc(s.created_at || '') + '</td><td><button class="btn secondary" style="font-size:0.65rem;padding:0.2rem 0.5rem">Open</button></td></tr>';
        }).join('');
        if (!rows) {
          return liveBanner + '<h3>Context</h3><p style="color:var(--muted);font-size:0.875rem;margin-bottom:1rem">Select a session to inspect turn-by-turn context allocation, token budgets, and reasoning traces.</p><div class="card"><p style="color:var(--muted)">No sessions with recorded turns yet.</p><button class="btn secondary" data-page-nav="sessions">Go to Sessions</button></div>';
        }
        return liveBanner + '<h3>Context</h3><p style="color:var(--muted);font-size:0.875rem;margin-bottom:1rem">Select a session to inspect turn-by-turn context allocation, token budgets, and reasoning traces.</p><div class="table-wrap"><table><thead><tr><th>Session</th><th>Agent</th><th>Created</th><th>Action</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
      });
};