bugfixes
This commit is contained in:
@@ -177,10 +177,9 @@ export class AuthService {
|
|||||||
async getSession(token: string) {
|
async getSession(token: string) {
|
||||||
try {
|
try {
|
||||||
const session = await this.config.auth.api.getSession({
|
const session = await this.config.auth.api.getSession({
|
||||||
headers: {
|
headers: new Headers({
|
||||||
// Better Auth expects the session token in the cookie header
|
'Authorization': `Bearer ${token}`,
|
||||||
cookie: `better-auth.session_token=${token}`,
|
}),
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
|
|||||||
@@ -264,7 +264,12 @@ export class AgentHarness {
|
|||||||
this.mcpClient,
|
this.mcpClient,
|
||||||
this.availableMCPTools,
|
this.availableMCPTools,
|
||||||
this.workspaceManager,
|
this.workspaceManager,
|
||||||
(img) => this.researchImageCapture.push(img)
|
(img) => this.researchImageCapture.push(img),
|
||||||
|
(storeName, newState) => {
|
||||||
|
this.workspaceManager?.setState(storeName, newState).catch((err) =>
|
||||||
|
this.config.logger.error({ err, storeName }, 'Failed to sync workspace after research mutation')
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Inject web_explore tool if the web-explore subagent is ready
|
// Inject web_explore tool if the web-explore subagent is ready
|
||||||
@@ -475,7 +480,11 @@ export class AgentHarness {
|
|||||||
this.availableMCPTools,
|
this.availableMCPTools,
|
||||||
this.workspaceManager,
|
this.workspaceManager,
|
||||||
undefined,
|
undefined,
|
||||||
undefined
|
(storeName, newState) => {
|
||||||
|
this.workspaceManager?.setState(storeName, newState).catch((err) =>
|
||||||
|
this.config.logger.error({ err, storeName }, 'Failed to sync workspace after strategy mutation')
|
||||||
|
);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const strategySubagentPath = join(__dirname, 'subagents', 'strategy');
|
const strategySubagentPath = join(__dirname, 'subagents', 'strategy');
|
||||||
|
|||||||
@@ -45,21 +45,26 @@ export function createMCPToolWrapper(
|
|||||||
|
|
||||||
// Fire workspace mutation callback when workspace_patch or workspace_write succeeds.
|
// Fire workspace mutation callback when workspace_patch or workspace_write succeeds.
|
||||||
// The sandbox returns {"success": true, "data": <newState>} as a text content item.
|
// The sandbox returns {"success": true, "data": <newState>} as a text content item.
|
||||||
if (
|
if (onWorkspaceMutation) {
|
||||||
onWorkspaceMutation &&
|
|
||||||
(toolInfo.name === 'workspace_patch' || toolInfo.name === 'workspace_write')
|
|
||||||
) {
|
|
||||||
const content = (result as any)?.content;
|
const content = (result as any)?.content;
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
for (const item of content) {
|
for (const item of content) {
|
||||||
if (item.type === 'text' && item.text) {
|
if (item.type === 'text' && item.text) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(item.text);
|
const parsed = JSON.parse(item.text);
|
||||||
if (parsed?.success && parsed?.data !== undefined) {
|
// workspace_patch / workspace_write: {"success": true, "data": <state>}
|
||||||
|
if (
|
||||||
|
(toolInfo.name === 'workspace_patch' || toolInfo.name === 'workspace_write') &&
|
||||||
|
parsed?.success && parsed?.data !== undefined
|
||||||
|
) {
|
||||||
onWorkspaceMutation((input as any).store_name as string, parsed.data);
|
onWorkspaceMutation((input as any).store_name as string, parsed.data);
|
||||||
}
|
}
|
||||||
|
// python_write / python_edit / python_delete / python_revert:
|
||||||
|
// {"_workspace_sync": {"store": <name>, "data": <state>}}
|
||||||
|
if (parsed?._workspace_sync?.store && parsed._workspace_sync.data !== undefined) {
|
||||||
|
onWorkspaceMutation(parsed._workspace_sync.store, parsed._workspace_sync.data);
|
||||||
|
}
|
||||||
} catch { /* ignore parse errors */ }
|
} catch { /* ignore parse errors */ }
|
||||||
break; // only need first text item
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ All internal timestamps use nanoseconds since epoch (UTC).
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from dateutil import parser as dateparser
|
from dateutil import parser as _dateutil_parser
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -18,6 +20,20 @@ TimestampInput = Union[int, float, str, datetime, pd.Timestamp]
|
|||||||
|
|
||||||
NANOS_PER_SECOND = 1_000_000_000
|
NANOS_PER_SECOND = 1_000_000_000
|
||||||
|
|
||||||
|
_RELATIVE_RE = re.compile(
|
||||||
|
r'^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_relative_date(s: str) -> datetime | None:
|
||||||
|
"""Parse relative date strings like '30 days ago', '2 weeks ago'."""
|
||||||
|
m = _RELATIVE_RE.match(s.strip())
|
||||||
|
if not m:
|
||||||
|
return None
|
||||||
|
n, unit = int(m.group(1)), m.group(2).lower()
|
||||||
|
return datetime.now(timezone.utc) - relativedelta(**{f'{unit}s': n})
|
||||||
|
|
||||||
|
|
||||||
def to_nanoseconds(timestamp: TimestampInput) -> int:
|
def to_nanoseconds(timestamp: TimestampInput) -> int:
|
||||||
"""
|
"""
|
||||||
@@ -31,6 +47,7 @@ def to_nanoseconds(timestamp: TimestampInput) -> int:
|
|||||||
timestamp: Can be:
|
timestamp: Can be:
|
||||||
- Unix timestamp (int/float) - assumed to be in seconds
|
- Unix timestamp (int/float) - assumed to be in seconds
|
||||||
- ISO date string (str) - parsed using dateutil
|
- ISO date string (str) - parsed using dateutil
|
||||||
|
- Relative date string (str) - e.g. "30 days ago", "2 weeks ago"
|
||||||
- datetime object
|
- datetime object
|
||||||
- pandas Timestamp
|
- pandas Timestamp
|
||||||
|
|
||||||
@@ -48,7 +65,9 @@ def to_nanoseconds(timestamp: TimestampInput) -> int:
|
|||||||
if isinstance(timestamp, (int, float)):
|
if isinstance(timestamp, (int, float)):
|
||||||
return int(timestamp * NANOS_PER_SECOND)
|
return int(timestamp * NANOS_PER_SECOND)
|
||||||
elif isinstance(timestamp, str):
|
elif isinstance(timestamp, str):
|
||||||
dt = dateparser.parse(timestamp)
|
dt = _parse_relative_date(timestamp)
|
||||||
|
if dt is None:
|
||||||
|
dt = _dateutil_parser.parse(timestamp)
|
||||||
if dt is None:
|
if dt is None:
|
||||||
raise ValueError(f"Could not parse date string: {timestamp}")
|
raise ValueError(f"Could not parse date string: {timestamp}")
|
||||||
return int(dt.timestamp() * NANOS_PER_SECOND)
|
return int(dt.timestamp() * NANOS_PER_SECOND)
|
||||||
|
|||||||
@@ -155,6 +155,21 @@ def _remove_indicator_instances(workspace_store, pandas_ta_name: str) -> None:
|
|||||||
logging.warning(f"Failed to remove indicator instances for {pandas_ta_name}", exc_info=True)
|
logging.warning(f"Failed to remove indicator instances for {pandas_ta_name}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _workspace_sync_content(workspace_store, category: str) -> "TextContent | None":
|
||||||
|
"""
|
||||||
|
Return a TextContent item carrying the current {category}_types workspace state so the
|
||||||
|
gateway can sync it to connected web clients without a separate workspace_patch call.
|
||||||
|
The gateway detects items of the form {"_workspace_sync": {"store": ..., "data": ...}}.
|
||||||
|
"""
|
||||||
|
store = _type_store_name(category)
|
||||||
|
result = workspace_store.read(store)
|
||||||
|
if not result.get('exists'):
|
||||||
|
return None
|
||||||
|
import json as _json
|
||||||
|
payload = _json.dumps({"_workspace_sync": {"store": store, "data": result.get("data")}})
|
||||||
|
return TextContent(type="text", text=payload)
|
||||||
|
|
||||||
|
|
||||||
def _populate_types_from_disk(workspace_store, category_manager, category: str) -> None:
|
def _populate_types_from_disk(workspace_store, category_manager, category: str) -> None:
|
||||||
"""Scan existing category items and add any missing entries to the {category}_types store."""
|
"""Scan existing category items and add any missing entries to the {category}_types store."""
|
||||||
store = _type_store_name(category)
|
store = _type_store_name(category)
|
||||||
@@ -921,6 +936,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
|||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
_upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", ""))
|
_upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", ""))
|
||||||
await cleanup_extra_packages_async(get_data_dir(), _get_env_yml())
|
await cleanup_extra_packages_async(get_data_dir(), _get_env_yml())
|
||||||
|
sync = _workspace_sync_content(workspace_store, arguments.get("category", ""))
|
||||||
|
if sync:
|
||||||
|
content.append(sync)
|
||||||
return content
|
return content
|
||||||
elif name == "python_edit":
|
elif name == "python_edit":
|
||||||
result = await category_manager.edit(
|
result = await category_manager.edit(
|
||||||
@@ -952,6 +970,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
|||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
_upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", ""))
|
_upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", ""))
|
||||||
await cleanup_extra_packages_async(get_data_dir(), _get_env_yml())
|
await cleanup_extra_packages_async(get_data_dir(), _get_env_yml())
|
||||||
|
sync = _workspace_sync_content(workspace_store, arguments.get("category", ""))
|
||||||
|
if sync:
|
||||||
|
content.append(sync)
|
||||||
return content
|
return content
|
||||||
elif name == "python_read":
|
elif name == "python_read":
|
||||||
return category_manager.read(
|
return category_manager.read(
|
||||||
@@ -987,6 +1008,11 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
|||||||
meta_parts.append(f"validation errors: {result['validation'].get('errors', [])}")
|
meta_parts.append(f"validation errors: {result['validation'].get('errors', [])}")
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
_upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", ""))
|
_upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", ""))
|
||||||
|
sync = _workspace_sync_content(workspace_store, arguments.get("category", ""))
|
||||||
|
content_out = [TextContent(type="text", text="\n".join(meta_parts))]
|
||||||
|
if sync:
|
||||||
|
content_out.append(sync)
|
||||||
|
return content_out
|
||||||
return [TextContent(type="text", text="\n".join(meta_parts))]
|
return [TextContent(type="text", text="\n".join(meta_parts))]
|
||||||
elif name == "python_delete":
|
elif name == "python_delete":
|
||||||
result = await category_manager.delete(
|
result = await category_manager.delete(
|
||||||
@@ -1002,7 +1028,12 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
|||||||
for k in ("category", "name", "revision", "packages_removed", "error"):
|
for k in ("category", "name", "revision", "packages_removed", "error"):
|
||||||
if result.get(k):
|
if result.get(k):
|
||||||
parts.append(f"{k}: {result[k]}")
|
parts.append(f"{k}: {result[k]}")
|
||||||
return [TextContent(type="text", text="\n".join(parts))]
|
content_out = [TextContent(type="text", text="\n".join(parts))]
|
||||||
|
if result.get("success"):
|
||||||
|
sync = _workspace_sync_content(workspace_store, arguments.get("category", ""))
|
||||||
|
if sync:
|
||||||
|
content_out.append(sync)
|
||||||
|
return content_out
|
||||||
elif name == "conda_sync":
|
elif name == "conda_sync":
|
||||||
return await sync_packages_async(
|
return await sync_packages_async(
|
||||||
data_dir=get_data_dir(),
|
data_dir=get_data_dir(),
|
||||||
|
|||||||
@@ -653,10 +653,10 @@ onUnmounted(() => {
|
|||||||
v-if="isAgentProcessing"
|
v-if="isAgentProcessing"
|
||||||
slot="send-icon"
|
slot="send-icon"
|
||||||
@click.stop="stopAgent"
|
@click.stop="stopAgent"
|
||||||
style="display:flex;align-items:center;justify-content:center;width:100%;height:100%"
|
class="stop-btn"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18">
|
||||||
<rect x="4" y="4" width="16" height="16" rx="2" fill="#f23645"/>
|
<rect x="4" y="4" width="16" height="16" rx="2" fill="currentColor"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</vue-advanced-chat>
|
</vue-advanced-chat>
|
||||||
@@ -664,6 +664,30 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.stop-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: #f23645;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: color 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn:hover {
|
||||||
|
color: #ff4d5e;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 0 8px rgba(242, 54, 69, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-btn:active {
|
||||||
|
color: #c41e2e;
|
||||||
|
transform: scale(0.92);
|
||||||
|
box-shadow: 0 0 4px rgba(242, 54, 69, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
.chat-container {
|
.chat-container {
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user