databricks-agentbricks 0.2.0__py3-none-any.whl
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.
- databricks_agentbricks/__init__.py +1 -0
- databricks_agentbricks/_group.py +217 -0
- databricks_agentbricks/agent_project.py +657 -0
- databricks_agentbricks/app_resources.py +139 -0
- databricks_agentbricks/cli/__init__.py +12 -0
- databricks_agentbricks/cli/app.py +108 -0
- databricks_agentbricks/cli/app_auth.py +198 -0
- databricks_agentbricks/cli/auth.py +142 -0
- databricks_agentbricks/cli/deploy.py +954 -0
- databricks_agentbricks/cli/dev.py +310 -0
- databricks_agentbricks/cli/endpoint.py +145 -0
- databricks_agentbricks/cli/endpoint_examples.py +33 -0
- databricks_agentbricks/cli/endpoint_output.py +53 -0
- databricks_agentbricks/cli/endpoint_request.py +71 -0
- databricks_agentbricks/cli/endpoint_transport.py +148 -0
- databricks_agentbricks/cli/help.py +467 -0
- databricks_agentbricks/cli/init.py +496 -0
- databricks_agentbricks/cli/mcp.py +75 -0
- databricks_agentbricks/cli/memory.py +520 -0
- databricks_agentbricks/cli/sandbox.py +823 -0
- databricks_agentbricks/cli/sessions.py +588 -0
- databricks_agentbricks/cli/tools.py +456 -0
- databricks_agentbricks/cli/tracing.py +724 -0
- databricks_agentbricks/databricks_cli.py +38 -0
- databricks_agentbricks/errors.py +114 -0
- databricks_agentbricks/lakebase_runtime_store.py +107 -0
- databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
- databricks_agentbricks/project_config.py +157 -0
- databricks_agentbricks/project_types.py +55 -0
- databricks_agentbricks/py.typed +0 -0
- databricks_agentbricks/render.py +349 -0
- databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
- databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
- databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
- databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
- databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
- databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
- databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
- databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
- databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
- databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/agent-openai/.env.example +37 -0
- databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
- databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
- databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
- databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
- databricks_agentbricks/templates/agent-openai/README.md +143 -0
- databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
- databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
- databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
- databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
- databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
- databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
- databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
- databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
- databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
- databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
- databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
- databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
- databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
- databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
- databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
- databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
- databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
- databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
- databricks_agentbricks/templates/sandbox_mcp.py +39 -0
- databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
- databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
- databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
- databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
- databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
- databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
- databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
- databricks_agentbricks/theme.py +72 -0
- databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
- databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
- databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
- databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
- databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
- databricks_agentkit/__init__.py +67 -0
- databricks_agentkit/_api_client.py +673 -0
- databricks_agentkit/_pagination.py +25 -0
- databricks_agentkit/client.py +31 -0
- databricks_agentkit/langgraph/__init__.py +98 -0
- databricks_agentkit/langgraph/genie.py +50 -0
- databricks_agentkit/langgraph/mcp.py +216 -0
- databricks_agentkit/langgraph/memory.py +67 -0
- databricks_agentkit/langgraph/session_store.py +485 -0
- databricks_agentkit/memory_store.py +371 -0
- databricks_agentkit/models.py +330 -0
- databricks_agentkit/openai/__init__.py +93 -0
- databricks_agentkit/openai/genie.py +50 -0
- databricks_agentkit/openai/mcp.py +189 -0
- databricks_agentkit/openai/memory.py +67 -0
- databricks_agentkit/openai/sessions.py +157 -0
- databricks_agentkit/py.typed +0 -0
- databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
- databricks_agentkit/runtime/README.md +184 -0
- databricks_agentkit/runtime/__init__.py +69 -0
- databricks_agentkit/runtime/app.py +316 -0
- databricks_agentkit/runtime/auth.py +149 -0
- databricks_agentkit/runtime/durability/__init__.py +1 -0
- databricks_agentkit/runtime/durability/execution.py +133 -0
- databricks_agentkit/runtime/durability/heartbeat.py +64 -0
- databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
- databricks_agentkit/runtime/durability/recovery.py +70 -0
- databricks_agentkit/runtime/durability/store.py +33 -0
- databricks_agentkit/runtime/execution.py +160 -0
- databricks_agentkit/runtime/genie.py +224 -0
- databricks_agentkit/runtime/mcp_auth.py +114 -0
- databricks_agentkit/runtime/model_services.py +90 -0
- databricks_agentkit/runtime/runtime.py +273 -0
- databricks_agentkit/runtime/session_store_client.py +120 -0
- databricks_agentkit/runtime/store.py +287 -0
- databricks_agentkit/runtime/tool_manifest.py +223 -0
- databricks_agentkit/runtime/tracing.py +71 -0
- databricks_agentkit/runtime/types.py +118 -0
- databricks_agentkit/runtime/workspace.py +27 -0
- databricks_agentkit/session_store.py +434 -0
- databricks_agentkit/timefmt.py +85 -0
|
@@ -0,0 +1,1464 @@
|
|
|
1
|
+
const elements = {
|
|
2
|
+
approvalPanel: document.querySelector("#approval-panel"),
|
|
3
|
+
approvalSummary: document.querySelector("#approval-summary"),
|
|
4
|
+
approveAction: document.querySelector("#approve-action"),
|
|
5
|
+
backgroundMode: document.querySelector("#background-mode-value"),
|
|
6
|
+
backgroundStatus: document.querySelector("#background-status"),
|
|
7
|
+
chatLog: document.querySelector("#chat-log"),
|
|
8
|
+
clearEvents: document.querySelector("#clear-events"),
|
|
9
|
+
composer: document.querySelector("#composer"),
|
|
10
|
+
copySession: document.querySelector("#copy-session"),
|
|
11
|
+
emptyState: document.querySelector("#empty-state"),
|
|
12
|
+
eventLog: document.querySelector("#event-log"),
|
|
13
|
+
memoryMode: document.querySelector("#memory-mode-value"),
|
|
14
|
+
memoryStatus: document.querySelector("#memory-status"),
|
|
15
|
+
tracingMode: document.querySelector("#tracing-mode-value"),
|
|
16
|
+
tracingStatus: document.querySelector("#tracing-status"),
|
|
17
|
+
modelSelect: document.querySelector("#model-select"),
|
|
18
|
+
newSession: document.querySelector("#new-session"),
|
|
19
|
+
promptInput: document.querySelector("#prompt-input"),
|
|
20
|
+
refreshConfig: document.querySelector("#refresh-config"),
|
|
21
|
+
refreshSession: document.querySelector("#refresh-session"),
|
|
22
|
+
rejectAction: document.querySelector("#reject-action"),
|
|
23
|
+
rejectSession: document.querySelector("#reject-session"),
|
|
24
|
+
resumeSession: document.querySelector("#resume-session"),
|
|
25
|
+
runStatus: document.querySelector("#run-status"),
|
|
26
|
+
sendButton: document.querySelector("#send-button"),
|
|
27
|
+
sessionId: document.querySelector("#session-id"),
|
|
28
|
+
sessionItems: document.querySelector("#session-items"),
|
|
29
|
+
sessionList: document.querySelector("#session-list"),
|
|
30
|
+
sessionMode: document.querySelector("#session-mode-value"),
|
|
31
|
+
sessionStatus: document.querySelector("#session-status"),
|
|
32
|
+
sessionStoreLabel: document.querySelector("#session-store-label"),
|
|
33
|
+
streamingMode: document.querySelector("#streaming-mode-value"),
|
|
34
|
+
streamingStatus: document.querySelector("#streaming-status"),
|
|
35
|
+
viewerValue: document.querySelector("#viewer-value"),
|
|
36
|
+
viewerAvatar: document.querySelector("#viewer-avatar"),
|
|
37
|
+
brandSub: document.querySelector("#brand-sub"),
|
|
38
|
+
sessionSearch: document.querySelector("#session-search-input"),
|
|
39
|
+
eventFilters: document.querySelector("#event-filters"),
|
|
40
|
+
eventDetail: document.querySelector("#event-detail"),
|
|
41
|
+
eventDetailJson: document.querySelector("#event-detail-json"),
|
|
42
|
+
copyEventDetail: document.querySelector("#copy-event-detail"),
|
|
43
|
+
eventViewLogs: document.querySelector("#event-view-logs"),
|
|
44
|
+
eventViewList: document.querySelector("#event-view-list"),
|
|
45
|
+
leftCard: document.querySelector(".left-card"),
|
|
46
|
+
capabilitiesBlock: document.querySelector("#capabilities-block"),
|
|
47
|
+
memoryPane: document.querySelector("#memory-pane"),
|
|
48
|
+
memoryBack: document.querySelector("#memory-back"),
|
|
49
|
+
memoryRefresh: document.querySelector("#memory-refresh"),
|
|
50
|
+
memoryStoreName: document.querySelector("#memory-store-name"),
|
|
51
|
+
memoryStoreResource: document.querySelector("#memory-store-resource"),
|
|
52
|
+
memoryActor: document.querySelector("#memory-actor-input"),
|
|
53
|
+
memorySearch: document.querySelector("#memory-search-input"),
|
|
54
|
+
memoryCount: document.querySelector("#memory-count"),
|
|
55
|
+
memoryEntries: document.querySelector("#memory-entries"),
|
|
56
|
+
memoryModal: document.querySelector("#memory-modal"),
|
|
57
|
+
memoryModalTitle: document.querySelector("#memory-modal-title"),
|
|
58
|
+
memoryModalBody: document.querySelector("#memory-modal-body"),
|
|
59
|
+
memoryModalFoot: document.querySelector("#memory-modal-foot"),
|
|
60
|
+
memoryModalClose: document.querySelector("#memory-modal-close"),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// The framework label shown in the header subtitle, next to the host.
|
|
64
|
+
const AGENT_FRAMEWORK = "LangGraph";
|
|
65
|
+
|
|
66
|
+
const SESSION_STORAGE_KEY = "databricks-agentbricks-session-id";
|
|
67
|
+
|
|
68
|
+
function newSessionId() {
|
|
69
|
+
return crypto.randomUUID();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const state = {
|
|
73
|
+
busy: false,
|
|
74
|
+
config: null,
|
|
75
|
+
draft: null,
|
|
76
|
+
draftText: "",
|
|
77
|
+
events: [],
|
|
78
|
+
eventSeq: 0,
|
|
79
|
+
eventView: "logs",
|
|
80
|
+
eventFilter: "all",
|
|
81
|
+
selectedEventId: null,
|
|
82
|
+
sessionFilter: "",
|
|
83
|
+
sessions: [],
|
|
84
|
+
instanceId: null,
|
|
85
|
+
lastAssistantText: "",
|
|
86
|
+
managedSessionId: "",
|
|
87
|
+
model: "",
|
|
88
|
+
mode: "streaming",
|
|
89
|
+
pendingInterrupt: null,
|
|
90
|
+
sessionId: localStorage.getItem(SESSION_STORAGE_KEY) || newSessionId(),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function ensureSessionId() {
|
|
94
|
+
if (!state.sessionId) throw new Error("The routing session is not initialized yet.");
|
|
95
|
+
return state.sessionId;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function setSessionId(value) {
|
|
99
|
+
const nextSessionId = String(value || "");
|
|
100
|
+
if (!nextSessionId) return;
|
|
101
|
+
if (state.sessionId !== nextSessionId) state.managedSessionId = "";
|
|
102
|
+
state.sessionId = nextSessionId;
|
|
103
|
+
localStorage.setItem(SESSION_STORAGE_KEY, state.sessionId);
|
|
104
|
+
elements.sessionId.textContent = state.sessionId;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function demoUrl(path) {
|
|
108
|
+
const url = new URL(path, window.location.origin);
|
|
109
|
+
url.searchParams.set("session_id", ensureSessionId());
|
|
110
|
+
return `${url.pathname}${url.search}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function setStatus(label, type = "ready") {
|
|
114
|
+
elements.runStatus.textContent = label;
|
|
115
|
+
elements.runStatus.className = `run-status ${type === "ready" ? "" : type}`.trim();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The send button is enabled only when there is text to send and no run is in flight.
|
|
119
|
+
function updateSendState() {
|
|
120
|
+
elements.sendButton.disabled = state.busy || !elements.promptInput.value.trim();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function setBusy(busy, label = "Working") {
|
|
124
|
+
state.busy = busy;
|
|
125
|
+
elements.chatLog.setAttribute("aria-busy", String(busy));
|
|
126
|
+
updateSendState();
|
|
127
|
+
elements.promptInput.disabled = busy;
|
|
128
|
+
elements.approveAction.disabled = busy;
|
|
129
|
+
elements.rejectAction.disabled = busy;
|
|
130
|
+
elements.newSession.disabled = busy;
|
|
131
|
+
elements.modelSelect.disabled = busy || elements.modelSelect.options.length <= 1;
|
|
132
|
+
elements.refreshSession.disabled = busy || !state.config?.session.history;
|
|
133
|
+
elements.resumeSession.disabled = busy || !state.config?.session.durable;
|
|
134
|
+
elements.rejectSession.disabled = busy || !state.config?.session.durable;
|
|
135
|
+
document.querySelectorAll(".session-open-button").forEach((button) => {
|
|
136
|
+
button.disabled = busy;
|
|
137
|
+
});
|
|
138
|
+
if (busy) setStatus(label, "busy");
|
|
139
|
+
else if (!elements.runStatus.classList.contains("error")) setStatus("Ready");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Human-readable name for each orb color, shown in the expanded capability detail.
|
|
143
|
+
const CAPABILITY_SUMMARY = { enabled: "Available", degraded: "Limited", disabled: "Unavailable" };
|
|
144
|
+
|
|
145
|
+
// Populate a capability's status orb and its click-to-expand detail panel.
|
|
146
|
+
// `link` (optional) renders an external link in the detail, e.g. to an MLflow experiment.
|
|
147
|
+
function setCapability(element, state, detail, command, link) {
|
|
148
|
+
const key = state === true ? "enabled" : state === "degraded" ? "degraded" : "disabled";
|
|
149
|
+
element.classList.toggle("enabled", key === "enabled");
|
|
150
|
+
element.classList.toggle("degraded", key === "degraded");
|
|
151
|
+
element.classList.toggle("disabled", key === "disabled");
|
|
152
|
+
element.setAttribute("role", "img");
|
|
153
|
+
element.setAttribute("aria-label", `Status: ${CAPABILITY_SUMMARY[key]}`);
|
|
154
|
+
const item = element.closest(".capability-item");
|
|
155
|
+
const panel = item?.querySelector(".capability-detail");
|
|
156
|
+
if (!panel) return;
|
|
157
|
+
|
|
158
|
+
panel.className = `capability-detail ${key}`;
|
|
159
|
+
panel.hidden = !item.classList.contains("expanded");
|
|
160
|
+
panel.replaceChildren();
|
|
161
|
+
|
|
162
|
+
const status = document.createElement("span");
|
|
163
|
+
status.className = "cap-status";
|
|
164
|
+
status.textContent = CAPABILITY_SUMMARY[key];
|
|
165
|
+
panel.append(status);
|
|
166
|
+
|
|
167
|
+
if (detail) {
|
|
168
|
+
const description = document.createElement("span");
|
|
169
|
+
description.textContent = detail;
|
|
170
|
+
panel.append(description);
|
|
171
|
+
}
|
|
172
|
+
if (command) {
|
|
173
|
+
const label = document.createElement("span");
|
|
174
|
+
label.className = "cap-command-label";
|
|
175
|
+
label.textContent = "Connect by running:";
|
|
176
|
+
const row = document.createElement("div");
|
|
177
|
+
row.className = "cap-command";
|
|
178
|
+
const code = document.createElement("code");
|
|
179
|
+
code.textContent = command;
|
|
180
|
+
const copy = document.createElement("button");
|
|
181
|
+
copy.type = "button";
|
|
182
|
+
copy.setAttribute("aria-label", `Copy command: ${command}`);
|
|
183
|
+
copy.innerHTML =
|
|
184
|
+
'<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
185
|
+
copy.addEventListener("click", (event) => {
|
|
186
|
+
event.stopPropagation();
|
|
187
|
+
void navigator.clipboard.writeText(command).catch(() => {});
|
|
188
|
+
});
|
|
189
|
+
row.append(code, copy);
|
|
190
|
+
panel.append(label, row);
|
|
191
|
+
}
|
|
192
|
+
if (link) {
|
|
193
|
+
const anchor = document.createElement("a");
|
|
194
|
+
anchor.className = "cap-link";
|
|
195
|
+
anchor.href = link.href;
|
|
196
|
+
anchor.target = "_blank";
|
|
197
|
+
anchor.rel = "noopener noreferrer";
|
|
198
|
+
anchor.append(
|
|
199
|
+
iconSvg(
|
|
200
|
+
"icon",
|
|
201
|
+
'<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
const label = document.createElement("span");
|
|
205
|
+
label.textContent = link.label;
|
|
206
|
+
anchor.append(label);
|
|
207
|
+
panel.append(anchor);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function formatJson(value) {
|
|
212
|
+
try {
|
|
213
|
+
return JSON.stringify(value, null, 2);
|
|
214
|
+
} catch {
|
|
215
|
+
return String(value);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function escapeHtml(text) {
|
|
220
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Basic JSON syntax highlighting: wraps tokens in <span> classes for coloring.
|
|
224
|
+
// The payload is HTML-escaped first, so runtime data can't inject markup.
|
|
225
|
+
function highlightJson(value) {
|
|
226
|
+
const json = escapeHtml(formatJson(value));
|
|
227
|
+
return json.replace(
|
|
228
|
+
/("(?:\\.|[^"\\])*"(\s*:)?|\b(?:true|false)\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
|
|
229
|
+
(match) => {
|
|
230
|
+
let cls = "json-num";
|
|
231
|
+
if (match.startsWith(""") || match.startsWith('"')) {
|
|
232
|
+
cls = /:\s*$/.test(match) ? "json-key" : "json-str";
|
|
233
|
+
} else if (match === "true" || match === "false") {
|
|
234
|
+
cls = "json-bool";
|
|
235
|
+
} else if (match === "null") {
|
|
236
|
+
cls = "json-null";
|
|
237
|
+
}
|
|
238
|
+
return `<span class="${cls}">${match}</span>`;
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Bucket an event type into one of the filter categories used by the list view.
|
|
244
|
+
function eventCategory(type) {
|
|
245
|
+
const value = String(type || "").toLowerCase();
|
|
246
|
+
if (value === "error" || value.endsWith(".error")) return "error";
|
|
247
|
+
if (value.startsWith("session")) return "sessions";
|
|
248
|
+
if (value.startsWith("memory")) return "memory";
|
|
249
|
+
if (
|
|
250
|
+
value.startsWith("background") ||
|
|
251
|
+
value.startsWith("model") ||
|
|
252
|
+
value.startsWith("run") ||
|
|
253
|
+
["response", "delta", "message", "interrupt", "runtime.config"].includes(value)
|
|
254
|
+
) {
|
|
255
|
+
return "invocation";
|
|
256
|
+
}
|
|
257
|
+
return "system";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// A short one-line summary for the list view, derived from the payload.
|
|
261
|
+
function eventSummary(type, payload) {
|
|
262
|
+
if (payload && typeof payload === "object") {
|
|
263
|
+
for (const key of ["message", "summary", "detail", "status", "error"]) {
|
|
264
|
+
if (typeof payload[key] === "string" && payload[key]) return payload[key];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return String(type || "event");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function eventRunId(payload) {
|
|
271
|
+
if (payload && typeof payload === "object") {
|
|
272
|
+
return payload.run_id || payload.runId || payload.id || payload.invocation_id || "";
|
|
273
|
+
}
|
|
274
|
+
return "";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function addEvent(type, payload) {
|
|
278
|
+
state.events.unshift({
|
|
279
|
+
id: `evt-${(state.eventSeq += 1)}`,
|
|
280
|
+
type,
|
|
281
|
+
category: eventCategory(type),
|
|
282
|
+
summary: eventSummary(type, payload),
|
|
283
|
+
runId: eventRunId(payload),
|
|
284
|
+
payload,
|
|
285
|
+
at: new Date(),
|
|
286
|
+
});
|
|
287
|
+
state.events = state.events.slice(0, 80);
|
|
288
|
+
renderEvents();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function eventEmpty() {
|
|
292
|
+
const wrap = document.createElement("div");
|
|
293
|
+
wrap.className = "event-empty";
|
|
294
|
+
wrap.innerHTML =
|
|
295
|
+
'<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>' +
|
|
296
|
+
"<strong>No events in this session</strong>" +
|
|
297
|
+
"<span>Start an agent run to collect runtime evidence.</span>";
|
|
298
|
+
return wrap;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function renderEventDetail() {
|
|
302
|
+
const event = state.events.find((item) => item.id === state.selectedEventId);
|
|
303
|
+
const show = state.eventView === "list" && Boolean(event);
|
|
304
|
+
elements.eventDetail.hidden = !show;
|
|
305
|
+
if (show) elements.eventDetailJson.innerHTML = highlightJson(event.payload);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function renderEvents() {
|
|
309
|
+
const listView = state.eventView === "list";
|
|
310
|
+
elements.eventFilters.hidden = !listView;
|
|
311
|
+
elements.eventLog.classList.toggle("list-view", listView);
|
|
312
|
+
elements.eventLog.classList.toggle("logs-view", !listView);
|
|
313
|
+
elements.eventLog.replaceChildren();
|
|
314
|
+
|
|
315
|
+
const events = listView
|
|
316
|
+
? state.events.filter((event) => state.eventFilter === "all" || event.category === state.eventFilter)
|
|
317
|
+
: state.events;
|
|
318
|
+
|
|
319
|
+
if (!events.length) {
|
|
320
|
+
elements.eventLog.append(eventEmpty());
|
|
321
|
+
renderEventDetail();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!listView) {
|
|
326
|
+
for (const event of events) {
|
|
327
|
+
const entry = document.createElement("div");
|
|
328
|
+
entry.className = "event-entry";
|
|
329
|
+
const header = document.createElement("div");
|
|
330
|
+
header.className = "event-entry-header";
|
|
331
|
+
const name = document.createElement("span");
|
|
332
|
+
name.className = `event-type cat-${event.category}`;
|
|
333
|
+
name.textContent = event.type;
|
|
334
|
+
const time = document.createElement("span");
|
|
335
|
+
time.className = "event-time";
|
|
336
|
+
time.textContent = event.at.toLocaleTimeString();
|
|
337
|
+
const body = document.createElement("pre");
|
|
338
|
+
body.innerHTML = highlightJson(event.payload);
|
|
339
|
+
header.append(name, time);
|
|
340
|
+
entry.append(header, body);
|
|
341
|
+
elements.eventLog.append(entry);
|
|
342
|
+
}
|
|
343
|
+
renderEventDetail();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const event of events) {
|
|
348
|
+
const row = document.createElement("button");
|
|
349
|
+
row.type = "button";
|
|
350
|
+
row.className = `event-row${event.id === state.selectedEventId ? " selected" : ""}`;
|
|
351
|
+
const top = document.createElement("div");
|
|
352
|
+
top.className = "event-row-top";
|
|
353
|
+
const type = document.createElement("span");
|
|
354
|
+
type.className = `event-row-type cat-${event.category}`;
|
|
355
|
+
type.textContent = event.type;
|
|
356
|
+
const time = document.createElement("span");
|
|
357
|
+
time.className = "event-row-time";
|
|
358
|
+
time.textContent = event.at.toLocaleTimeString();
|
|
359
|
+
top.append(type, time);
|
|
360
|
+
const summary = document.createElement("div");
|
|
361
|
+
summary.className = "event-row-summary";
|
|
362
|
+
summary.textContent = event.summary;
|
|
363
|
+
row.append(top, summary);
|
|
364
|
+
if (event.runId) {
|
|
365
|
+
const run = document.createElement("div");
|
|
366
|
+
run.className = "event-row-run";
|
|
367
|
+
run.textContent = event.runId;
|
|
368
|
+
row.append(run);
|
|
369
|
+
}
|
|
370
|
+
row.addEventListener("click", () => {
|
|
371
|
+
state.selectedEventId = event.id;
|
|
372
|
+
renderEvents();
|
|
373
|
+
});
|
|
374
|
+
elements.eventLog.append(row);
|
|
375
|
+
}
|
|
376
|
+
renderEventDetail();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function setEventView(view) {
|
|
380
|
+
state.eventView = view;
|
|
381
|
+
elements.eventViewLogs.classList.toggle("active", view === "logs");
|
|
382
|
+
elements.eventViewLogs.setAttribute("aria-pressed", String(view === "logs"));
|
|
383
|
+
elements.eventViewList.classList.toggle("active", view === "list");
|
|
384
|
+
elements.eventViewList.setAttribute("aria-pressed", String(view === "list"));
|
|
385
|
+
renderEvents();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function normalizeRole(message) {
|
|
389
|
+
const value = String(message?.role || message?.type || "assistant").toLowerCase();
|
|
390
|
+
if (["human", "user"].includes(value)) return "user";
|
|
391
|
+
if (["tool", "function"].includes(value)) return "tool";
|
|
392
|
+
if (["system", "developer"].includes(value)) return "system";
|
|
393
|
+
return "assistant";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function extractText(content) {
|
|
397
|
+
if (content == null) return "";
|
|
398
|
+
if (typeof content === "string") return content;
|
|
399
|
+
if (Array.isArray(content)) {
|
|
400
|
+
return content
|
|
401
|
+
.map((part) => {
|
|
402
|
+
if (typeof part === "string") return part;
|
|
403
|
+
if (typeof part?.text === "string") return part.text;
|
|
404
|
+
if (typeof part?.content === "string") return part.content;
|
|
405
|
+
return "";
|
|
406
|
+
})
|
|
407
|
+
.filter(Boolean)
|
|
408
|
+
.join("\n");
|
|
409
|
+
}
|
|
410
|
+
return typeof content === "object" ? formatJson(content) : String(content);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function hideEmptyState() {
|
|
414
|
+
elements.emptyState.hidden = true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function initials(name) {
|
|
418
|
+
const parts = String(name || "")
|
|
419
|
+
.replace(/@.*/, "")
|
|
420
|
+
.split(/[.\s_-]+/)
|
|
421
|
+
.filter(Boolean);
|
|
422
|
+
if (!parts.length) return "•";
|
|
423
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
424
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function setViewer(name) {
|
|
428
|
+
const label = name || "Local developer";
|
|
429
|
+
elements.viewerValue.textContent = label;
|
|
430
|
+
if (elements.viewerAvatar) elements.viewerAvatar.textContent = initials(label);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function formatClock(date = new Date()) {
|
|
434
|
+
return date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function messageName(role, label) {
|
|
438
|
+
if (label) return label;
|
|
439
|
+
if (role === "tool") return "Tool";
|
|
440
|
+
if (role === "system") return "System";
|
|
441
|
+
if (role === "error") return "Error";
|
|
442
|
+
return "Agent";
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Hover actions under an agent message. "View trace" is intentionally omitted
|
|
446
|
+
// until the demo backend exposes a trace URL (per design feedback).
|
|
447
|
+
function buildMessageActions(textEl) {
|
|
448
|
+
const actions = document.createElement("div");
|
|
449
|
+
actions.className = "message-actions";
|
|
450
|
+
const copy = document.createElement("button");
|
|
451
|
+
copy.type = "button";
|
|
452
|
+
copy.className = "message-action";
|
|
453
|
+
copy.innerHTML =
|
|
454
|
+
'<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><span>Copy</span>';
|
|
455
|
+
copy.addEventListener("click", async () => {
|
|
456
|
+
try {
|
|
457
|
+
await navigator.clipboard.writeText(textEl.textContent || "");
|
|
458
|
+
} catch {
|
|
459
|
+
/* clipboard may be unavailable */
|
|
460
|
+
}
|
|
461
|
+
const span = copy.querySelector("span");
|
|
462
|
+
if (span) {
|
|
463
|
+
span.textContent = "Copied";
|
|
464
|
+
setTimeout(() => {
|
|
465
|
+
span.textContent = "Copy";
|
|
466
|
+
}, 1200);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
actions.append(copy);
|
|
470
|
+
return actions;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function appendMessage(role, content, label, { time } = {}) {
|
|
474
|
+
hideEmptyState();
|
|
475
|
+
const wrapper = document.createElement("article");
|
|
476
|
+
wrapper.className = `message ${role}`;
|
|
477
|
+
const text = document.createElement("div");
|
|
478
|
+
text.className = "message-content";
|
|
479
|
+
text.textContent = content;
|
|
480
|
+
|
|
481
|
+
if (role === "user") {
|
|
482
|
+
wrapper.append(text);
|
|
483
|
+
} else {
|
|
484
|
+
const head = document.createElement("div");
|
|
485
|
+
head.className = "message-head";
|
|
486
|
+
const name = document.createElement("span");
|
|
487
|
+
name.className = "message-name";
|
|
488
|
+
name.textContent = messageName(role, label);
|
|
489
|
+
const stamp = document.createElement("span");
|
|
490
|
+
stamp.className = "message-time";
|
|
491
|
+
stamp.textContent = time || formatClock();
|
|
492
|
+
head.append(name, stamp);
|
|
493
|
+
wrapper.append(head, text);
|
|
494
|
+
if (role === "assistant") wrapper.append(buildMessageActions(text));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
elements.chatLog.append(wrapper);
|
|
498
|
+
elements.chatLog.scrollTop = elements.chatLog.scrollHeight;
|
|
499
|
+
return { wrapper, text };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function appendError(error) {
|
|
503
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
504
|
+
appendMessage("error", message, "Request failed");
|
|
505
|
+
setStatus("Error", "error");
|
|
506
|
+
addEvent("error", { message });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function startDraft() {
|
|
510
|
+
if (state.draft) return state.draft;
|
|
511
|
+
const draft = appendMessage("assistant", "", "Agent");
|
|
512
|
+
draft.wrapper.classList.add("streaming");
|
|
513
|
+
state.draft = draft;
|
|
514
|
+
state.draftText = "";
|
|
515
|
+
return draft;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function appendDelta(content) {
|
|
519
|
+
const text = extractText(content);
|
|
520
|
+
if (!text) return;
|
|
521
|
+
const draft = startDraft();
|
|
522
|
+
state.draftText += text;
|
|
523
|
+
state.lastAssistantText = state.draftText;
|
|
524
|
+
draft.text.textContent = state.draftText;
|
|
525
|
+
elements.chatLog.scrollTop = elements.chatLog.scrollHeight;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function finishDraft(finalText = "") {
|
|
529
|
+
if (!state.draft) return false;
|
|
530
|
+
if (finalText) {
|
|
531
|
+
state.draftText = finalText;
|
|
532
|
+
state.lastAssistantText = finalText;
|
|
533
|
+
state.draft.text.textContent = finalText;
|
|
534
|
+
}
|
|
535
|
+
state.draft.wrapper.classList.remove("streaming");
|
|
536
|
+
state.draft = null;
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function toolSummary(message) {
|
|
541
|
+
if (message?.name) return `${message.name}\n${extractText(message.content)}`.trim();
|
|
542
|
+
if (Array.isArray(message?.tool_calls) && message.tool_calls.length) {
|
|
543
|
+
return message.tool_calls.map((call) => `${call.name || "tool"}(${formatJson(call.args || {})})`).join("\n");
|
|
544
|
+
}
|
|
545
|
+
return extractText(message?.content) || formatJson(message);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function handleAgentMessage(message) {
|
|
549
|
+
const role = normalizeRole(message);
|
|
550
|
+
if (role === "user") return;
|
|
551
|
+
if (role === "assistant") {
|
|
552
|
+
const text = extractText(message?.content);
|
|
553
|
+
if (finishDraft(text)) return;
|
|
554
|
+
if (text) {
|
|
555
|
+
state.lastAssistantText = text;
|
|
556
|
+
appendMessage("assistant", text, "Agent");
|
|
557
|
+
}
|
|
558
|
+
if (message?.tool_calls?.length) appendMessage("tool", toolSummary(message), "Tool request");
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
appendMessage(role, toolSummary(message), role === "tool" ? "Tool result" : "System");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function interruptSummary(interrupt) {
|
|
565
|
+
const requests = interrupt?.value?.action_requests || [];
|
|
566
|
+
if (!requests.length) return "The agent paused and needs a decision.";
|
|
567
|
+
return requests
|
|
568
|
+
.map((request) => `${request.name || "tool"} ${formatJson(request.args || {})}`)
|
|
569
|
+
.join(" · ");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function handleInterrupt(interrupt) {
|
|
573
|
+
finishDraft();
|
|
574
|
+
state.pendingInterrupt = interrupt;
|
|
575
|
+
elements.approvalSummary.textContent = interruptSummary(interrupt);
|
|
576
|
+
elements.approvalPanel.hidden = false;
|
|
577
|
+
appendMessage("system", interruptSummary(interrupt), "Approval required");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function handleEvent(event) {
|
|
581
|
+
addEvent(event?.type || "event", event);
|
|
582
|
+
if (event?.type === "delta") appendDelta(event.content);
|
|
583
|
+
if (event?.type === "message") handleAgentMessage(event.message);
|
|
584
|
+
if (event?.type === "interrupt") handleInterrupt(event);
|
|
585
|
+
if (event?.error) throw new Error(event.error);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function handleOutput(output) {
|
|
589
|
+
for (const item of output || []) {
|
|
590
|
+
if (item?.type === "interrupt") handleInterrupt(item);
|
|
591
|
+
else handleAgentMessage(item);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function invocationHeaders() {
|
|
596
|
+
return {
|
|
597
|
+
"Content-Type": "application/json",
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function invocationPayload(payload, transport = {}) {
|
|
602
|
+
const sessionId = ensureSessionId();
|
|
603
|
+
const input = {
|
|
604
|
+
session_id: sessionId,
|
|
605
|
+
actor: state.config?.session.actor || sessionId,
|
|
606
|
+
...payload,
|
|
607
|
+
};
|
|
608
|
+
if (state.model) input.model = state.model;
|
|
609
|
+
return { id: newSessionId(), input, ...transport };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function agentResult(result) {
|
|
613
|
+
return result?.output && !Array.isArray(result.output) ? result.output : result;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function jsonResponse(response) {
|
|
617
|
+
const body = await response.json().catch(() => ({}));
|
|
618
|
+
if (!response.ok) throw new Error(body.detail || body.error || `Request failed with ${response.status}`);
|
|
619
|
+
return body;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function stateMessage(container, message, kind = "empty") {
|
|
623
|
+
container.replaceChildren();
|
|
624
|
+
const item = document.createElement("div");
|
|
625
|
+
item.className = `state-${kind}`;
|
|
626
|
+
item.textContent = message;
|
|
627
|
+
container.append(item);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function renderStateItems(container, items, emptyMessage, renderItem) {
|
|
631
|
+
container.replaceChildren();
|
|
632
|
+
if (!items.length) {
|
|
633
|
+
stateMessage(container, emptyMessage);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
for (const value of items) {
|
|
637
|
+
const item = document.createElement("article");
|
|
638
|
+
item.className = "state-item";
|
|
639
|
+
const rendered = renderItem(value);
|
|
640
|
+
const title = document.createElement("strong");
|
|
641
|
+
title.textContent = rendered.title;
|
|
642
|
+
const content = document.createElement("p");
|
|
643
|
+
content.textContent = rendered.content || "No content returned.";
|
|
644
|
+
const meta = document.createElement("small");
|
|
645
|
+
meta.textContent = rendered.meta || "";
|
|
646
|
+
item.append(title, content, meta);
|
|
647
|
+
container.append(item);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function memoryEntries(payload) {
|
|
652
|
+
return payload?.managed_memory_entries || [];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function sessionItems(payload) {
|
|
656
|
+
return payload?.session_items || [];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function sessions(payload) {
|
|
660
|
+
return payload?.sessions || [];
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ---- Memory pane -----------------------------------------------------------
|
|
664
|
+
|
|
665
|
+
const memoryState = { query: "", searchTimer: null };
|
|
666
|
+
|
|
667
|
+
function prettyDate(value) {
|
|
668
|
+
if (!value) return "";
|
|
669
|
+
const d = new Date(value);
|
|
670
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
671
|
+
return d.toLocaleDateString([], { year: "numeric", month: "short", day: "numeric" });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function prettyDateTime(value) {
|
|
675
|
+
if (!value) return "";
|
|
676
|
+
const d = new Date(value);
|
|
677
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
678
|
+
return d.toLocaleString([], {
|
|
679
|
+
year: "numeric",
|
|
680
|
+
month: "short",
|
|
681
|
+
day: "numeric",
|
|
682
|
+
hour: "numeric",
|
|
683
|
+
minute: "2-digit",
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function iconSvg(className, inner) {
|
|
688
|
+
const span = document.createElement("span");
|
|
689
|
+
span.innerHTML = `<svg class="${className}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`;
|
|
690
|
+
return span.firstChild;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Store name = the trailing segment of "memory-stores/<name>"; resource = the full path.
|
|
694
|
+
function memoryStoreName() {
|
|
695
|
+
const store = state.config?.memory.store || "";
|
|
696
|
+
const parts = store.split("/").filter(Boolean);
|
|
697
|
+
return parts.length ? parts[parts.length - 1] : "Memory store";
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function currentMemoryActor() {
|
|
701
|
+
return (elements.memoryActor.value || "").trim() || state.config?.memory.actor || "";
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function memoryMessage(text, kind = "empty") {
|
|
705
|
+
elements.memoryEntries.replaceChildren();
|
|
706
|
+
const div = document.createElement("div");
|
|
707
|
+
div.className = `memory-${kind}`;
|
|
708
|
+
div.textContent = text;
|
|
709
|
+
elements.memoryEntries.append(div);
|
|
710
|
+
elements.memoryCount.textContent = "";
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function renderMemoryCards(entries, query) {
|
|
714
|
+
elements.memoryEntries.replaceChildren();
|
|
715
|
+
elements.memoryCount.textContent = entries.length
|
|
716
|
+
? query
|
|
717
|
+
? `${entries.length} matching`
|
|
718
|
+
: `${entries.length} of ${entries.length} entries`
|
|
719
|
+
: "";
|
|
720
|
+
if (!entries.length) {
|
|
721
|
+
memoryMessage(query ? "No entries match your search." : "No memory entries for this actor yet.");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
for (const entry of entries) {
|
|
725
|
+
const card = document.createElement("button");
|
|
726
|
+
card.type = "button";
|
|
727
|
+
card.className = "memory-entry";
|
|
728
|
+
const top = document.createElement("div");
|
|
729
|
+
top.className = "memory-entry-top";
|
|
730
|
+
const path = document.createElement("span");
|
|
731
|
+
path.className = "memory-entry-path";
|
|
732
|
+
path.textContent = entry.path || entry.name || "Memory entry";
|
|
733
|
+
top.append(path, iconSvg("memory-entry-expand icon", '<path d="M7 7h10v10"/><path d="M7 17 17 7"/>'));
|
|
734
|
+
const preview = document.createElement("p");
|
|
735
|
+
preview.className = "memory-entry-preview";
|
|
736
|
+
preview.textContent = extractText(entry.content) || entry.description || "";
|
|
737
|
+
const foot = document.createElement("div");
|
|
738
|
+
foot.className = "memory-entry-foot";
|
|
739
|
+
const actor = document.createElement("span");
|
|
740
|
+
actor.className = "memory-entry-actor";
|
|
741
|
+
actor.textContent = entry.actor_id || "";
|
|
742
|
+
const time = document.createElement("span");
|
|
743
|
+
time.className = "memory-entry-time";
|
|
744
|
+
time.textContent = prettyDate(entry.update_time || entry.create_time);
|
|
745
|
+
foot.append(actor, time);
|
|
746
|
+
card.append(top, preview, foot);
|
|
747
|
+
card.addEventListener("click", () => openMemoryModal(entry));
|
|
748
|
+
elements.memoryEntries.append(card);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function loadMemory() {
|
|
753
|
+
if (!state.config?.memory.enabled) {
|
|
754
|
+
memoryMessage("Connect a Memory Store to browse entries: ab memory bind <store-name>");
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const query = memoryState.query.trim();
|
|
758
|
+
const actor = currentMemoryActor();
|
|
759
|
+
memoryMessage(query ? "Searching…" : "Loading memory…", "loading");
|
|
760
|
+
try {
|
|
761
|
+
let entries;
|
|
762
|
+
if (query) {
|
|
763
|
+
const response = await fetch(demoUrl("/api/demo/memory/search"), {
|
|
764
|
+
method: "POST",
|
|
765
|
+
headers: { "Content-Type": "application/json" },
|
|
766
|
+
body: JSON.stringify({ query, limit: 50, actor: actor || undefined }),
|
|
767
|
+
});
|
|
768
|
+
entries = memoryEntries(await jsonResponse(response));
|
|
769
|
+
} else {
|
|
770
|
+
const url = demoUrl("/api/demo/memory/entries") + (actor ? `&actor=${encodeURIComponent(actor)}` : "");
|
|
771
|
+
const response = await fetch(url, { cache: "no-store" });
|
|
772
|
+
entries = memoryEntries(await jsonResponse(response));
|
|
773
|
+
}
|
|
774
|
+
renderMemoryCards(entries, query);
|
|
775
|
+
addEvent(query ? "memory.entries.search" : "memory.entries.list", {
|
|
776
|
+
count: entries.length,
|
|
777
|
+
actor,
|
|
778
|
+
query: query || undefined,
|
|
779
|
+
});
|
|
780
|
+
} catch (error) {
|
|
781
|
+
memoryMessage(error instanceof Error ? error.message : String(error), "error");
|
|
782
|
+
addEvent("memory.error", { message: String(error) });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function openMemoryPane() {
|
|
787
|
+
elements.leftCard.classList.add("memory-open");
|
|
788
|
+
elements.capabilitiesBlock.hidden = true;
|
|
789
|
+
elements.memoryPane.hidden = false;
|
|
790
|
+
elements.memoryStoreName.textContent = memoryStoreName();
|
|
791
|
+
elements.memoryStoreResource.textContent = state.config?.memory.store || "";
|
|
792
|
+
if (!elements.memoryActor.value) elements.memoryActor.value = state.config?.memory.actor || "";
|
|
793
|
+
void loadMemory();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function closeMemoryPane() {
|
|
797
|
+
elements.leftCard.classList.remove("memory-open");
|
|
798
|
+
elements.memoryPane.hidden = true;
|
|
799
|
+
elements.capabilitiesBlock.hidden = false;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function openMemoryModal(entry) {
|
|
803
|
+
elements.memoryModalTitle.textContent = entry.path || entry.name || "Memory entry";
|
|
804
|
+
elements.memoryModalBody.textContent = extractText(entry.content) || entry.description || "(no content)";
|
|
805
|
+
elements.memoryModalFoot.textContent = [entry.actor_id, prettyDateTime(entry.update_time || entry.create_time)]
|
|
806
|
+
.filter(Boolean)
|
|
807
|
+
.join(" · ");
|
|
808
|
+
elements.memoryModal.hidden = false;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function closeMemoryModal() {
|
|
812
|
+
elements.memoryModal.hidden = true;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function renderSessionItems(items) {
|
|
816
|
+
renderStateItems(elements.sessionItems, items, "No transcript items yet.", (item) => {
|
|
817
|
+
const data = item?.data || {};
|
|
818
|
+
return {
|
|
819
|
+
title: String(data.role || data.type || "item"),
|
|
820
|
+
content: extractText(data.content ?? data),
|
|
821
|
+
meta: [item.item_id, item.create_time, data.transport].filter(Boolean).join(" · "),
|
|
822
|
+
};
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function relativeTime(value) {
|
|
827
|
+
if (!value) return "";
|
|
828
|
+
const then = new Date(value).getTime();
|
|
829
|
+
if (Number.isNaN(then)) return "";
|
|
830
|
+
const minutes = Math.round((Date.now() - then) / 60000);
|
|
831
|
+
if (minutes < 1) return "Just now";
|
|
832
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
833
|
+
const hours = Math.round(minutes / 60);
|
|
834
|
+
if (hours < 24) return `${hours} hr ago`;
|
|
835
|
+
const days = Math.round(hours / 24);
|
|
836
|
+
return days === 1 ? "Yesterday" : `${days} days ago`;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function renderSessions(items) {
|
|
840
|
+
state.sessions = items || [];
|
|
841
|
+
renderSessionList();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function renderSessionList() {
|
|
845
|
+
elements.sessionList.replaceChildren();
|
|
846
|
+
if (!state.sessions.length) {
|
|
847
|
+
stateMessage(elements.sessionList, "No sessions yet.");
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const query = state.sessionFilter.trim().toLowerCase();
|
|
851
|
+
const items = state.sessions.filter(
|
|
852
|
+
(session) =>
|
|
853
|
+
!query ||
|
|
854
|
+
String(session.session_id || "").toLowerCase().includes(query) ||
|
|
855
|
+
String(session.actor_id || "").toLowerCase().includes(query),
|
|
856
|
+
);
|
|
857
|
+
if (!items.length) {
|
|
858
|
+
stateMessage(elements.sessionList, "No sessions match your search.");
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
for (const session of items) {
|
|
862
|
+
const current = session.session_id === state.sessionId;
|
|
863
|
+
const canOpen = !current && state.config?.session.managed;
|
|
864
|
+
const row = document.createElement(canOpen ? "button" : "div");
|
|
865
|
+
row.className = `session-item${current ? " current" : ""}${canOpen ? " session-open-button" : ""}`;
|
|
866
|
+
if (canOpen) {
|
|
867
|
+
row.type = "button";
|
|
868
|
+
row.disabled = state.busy;
|
|
869
|
+
row.addEventListener("click", () => openSession(session.session_id));
|
|
870
|
+
}
|
|
871
|
+
const top = document.createElement("div");
|
|
872
|
+
top.className = "session-item-top";
|
|
873
|
+
const title = document.createElement("span");
|
|
874
|
+
title.className = "session-item-title";
|
|
875
|
+
title.textContent = current ? "Current session" : "Session";
|
|
876
|
+
const time = document.createElement("span");
|
|
877
|
+
time.className = "session-item-time";
|
|
878
|
+
time.textContent = current ? "Active" : relativeTime(session.last_activity_time || session.create_time);
|
|
879
|
+
top.append(title, time);
|
|
880
|
+
const id = document.createElement("span");
|
|
881
|
+
id.className = "session-item-id";
|
|
882
|
+
id.textContent = session.session_id || "unknown";
|
|
883
|
+
row.append(top, id);
|
|
884
|
+
elements.sessionList.append(row);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function renderSessionTranscript(items) {
|
|
889
|
+
state.draft = null;
|
|
890
|
+
state.draftText = "";
|
|
891
|
+
state.lastAssistantText = "";
|
|
892
|
+
elements.chatLog.replaceChildren(elements.emptyState);
|
|
893
|
+
elements.emptyState.hidden = false;
|
|
894
|
+
|
|
895
|
+
for (const item of items) {
|
|
896
|
+
const data = item?.data || {};
|
|
897
|
+
const storedRole = String(data.role || data.type || "").toLowerCase();
|
|
898
|
+
const content = extractText(data.content ?? data);
|
|
899
|
+
if (!content) continue;
|
|
900
|
+
if (storedRole === "human_decision") {
|
|
901
|
+
appendMessage("system", content, "Human decision");
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const role = normalizeRole(data);
|
|
905
|
+
if (role === "assistant") state.lastAssistantText = content;
|
|
906
|
+
appendMessage(
|
|
907
|
+
role,
|
|
908
|
+
role === "tool" ? toolSummary(data) : content,
|
|
909
|
+
role === "user" ? "You" : role === "assistant" ? "Agent" : role === "tool" ? "Tool result" : "System",
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async function ensureManagedSession() {
|
|
915
|
+
if (!state.config?.session.managed) return null;
|
|
916
|
+
const sessionId = ensureSessionId();
|
|
917
|
+
if (state.managedSessionId === sessionId) return sessionId;
|
|
918
|
+
stateMessage(elements.sessionItems, "Connecting managed session…", "loading");
|
|
919
|
+
const response = await fetch(demoUrl("/api/demo/sessions"), {
|
|
920
|
+
method: "POST",
|
|
921
|
+
headers: { "Content-Type": "application/json" },
|
|
922
|
+
body: "{}",
|
|
923
|
+
});
|
|
924
|
+
const result = await jsonResponse(response);
|
|
925
|
+
state.managedSessionId = sessionId;
|
|
926
|
+
addEvent("session.managed", result);
|
|
927
|
+
return sessionId;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async function refreshSession({ hydrateChat = false } = {}) {
|
|
931
|
+
if (!state.config?.session.history) {
|
|
932
|
+
stateMessage(elements.sessionItems, "Session history is not available.");
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
const sessionId = state.config.session.managed ? await ensureManagedSession() : ensureSessionId();
|
|
937
|
+
const response = await fetch(demoUrl("/api/demo/session/items"), { cache: "no-store" });
|
|
938
|
+
const result = await jsonResponse(response);
|
|
939
|
+
const items = sessionItems(result);
|
|
940
|
+
renderSessionItems(items);
|
|
941
|
+
if (hydrateChat) {
|
|
942
|
+
renderSessionTranscript(items);
|
|
943
|
+
for (const interrupt of result.interrupts || []) handleInterrupt(interrupt);
|
|
944
|
+
}
|
|
945
|
+
addEvent("session.items.list", result);
|
|
946
|
+
} catch (error) {
|
|
947
|
+
stateMessage(elements.sessionItems, error instanceof Error ? error.message : String(error), "error");
|
|
948
|
+
addEvent("session.error", { message: String(error) });
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
async function refreshSessions() {
|
|
953
|
+
stateMessage(elements.sessionList, "Loading sessions…", "loading");
|
|
954
|
+
try {
|
|
955
|
+
const response = await fetch(demoUrl("/api/demo/sessions"), { cache: "no-store" });
|
|
956
|
+
const result = await jsonResponse(response);
|
|
957
|
+
renderSessions(sessions(result));
|
|
958
|
+
addEvent("sessions.list", result);
|
|
959
|
+
} catch (error) {
|
|
960
|
+
stateMessage(elements.sessionList, error instanceof Error ? error.message : String(error), "error");
|
|
961
|
+
addEvent("session.error", { message: String(error) });
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
async function refreshSessionView({ hydrateChat = false } = {}) {
|
|
966
|
+
await refreshSession({ hydrateChat });
|
|
967
|
+
await refreshSessions();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function recordSessionItems(items) {
|
|
971
|
+
if (!state.config?.session.managed || !items.length) return;
|
|
972
|
+
try {
|
|
973
|
+
const sessionId = await ensureManagedSession();
|
|
974
|
+
const response = await fetch(demoUrl("/api/demo/session/items"), {
|
|
975
|
+
method: "POST",
|
|
976
|
+
headers: { "Content-Type": "application/json" },
|
|
977
|
+
body: JSON.stringify({ items }),
|
|
978
|
+
});
|
|
979
|
+
const result = await jsonResponse(response);
|
|
980
|
+
addEvent("session.items.append", result);
|
|
981
|
+
await refreshSessionView();
|
|
982
|
+
} catch (error) {
|
|
983
|
+
stateMessage(elements.sessionItems, error instanceof Error ? error.message : String(error), "error");
|
|
984
|
+
addEvent("session.error", { message: String(error) });
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function invokeSync(payload) {
|
|
989
|
+
const response = await fetch("/api/invocations", {
|
|
990
|
+
method: "POST",
|
|
991
|
+
credentials: "same-origin",
|
|
992
|
+
headers: invocationHeaders(),
|
|
993
|
+
body: JSON.stringify(invocationPayload(payload)),
|
|
994
|
+
});
|
|
995
|
+
const result = await jsonResponse(response);
|
|
996
|
+
const output = agentResult(result);
|
|
997
|
+
addEvent("response", result);
|
|
998
|
+
if (output.session_id) setSessionId(output.session_id);
|
|
999
|
+
handleOutput(output.output);
|
|
1000
|
+
return output;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function parseSseFrame(frame) {
|
|
1004
|
+
const data = frame
|
|
1005
|
+
.split("\n")
|
|
1006
|
+
.filter((line) => line.startsWith("data:"))
|
|
1007
|
+
.map((line) => line.slice(5).trimStart())
|
|
1008
|
+
.join("\n");
|
|
1009
|
+
if (!data || data === "[DONE]") return null;
|
|
1010
|
+
return JSON.parse(data);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async function invokeStreaming(payload) {
|
|
1014
|
+
const response = await fetch("/api/invocations", {
|
|
1015
|
+
method: "POST",
|
|
1016
|
+
credentials: "same-origin",
|
|
1017
|
+
headers: invocationHeaders(),
|
|
1018
|
+
body: JSON.stringify(invocationPayload(payload, { stream: true })),
|
|
1019
|
+
});
|
|
1020
|
+
if (!response.ok || !response.body) await jsonResponse(response);
|
|
1021
|
+
const reader = response.body.getReader();
|
|
1022
|
+
const decoder = new TextDecoder();
|
|
1023
|
+
let buffer = "";
|
|
1024
|
+
while (true) {
|
|
1025
|
+
const { value, done } = await reader.read();
|
|
1026
|
+
buffer += decoder.decode(value || new Uint8Array(), { stream: !done });
|
|
1027
|
+
const frames = buffer.split("\n\n");
|
|
1028
|
+
buffer = frames.pop() || "";
|
|
1029
|
+
for (const frame of frames) {
|
|
1030
|
+
const event = parseSseFrame(frame);
|
|
1031
|
+
if (event) handleEvent(event);
|
|
1032
|
+
}
|
|
1033
|
+
if (done) break;
|
|
1034
|
+
}
|
|
1035
|
+
if (buffer.trim()) {
|
|
1036
|
+
const event = parseSseFrame(buffer);
|
|
1037
|
+
if (event) handleEvent(event);
|
|
1038
|
+
}
|
|
1039
|
+
finishDraft();
|
|
1040
|
+
return { status: state.pendingInterrupt ? "interrupted" : "completed" };
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async function pollBackground(invocationId) {
|
|
1044
|
+
const deadline = Date.now() + 180000;
|
|
1045
|
+
while (Date.now() < deadline) {
|
|
1046
|
+
await new Promise((resolve) => setTimeout(resolve, 850));
|
|
1047
|
+
const response = await fetch(`/api/invocations/${encodeURIComponent(invocationId)}`, {
|
|
1048
|
+
cache: "no-store",
|
|
1049
|
+
credentials: "same-origin",
|
|
1050
|
+
});
|
|
1051
|
+
const result = await jsonResponse(response);
|
|
1052
|
+
addEvent("background.poll", result);
|
|
1053
|
+
if (result.status === "completed") {
|
|
1054
|
+
const output = agentResult(result);
|
|
1055
|
+
if (output.session_id) setSessionId(output.session_id);
|
|
1056
|
+
handleOutput(output.output);
|
|
1057
|
+
return output;
|
|
1058
|
+
}
|
|
1059
|
+
if (result.status === "failed") throw new Error(result.error || "Background invocation failed");
|
|
1060
|
+
setStatus(`Background · ${result.status}`, "busy");
|
|
1061
|
+
}
|
|
1062
|
+
throw new Error("Background invocation did not finish within three minutes.");
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function invokeBackground(payload) {
|
|
1066
|
+
const response = await fetch("/api/invocations", {
|
|
1067
|
+
method: "POST",
|
|
1068
|
+
credentials: "same-origin",
|
|
1069
|
+
headers: invocationHeaders(),
|
|
1070
|
+
body: JSON.stringify(invocationPayload(payload, { background: true })),
|
|
1071
|
+
});
|
|
1072
|
+
const started = await jsonResponse(response);
|
|
1073
|
+
addEvent("background.started", started);
|
|
1074
|
+
setStatus(`Background · ${started.id}`, "busy");
|
|
1075
|
+
return pollBackground(started.id);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async function dispatch(payload, mode = state.mode) {
|
|
1079
|
+
state.lastAssistantText = "";
|
|
1080
|
+
state.pendingInterrupt = null;
|
|
1081
|
+
elements.approvalPanel.hidden = true;
|
|
1082
|
+
if (mode === "streaming") return invokeStreaming(payload);
|
|
1083
|
+
if (mode === "background") return invokeBackground(payload);
|
|
1084
|
+
return invokeSync(payload);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function sendText(text, mode = state.mode) {
|
|
1088
|
+
const content = text.trim();
|
|
1089
|
+
if (!content || state.busy) return "";
|
|
1090
|
+
appendMessage("user", content, "You");
|
|
1091
|
+
setBusy(true, mode === "background" ? "Starting background run" : mode === "streaming" ? "Streaming" : "Running");
|
|
1092
|
+
try {
|
|
1093
|
+
await dispatch({ messages: [{ role: "user", content }] }, mode);
|
|
1094
|
+
const items = [{ role: "user", content, transport: mode, instance_id: state.instanceId }];
|
|
1095
|
+
if (state.lastAssistantText) {
|
|
1096
|
+
items.push({
|
|
1097
|
+
role: "assistant",
|
|
1098
|
+
content: state.lastAssistantText,
|
|
1099
|
+
transport: mode,
|
|
1100
|
+
instance_id: state.instanceId,
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
await recordSessionItems(items);
|
|
1104
|
+
return state.lastAssistantText;
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
finishDraft();
|
|
1107
|
+
appendError(error);
|
|
1108
|
+
throw error;
|
|
1109
|
+
} finally {
|
|
1110
|
+
setBusy(false);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async function resume(decision) {
|
|
1115
|
+
if (state.busy) return;
|
|
1116
|
+
if (!state.pendingInterrupt && !state.config?.session.durable) {
|
|
1117
|
+
appendError(new Error("No paused run is loaded for the current routing session."));
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const payload =
|
|
1121
|
+
decision === "approve"
|
|
1122
|
+
? { resume: { decisions: [{ type: "approve" }] } }
|
|
1123
|
+
: { resume: { decisions: [{ type: "reject", message: "Rejected from the Agent Bricks demo UI." }] } };
|
|
1124
|
+
appendMessage("system", decision === "approve" ? "Approved pending tool call." : "Rejected pending tool call.", "Human decision");
|
|
1125
|
+
setBusy(true, "Resuming");
|
|
1126
|
+
try {
|
|
1127
|
+
await dispatch(payload, "streaming");
|
|
1128
|
+
const items = [
|
|
1129
|
+
{ role: "human_decision", content: decision, instance_id: state.instanceId },
|
|
1130
|
+
];
|
|
1131
|
+
if (state.lastAssistantText) {
|
|
1132
|
+
items.push({
|
|
1133
|
+
role: "assistant",
|
|
1134
|
+
content: state.lastAssistantText,
|
|
1135
|
+
transport: "streaming",
|
|
1136
|
+
instance_id: state.instanceId,
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
await recordSessionItems(items);
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
appendError(error);
|
|
1142
|
+
} finally {
|
|
1143
|
+
setBusy(false);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function clearChat({ recordEvent = true } = {}) {
|
|
1148
|
+
state.pendingInterrupt = null;
|
|
1149
|
+
state.draft = null;
|
|
1150
|
+
state.draftText = "";
|
|
1151
|
+
state.lastAssistantText = "";
|
|
1152
|
+
elements.approvalPanel.hidden = true;
|
|
1153
|
+
elements.chatLog.replaceChildren(elements.emptyState);
|
|
1154
|
+
elements.emptyState.hidden = false;
|
|
1155
|
+
elements.promptInput.focus();
|
|
1156
|
+
if (recordEvent) addEvent("chat.cleared", { session_id: state.sessionId });
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function resetSessionState() {
|
|
1160
|
+
clearChat({ recordEvent: false });
|
|
1161
|
+
stateMessage(elements.sessionList, "Loading sessions…", "loading");
|
|
1162
|
+
stateMessage(elements.sessionItems, "Loading transcript…", "loading");
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async function createNewSession() {
|
|
1166
|
+
if (state.busy) return;
|
|
1167
|
+
setBusy(true, "Creating session");
|
|
1168
|
+
try {
|
|
1169
|
+
const previousSessionId = ensureSessionId();
|
|
1170
|
+
setSessionId(newSessionId());
|
|
1171
|
+
resetSessionState();
|
|
1172
|
+
addEvent("session.new", { session_id: state.sessionId, previous_session_id: previousSessionId });
|
|
1173
|
+
if (state.config?.session.managed) await ensureManagedSession();
|
|
1174
|
+
await refreshSessionView({ hydrateChat: true });
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
appendError(error);
|
|
1177
|
+
} finally {
|
|
1178
|
+
setBusy(false);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
async function openSession(sessionId) {
|
|
1183
|
+
if (state.busy || !sessionId || sessionId === state.sessionId) return;
|
|
1184
|
+
setBusy(true, "Opening session");
|
|
1185
|
+
try {
|
|
1186
|
+
const response = await fetch(demoUrl(`/api/demo/sessions/${encodeURIComponent(sessionId)}/open`), {
|
|
1187
|
+
method: "POST",
|
|
1188
|
+
credentials: "same-origin",
|
|
1189
|
+
});
|
|
1190
|
+
const result = await jsonResponse(response);
|
|
1191
|
+
setSessionId(result.session_id);
|
|
1192
|
+
resetSessionState();
|
|
1193
|
+
addEvent("session.open", result);
|
|
1194
|
+
await refreshSessionView({ hydrateChat: true });
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
appendError(error);
|
|
1197
|
+
} finally {
|
|
1198
|
+
setBusy(false);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function renderModels(models) {
|
|
1203
|
+
const available = models?.available?.length ? models.available : [models?.default].filter(Boolean);
|
|
1204
|
+
state.model = available.includes(state.model) ? state.model : models?.default || available[0] || "";
|
|
1205
|
+
elements.modelSelect.replaceChildren();
|
|
1206
|
+
for (const name of available) {
|
|
1207
|
+
const option = document.createElement("option");
|
|
1208
|
+
option.value = name;
|
|
1209
|
+
option.textContent = name;
|
|
1210
|
+
option.selected = name === state.model;
|
|
1211
|
+
elements.modelSelect.append(option);
|
|
1212
|
+
}
|
|
1213
|
+
// A single choice is informational, not a decision to make.
|
|
1214
|
+
elements.modelSelect.disabled = state.busy || available.length <= 1;
|
|
1215
|
+
sizeModelSelect();
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// Native selects size to their widest option, which leaves a gap between a short
|
|
1219
|
+
// selected name and the chevron. Size the control to the selected option instead.
|
|
1220
|
+
function sizeModelSelect() {
|
|
1221
|
+
const select = elements.modelSelect;
|
|
1222
|
+
const option = select.selectedOptions && select.selectedOptions[0];
|
|
1223
|
+
if (!option) return;
|
|
1224
|
+
const probe = document.createElement("span");
|
|
1225
|
+
const style = getComputedStyle(select);
|
|
1226
|
+
probe.style.cssText = "position:absolute;visibility:hidden;white-space:pre;";
|
|
1227
|
+
probe.style.fontFamily = style.fontFamily;
|
|
1228
|
+
probe.style.fontSize = style.fontSize;
|
|
1229
|
+
probe.style.fontWeight = style.fontWeight;
|
|
1230
|
+
probe.style.letterSpacing = style.letterSpacing;
|
|
1231
|
+
probe.textContent = option.textContent;
|
|
1232
|
+
document.body.append(probe);
|
|
1233
|
+
const width = probe.getBoundingClientRect().width;
|
|
1234
|
+
probe.remove();
|
|
1235
|
+
// selected text + left padding (8) + room for the chevron (~26)
|
|
1236
|
+
select.style.width = `${Math.ceil(width) + 34}px`;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
async function loadModels() {
|
|
1240
|
+
const response = await fetch(demoUrl("/api/demo/models"), { cache: "no-store" });
|
|
1241
|
+
renderModels(await jsonResponse(response));
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
async function loadConfig() {
|
|
1245
|
+
try {
|
|
1246
|
+
const response = await fetch(demoUrl("/api/ui/config"), { cache: "no-store" });
|
|
1247
|
+
const config = await jsonResponse(response);
|
|
1248
|
+
state.config = config;
|
|
1249
|
+
state.instanceId = config.instance_id;
|
|
1250
|
+
setSessionId(config.session_id);
|
|
1251
|
+
renderModels(config.models);
|
|
1252
|
+
void loadModels().catch((error) => addEvent("models.error", { message: String(error) }));
|
|
1253
|
+
setViewer(config.viewer);
|
|
1254
|
+
elements.streamingMode.textContent = config.streaming.mode;
|
|
1255
|
+
elements.backgroundMode.textContent = config.background.mode;
|
|
1256
|
+
elements.sessionMode.textContent = config.session.mode;
|
|
1257
|
+
elements.memoryMode.textContent = config.memory.enabled ? `Managed · actor ${config.memory.actor}` : "Not connected";
|
|
1258
|
+
setCapability(
|
|
1259
|
+
elements.streamingStatus,
|
|
1260
|
+
config.streaming.enabled,
|
|
1261
|
+
config.streaming.enabled
|
|
1262
|
+
? `Streaming responses over ${config.streaming.transport} use ${config.streaming.persistent ? "the Runtime Store." : "an in-process Runtime Store."}`
|
|
1263
|
+
: "Streaming is disabled for this deployment.",
|
|
1264
|
+
);
|
|
1265
|
+
setCapability(
|
|
1266
|
+
elements.backgroundStatus,
|
|
1267
|
+
config.background.enabled,
|
|
1268
|
+
config.background.enabled
|
|
1269
|
+
? `Background invocations use ${config.background.persistent ? "the Runtime Store." : "an in-process Runtime Store."}`
|
|
1270
|
+
: "Background invocations are disabled.",
|
|
1271
|
+
);
|
|
1272
|
+
setCapability(
|
|
1273
|
+
elements.sessionStatus,
|
|
1274
|
+
config.session.managed ? true : config.session.history ? "degraded" : false,
|
|
1275
|
+
config.session.managed
|
|
1276
|
+
? `Connected to Session Store "${config.session.store}" for actor ${config.session.actor}. State is durable and shareable.`
|
|
1277
|
+
: "Session state is kept in process and resets when the app restarts.",
|
|
1278
|
+
config.session.managed ? null : "ab sessions bind <store-name>",
|
|
1279
|
+
);
|
|
1280
|
+
setCapability(
|
|
1281
|
+
elements.memoryStatus,
|
|
1282
|
+
config.memory.enabled,
|
|
1283
|
+
config.memory.enabled
|
|
1284
|
+
? `Connected to ${config.memory.store} for actor ${config.memory.actor}.`
|
|
1285
|
+
: "No long-term memory is connected.",
|
|
1286
|
+
config.memory.enabled ? null : "ab memory bind <store-name>",
|
|
1287
|
+
);
|
|
1288
|
+
elements.tracingMode.textContent = config.tracing?.enabled ? "Connected" : "Not configured";
|
|
1289
|
+
setCapability(
|
|
1290
|
+
elements.tracingStatus,
|
|
1291
|
+
config.tracing?.enabled ? true : "degraded",
|
|
1292
|
+
config.tracing?.enabled
|
|
1293
|
+
? "Every run is traced to an MLflow experiment."
|
|
1294
|
+
: "Tracing is off. Set an MLflow destination and experiment to record traces.",
|
|
1295
|
+
config.tracing?.enabled ? null : "ab tracing bind --experiment-name <path>",
|
|
1296
|
+
config.tracing?.enabled && config.tracing?.url
|
|
1297
|
+
? { href: config.tracing.url, label: "View traces in MLflow" }
|
|
1298
|
+
: null,
|
|
1299
|
+
);
|
|
1300
|
+
elements.newSession.disabled = state.busy;
|
|
1301
|
+
elements.refreshSession.disabled = state.busy || !config.session.history;
|
|
1302
|
+
elements.resumeSession.disabled = state.busy || !config.session.durable;
|
|
1303
|
+
elements.rejectSession.disabled = state.busy || !config.session.durable;
|
|
1304
|
+
elements.sessionStoreLabel.textContent = config.session.managed
|
|
1305
|
+
? `${config.session.store} · actor ${config.session.actor} · the browser session ID keys transcript and checkpoint state.`
|
|
1306
|
+
: config.session.history
|
|
1307
|
+
? "Messages load from the in-process LangGraph checkpoint for the current browser session."
|
|
1308
|
+
: "Session history is unavailable.";
|
|
1309
|
+
addEvent("runtime.config", config);
|
|
1310
|
+
void refreshSessionView({ hydrateChat: true });
|
|
1311
|
+
// The memory pane loads lazily when opened; refresh it if it is already open.
|
|
1312
|
+
if (!elements.memoryPane.hidden) void loadMemory();
|
|
1313
|
+
return config;
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
throw error;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
elements.composer.addEventListener("submit", async (event) => {
|
|
1319
|
+
event.preventDefault();
|
|
1320
|
+
const text = elements.promptInput.value;
|
|
1321
|
+
if (!text.trim()) return;
|
|
1322
|
+
elements.promptInput.value = "";
|
|
1323
|
+
elements.promptInput.style.height = "auto";
|
|
1324
|
+
updateSendState();
|
|
1325
|
+
try {
|
|
1326
|
+
await sendText(text);
|
|
1327
|
+
} catch {
|
|
1328
|
+
elements.promptInput.value = text;
|
|
1329
|
+
updateSendState();
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
elements.promptInput.addEventListener("input", () => {
|
|
1334
|
+
elements.promptInput.style.height = "auto";
|
|
1335
|
+
elements.promptInput.style.height = `${Math.min(elements.promptInput.scrollHeight, 180)}px`;
|
|
1336
|
+
updateSendState();
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
elements.promptInput.addEventListener("keydown", (event) => {
|
|
1340
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
1341
|
+
event.preventDefault();
|
|
1342
|
+
elements.composer.requestSubmit();
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
document.querySelectorAll(".mode-button").forEach((button) => {
|
|
1347
|
+
button.addEventListener("click", () => {
|
|
1348
|
+
state.mode = button.dataset.mode;
|
|
1349
|
+
document.querySelectorAll(".mode-button").forEach((item) => item.classList.toggle("active", item === button));
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
elements.modelSelect.addEventListener("change", () => {
|
|
1354
|
+
state.model = elements.modelSelect.value;
|
|
1355
|
+
sizeModelSelect();
|
|
1356
|
+
addEvent("model.selected", { model: state.model });
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
document.querySelectorAll("[data-prompt]").forEach((button) => {
|
|
1360
|
+
button.addEventListener("click", () => {
|
|
1361
|
+
elements.promptInput.value = button.dataset.prompt;
|
|
1362
|
+
elements.promptInput.dispatchEvent(new Event("input"));
|
|
1363
|
+
elements.promptInput.focus();
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
elements.copySession.addEventListener("click", async () => {
|
|
1368
|
+
await navigator.clipboard.writeText(ensureSessionId());
|
|
1369
|
+
const label = elements.copySession.querySelector("span");
|
|
1370
|
+
label.textContent = "Copied";
|
|
1371
|
+
setTimeout(() => { label.textContent = "Copy"; }, 1200);
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
elements.refreshConfig.addEventListener("click", () => loadConfig().catch(appendError));
|
|
1375
|
+
elements.newSession.addEventListener("click", createNewSession);
|
|
1376
|
+
elements.refreshSession.addEventListener("click", () => refreshSessionView({ hydrateChat: true }));
|
|
1377
|
+
elements.clearEvents.addEventListener("click", () => {
|
|
1378
|
+
state.events = [];
|
|
1379
|
+
state.selectedEventId = null;
|
|
1380
|
+
renderEvents();
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
// Capabilities expand inline on click; Memory instead opens its own pane.
|
|
1384
|
+
document.querySelectorAll(".capability").forEach((button) => {
|
|
1385
|
+
button.addEventListener("click", () => {
|
|
1386
|
+
const item = button.closest(".capability-item");
|
|
1387
|
+
if (item?.dataset.capability === "memory") {
|
|
1388
|
+
openMemoryPane();
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const panel = item?.querySelector(".capability-detail");
|
|
1392
|
+
const expanded = item.classList.toggle("expanded");
|
|
1393
|
+
button.setAttribute("aria-expanded", String(expanded));
|
|
1394
|
+
if (panel) panel.hidden = !expanded;
|
|
1395
|
+
});
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
// Memory pane controls.
|
|
1399
|
+
elements.memoryBack.addEventListener("click", closeMemoryPane);
|
|
1400
|
+
elements.memoryRefresh.addEventListener("click", () => loadMemory());
|
|
1401
|
+
elements.memoryActor.addEventListener("input", () => {
|
|
1402
|
+
window.clearTimeout(memoryState.searchTimer);
|
|
1403
|
+
memoryState.searchTimer = window.setTimeout(() => void loadMemory(), 300);
|
|
1404
|
+
});
|
|
1405
|
+
elements.memorySearch.addEventListener("input", () => {
|
|
1406
|
+
memoryState.query = elements.memorySearch.value;
|
|
1407
|
+
window.clearTimeout(memoryState.searchTimer);
|
|
1408
|
+
memoryState.searchTimer = window.setTimeout(() => void loadMemory(), 300);
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
// Memory modal: close on ×, backdrop click, or Escape.
|
|
1412
|
+
elements.memoryModalClose.addEventListener("click", closeMemoryModal);
|
|
1413
|
+
elements.memoryModal.addEventListener("click", (event) => {
|
|
1414
|
+
if (event.target === elements.memoryModal) closeMemoryModal();
|
|
1415
|
+
});
|
|
1416
|
+
document.addEventListener("keydown", (event) => {
|
|
1417
|
+
if (event.key === "Escape" && !elements.memoryModal.hidden) closeMemoryModal();
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
// Sessions search.
|
|
1421
|
+
elements.sessionSearch.addEventListener("input", () => {
|
|
1422
|
+
state.sessionFilter = elements.sessionSearch.value;
|
|
1423
|
+
renderSessionList();
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
// Events view toggles.
|
|
1427
|
+
elements.eventViewLogs.addEventListener("click", () => setEventView("logs"));
|
|
1428
|
+
elements.eventViewList.addEventListener("click", () => setEventView("list"));
|
|
1429
|
+
|
|
1430
|
+
// Events filter pills (list view only).
|
|
1431
|
+
elements.eventFilters.querySelectorAll(".event-filter").forEach((pill) => {
|
|
1432
|
+
pill.addEventListener("click", () => {
|
|
1433
|
+
state.eventFilter = pill.dataset.filter;
|
|
1434
|
+
elements.eventFilters
|
|
1435
|
+
.querySelectorAll(".event-filter")
|
|
1436
|
+
.forEach((item) => item.classList.toggle("active", item === pill));
|
|
1437
|
+
renderEvents();
|
|
1438
|
+
});
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
// Copy the selected event payload from the list-view detail pane.
|
|
1442
|
+
elements.copyEventDetail.addEventListener("click", async () => {
|
|
1443
|
+
try {
|
|
1444
|
+
await navigator.clipboard.writeText(elements.eventDetailJson.textContent || "");
|
|
1445
|
+
} catch {
|
|
1446
|
+
/* clipboard may be unavailable */
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
// Header subtitle: framework · host (host is only known client-side).
|
|
1451
|
+
if (elements.brandSub) elements.brandSub.textContent = `${AGENT_FRAMEWORK} · ${window.location.host}`;
|
|
1452
|
+
elements.approveAction.addEventListener("click", () => resume("approve"));
|
|
1453
|
+
elements.rejectAction.addEventListener("click", () => resume("reject"));
|
|
1454
|
+
elements.resumeSession.addEventListener("click", () => resume("approve"));
|
|
1455
|
+
elements.rejectSession.addEventListener("click", () => resume("reject"));
|
|
1456
|
+
|
|
1457
|
+
loadConfig().catch((error) => {
|
|
1458
|
+
appendError(error);
|
|
1459
|
+
elements.streamingMode.textContent = "Unavailable";
|
|
1460
|
+
elements.backgroundMode.textContent = "Unavailable";
|
|
1461
|
+
elements.sessionMode.textContent = "Unavailable";
|
|
1462
|
+
elements.memoryMode.textContent = "Unavailable";
|
|
1463
|
+
elements.tracingMode.textContent = "Unavailable";
|
|
1464
|
+
});
|