Improve active run timing and turn limits

This commit is contained in:
武阳
2026-05-07 17:51:23 +08:00
parent d57570e527
commit b1868a6401
7 changed files with 68 additions and 24 deletions
@@ -426,7 +426,12 @@ export function toReplayRepository(
role: message.role, role: message.role,
parts, parts,
...(message.role === "assistant" ...(message.role === "assistant"
? { metadata: { sessionId: session.session_id ?? fallbackSessionId } } ? {
metadata: {
sessionId: session.session_id ?? fallbackSessionId,
elapsedMs: normalizeElapsedMs(message.metadata?.elapsed_ms),
},
}
: {}), : {}),
} as UIMessage; } as UIMessage;
const threadMessage = { const threadMessage = {
@@ -442,7 +447,10 @@ export function toReplayRepository(
unstable_annotations: [], unstable_annotations: [],
unstable_data: [], unstable_data: [],
steps: [], steps: [],
custom: { sessionId: session.session_id ?? fallbackSessionId }, custom: {
sessionId: session.session_id ?? fallbackSessionId,
elapsedMs: normalizeElapsedMs(message.metadata?.elapsed_ms),
},
}, },
} }
: { metadata: { custom: {} } }), : { metadata: { custom: {} } }),
@@ -536,8 +544,9 @@ function buildActiveRunParts(runStatus: ClawActiveRunStatus) {
? "当前会话已有任务在执行,本轮正在排队。" ? "当前会话已有任务在执行,本轮正在排队。"
: runStatus.current_stage || "后端正在执行这个会话。"; : runStatus.current_stage || "后端正在执行这个会话。";
const reasoningText = eventLines.length ? eventLines.join("\n") : fallback; const reasoningText = eventLines.length ? eventLines.join("\n") : fallback;
const runStartedAtMs = normalizeRunTimestampMs(runStatus.started_at);
return [ return [
{ type: "reasoning", text: reasoningText }, { type: "reasoning", text: reasoningText, runStartedAtMs },
...buildRunToolParts(runStatus.events ?? []), ...buildRunToolParts(runStatus.events ?? []),
]; ];
} }
@@ -694,6 +703,11 @@ function normalizeRunTimestampMs(value: unknown) {
return value < 10_000_000_000 ? Math.round(value * 1000) : Math.round(value); return value < 10_000_000_000 ? Math.round(value * 1000) : Math.round(value);
} }
function normalizeElapsedMs(value: unknown) {
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
return Math.max(0, Math.round(value));
}
function findLastAssistantContentIndex(messages: readonly ClawStoredMessage[]) { function findLastAssistantContentIndex(messages: readonly ClawStoredMessage[]) {
for (let index = messages.length - 1; index >= 0; index -= 1) { for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index]; const message = messages[index];
@@ -1217,11 +1217,20 @@ const ActivityChainSummary: FC<{
}> = ({ messageId, partIndex }) => { }> = ({ messageId, partIndex }) => {
const { openItem } = useActivityPanel(); const { openItem } = useActivityPanel();
const startedAtRef = useRef(Date.now()); const startedAtRef = useRef(Date.now());
const [elapsedMs, setElapsedMs] = useState(0); const metadataRunStartedAtMs = useAuiState((s) => {
const runStartedAtMs = useAuiState((s) => {
const value = readMetadataValue(s.message.metadata, "runStartedAtMs"); const value = readMetadataValue(s.message.metadata, "runStartedAtMs");
return typeof value === "number" && Number.isFinite(value) ? value : null; return typeof value === "number" && Number.isFinite(value) ? value : null;
}); });
const contentRunStartedAtMs = useAuiState((s) =>
findNumberInActivityParts(s.message.content, "runStartedAtMs"),
);
const storedElapsedMs = useAuiState((s) => {
const value = readMetadataValue(s.message.metadata, "elapsedMs");
return typeof value === "number" && Number.isFinite(value)
? Math.max(0, Math.round(value))
: null;
});
const [elapsedMs, setElapsedMs] = useState(storedElapsedMs ?? 0);
const label = useAuiState((s) => { const label = useAuiState((s) => {
const message = s.message; const message = s.message;
const status = const status =
@@ -1238,14 +1247,18 @@ const ActivityChainSummary: FC<{
}); });
useEffect(() => { useEffect(() => {
if (!active) return; if (!active) {
const startedAt = runStartedAtMs ?? startedAtRef.current; if (storedElapsedMs !== null) setElapsedMs(storedElapsedMs);
return;
}
const startedAt =
metadataRunStartedAtMs ?? contentRunStartedAtMs ?? startedAtRef.current;
setElapsedMs(Math.max(0, Date.now() - startedAt)); setElapsedMs(Math.max(0, Date.now() - startedAt));
const timer = window.setInterval(() => { const timer = window.setInterval(() => {
setElapsedMs(Math.max(0, Date.now() - startedAt)); setElapsedMs(Math.max(0, Date.now() - startedAt));
}, 1000); }, 1000);
return () => window.clearInterval(timer); return () => window.clearInterval(timer);
}, [active, runStartedAtMs]); }, [active, metadataRunStartedAtMs, contentRunStartedAtMs, storedElapsedMs]);
return ( return (
<button <button
@@ -1272,8 +1285,20 @@ type ActivitySummaryPart = {
result?: unknown; result?: unknown;
args?: unknown; args?: unknown;
toolName?: string; toolName?: string;
runStartedAtMs?: unknown;
}; };
function findNumberInActivityParts(
content: readonly ActivitySummaryPart[],
key: keyof ActivitySummaryPart,
) {
for (const part of content) {
const value = part[key];
if (typeof value === "number" && Number.isFinite(value)) return value;
}
return null;
}
function summarizeActivityChainLabel( function summarizeActivityChainLabel(
content: readonly ActivitySummaryPart[], content: readonly ActivitySummaryPart[],
status: string | undefined, status: string | undefined,
+5 -5
View File
@@ -27,7 +27,7 @@ from .agent_tool_core import (
from .agent_tool_specs.data_agent import build_data_agent_tools from .agent_tool_specs.data_agent import build_data_agent_tools
from .agent_tool_specs.execution import build_execution_tools from .agent_tool_specs.execution import build_execution_tools
from .agent_tool_specs.files import build_file_tools from .agent_tool_specs.files import build_file_tools
from .agent_types import ToolExecutionResult from .agent_types import DEFAULT_MAX_TURNS, ToolExecutionResult
from .data_agent_records import ( from .data_agent_records import (
DataRecordError, DataRecordError,
confirm_generation_goal, confirm_generation_goal,
@@ -938,7 +938,7 @@ def default_tool_registry() -> dict[str, AgentTool]:
'properties': { 'properties': {
'prompt': {'type': 'string'}, 'prompt': {'type': 'string'},
'label': {'type': 'string'}, 'label': {'type': 'string'},
'max_turns': {'type': 'integer', 'minimum': 1, 'maximum': 20}, 'max_turns': {'type': 'integer', 'minimum': 1, 'maximum': DEFAULT_MAX_TURNS},
'resume_session_id': {'type': 'string'}, 'resume_session_id': {'type': 'string'},
'session_id': {'type': 'string'}, 'session_id': {'type': 'string'},
'depends_on': { 'depends_on': {
@@ -953,7 +953,7 @@ def default_tool_registry() -> dict[str, AgentTool]:
}, },
'resume_session_id': {'type': 'string'}, 'resume_session_id': {'type': 'string'},
'session_id': {'type': 'string'}, 'session_id': {'type': 'string'},
'max_turns': {'type': 'integer', 'minimum': 1, 'maximum': 20}, 'max_turns': {'type': 'integer', 'minimum': 1, 'maximum': DEFAULT_MAX_TURNS},
'allow_write': {'type': 'boolean'}, 'allow_write': {'type': 'boolean'},
'allow_shell': {'type': 'boolean'}, 'allow_shell': {'type': 'boolean'},
'include_parent_context': {'type': 'boolean'}, 'include_parent_context': {'type': 'boolean'},
@@ -983,7 +983,7 @@ def default_tool_registry() -> dict[str, AgentTool]:
'properties': { 'properties': {
'prompt': {'type': 'string'}, 'prompt': {'type': 'string'},
'label': {'type': 'string'}, 'label': {'type': 'string'},
'max_turns': {'type': 'integer', 'minimum': 1, 'maximum': 20}, 'max_turns': {'type': 'integer', 'minimum': 1, 'maximum': DEFAULT_MAX_TURNS},
'resume_session_id': {'type': 'string'}, 'resume_session_id': {'type': 'string'},
'session_id': {'type': 'string'}, 'session_id': {'type': 'string'},
'depends_on': { 'depends_on': {
@@ -998,7 +998,7 @@ def default_tool_registry() -> dict[str, AgentTool]:
}, },
'resume_session_id': {'type': 'string'}, 'resume_session_id': {'type': 'string'},
'session_id': {'type': 'string'}, 'session_id': {'type': 'string'},
'max_turns': {'type': 'integer', 'minimum': 1, 'maximum': 20}, 'max_turns': {'type': 'integer', 'minimum': 1, 'maximum': DEFAULT_MAX_TURNS},
'allow_write': {'type': 'boolean'}, 'allow_write': {'type': 'boolean'},
'allow_shell': {'type': 'boolean'}, 'allow_shell': {'type': 'boolean'},
'include_parent_context': {'type': 'boolean'}, 'include_parent_context': {'type': 'boolean'},
+4 -1
View File
@@ -150,10 +150,13 @@ class AgentPermissions:
allow_destructive_shell_commands: bool = False allow_destructive_shell_commands: bool = False
DEFAULT_MAX_TURNS = 50
@dataclass(frozen=True) @dataclass(frozen=True)
class AgentRuntimeConfig: class AgentRuntimeConfig:
cwd: Path cwd: Path
max_turns: int = 12 max_turns: int = DEFAULT_MAX_TURNS
command_timeout_seconds: float = 30.0 command_timeout_seconds: float = 30.0
max_output_chars: int = 12000 max_output_chars: int = 12000
stream_model_responses: bool = False stream_model_responses: bool = False
+9 -8
View File
@@ -23,6 +23,7 @@ from .agent_types import (
AgentPermissions, AgentPermissions,
AgentRuntimeConfig, AgentRuntimeConfig,
BudgetConfig, BudgetConfig,
DEFAULT_MAX_TURNS,
ModelConfig, ModelConfig,
ModelPricing, ModelPricing,
OutputSchemaConfig, OutputSchemaConfig,
@@ -104,7 +105,7 @@ def _add_agent_common_args(parser: argparse.ArgumentParser, *, include_backend:
def _build_runtime_config(args: argparse.Namespace) -> AgentRuntimeConfig: def _build_runtime_config(args: argparse.Namespace) -> AgentRuntimeConfig:
return AgentRuntimeConfig( return AgentRuntimeConfig(
cwd=Path(args.cwd).resolve(), cwd=Path(args.cwd).resolve(),
max_turns=getattr(args, 'max_turns', 12), max_turns=getattr(args, 'max_turns', None) or DEFAULT_MAX_TURNS,
permissions=AgentPermissions( permissions=AgentPermissions(
allow_file_write=args.allow_write, allow_file_write=args.allow_write,
allow_shell_commands=args.allow_shell, allow_shell_commands=args.allow_shell,
@@ -228,7 +229,7 @@ def _append_agent_forwarded_args(
include_backend: bool, include_backend: bool,
) -> None: ) -> None:
command.extend(['--cwd', str(args.cwd)]) command.extend(['--cwd', str(args.cwd)])
command.extend(['--max-turns', str(getattr(args, 'max_turns', 12))]) command.extend(['--max-turns', str(getattr(args, 'max_turns', None) or DEFAULT_MAX_TURNS)])
if include_backend: if include_backend:
command.extend(['--model', str(args.model)]) command.extend(['--model', str(args.model)])
command.extend(['--base-url', str(args.base_url)]) command.extend(['--base-url', str(args.base_url)])
@@ -837,13 +838,13 @@ def build_parser() -> argparse.ArgumentParser:
agent_parser = subparsers.add_parser('agent', help='run the real Python local-model agent') agent_parser = subparsers.add_parser('agent', help='run the real Python local-model agent')
agent_parser.add_argument('prompt') agent_parser.add_argument('prompt')
agent_parser.add_argument('--max-turns', type=int, default=12) agent_parser.add_argument('--max-turns', type=int, default=DEFAULT_MAX_TURNS)
agent_parser.add_argument('--show-transcript', action='store_true') agent_parser.add_argument('--show-transcript', action='store_true')
_add_agent_common_args(agent_parser, include_backend=True) _add_agent_common_args(agent_parser, include_backend=True)
background_parser = subparsers.add_parser('agent-bg', help='run the Python local-model agent as a local background session') background_parser = subparsers.add_parser('agent-bg', help='run the Python local-model agent as a local background session')
background_parser.add_argument('prompt') background_parser.add_argument('prompt')
background_parser.add_argument('--max-turns', type=int, default=12) background_parser.add_argument('--max-turns', type=int, default=DEFAULT_MAX_TURNS)
background_parser.add_argument('--show-transcript', action='store_true') background_parser.add_argument('--show-transcript', action='store_true')
_add_agent_common_args(background_parser, include_backend=True) _add_agent_common_args(background_parser, include_backend=True)
@@ -851,7 +852,7 @@ def build_parser() -> argparse.ArgumentParser:
background_worker_parser.add_argument('background_id') background_worker_parser.add_argument('background_id')
background_worker_parser.add_argument('prompt') background_worker_parser.add_argument('prompt')
background_worker_parser.add_argument('--background-root', required=True) background_worker_parser.add_argument('--background-root', required=True)
background_worker_parser.add_argument('--max-turns', type=int, default=12) background_worker_parser.add_argument('--max-turns', type=int, default=DEFAULT_MAX_TURNS)
background_worker_parser.add_argument('--show-transcript', action='store_true') background_worker_parser.add_argument('--show-transcript', action='store_true')
_add_agent_common_args(background_worker_parser, include_backend=True) _add_agent_common_args(background_worker_parser, include_backend=True)
@@ -875,7 +876,7 @@ def build_parser() -> argparse.ArgumentParser:
daemon_start_parser = daemon_subparsers.add_parser('start', help='launch a local daemon-style background agent session') daemon_start_parser = daemon_subparsers.add_parser('start', help='launch a local daemon-style background agent session')
daemon_start_parser.add_argument('prompt') daemon_start_parser.add_argument('prompt')
daemon_start_parser.add_argument('--max-turns', type=int, default=12) daemon_start_parser.add_argument('--max-turns', type=int, default=DEFAULT_MAX_TURNS)
daemon_start_parser.add_argument('--show-transcript', action='store_true') daemon_start_parser.add_argument('--show-transcript', action='store_true')
_add_agent_common_args(daemon_start_parser, include_backend=True) _add_agent_common_args(daemon_start_parser, include_backend=True)
@@ -883,7 +884,7 @@ def build_parser() -> argparse.ArgumentParser:
daemon_worker_parser.add_argument('background_id') daemon_worker_parser.add_argument('background_id')
daemon_worker_parser.add_argument('prompt') daemon_worker_parser.add_argument('prompt')
daemon_worker_parser.add_argument('--background-root', required=True) daemon_worker_parser.add_argument('--background-root', required=True)
daemon_worker_parser.add_argument('--max-turns', type=int, default=12) daemon_worker_parser.add_argument('--max-turns', type=int, default=DEFAULT_MAX_TURNS)
daemon_worker_parser.add_argument('--show-transcript', action='store_true') daemon_worker_parser.add_argument('--show-transcript', action='store_true')
_add_agent_common_args(daemon_worker_parser, include_backend=True) _add_agent_common_args(daemon_worker_parser, include_backend=True)
@@ -904,7 +905,7 @@ def build_parser() -> argparse.ArgumentParser:
chat_parser = subparsers.add_parser('agent-chat', help='run an interactive Python local-model chat loop') chat_parser = subparsers.add_parser('agent-chat', help='run an interactive Python local-model chat loop')
chat_parser.add_argument('prompt', nargs='?') chat_parser.add_argument('prompt', nargs='?')
chat_parser.add_argument('--resume-session-id') chat_parser.add_argument('--resume-session-id')
chat_parser.add_argument('--max-turns', type=int, default=12) chat_parser.add_argument('--max-turns', type=int, default=DEFAULT_MAX_TURNS)
chat_parser.add_argument('--show-transcript', action='store_true') chat_parser.add_argument('--show-transcript', action='store_true')
_add_agent_common_args(chat_parser, include_backend=True) _add_agent_common_args(chat_parser, include_backend=True)
+2 -1
View File
@@ -9,6 +9,7 @@ from .agent_types import (
AgentPermissions, AgentPermissions,
AgentRuntimeConfig, AgentRuntimeConfig,
BudgetConfig, BudgetConfig,
DEFAULT_MAX_TURNS,
ModelConfig, ModelConfig,
ModelPricing, ModelPricing,
OutputSchemaConfig, OutputSchemaConfig,
@@ -204,7 +205,7 @@ def deserialize_runtime_config(payload: JSONDict) -> AgentRuntimeConfig:
output_schema_payload = payload.get('output_schema') output_schema_payload = payload.get('output_schema')
return AgentRuntimeConfig( return AgentRuntimeConfig(
cwd=Path(str(payload['cwd'])).resolve(), cwd=Path(str(payload['cwd'])).resolve(),
max_turns=int(payload.get('max_turns', 12)), max_turns=int(payload.get('max_turns', DEFAULT_MAX_TURNS)),
command_timeout_seconds=float(payload.get('command_timeout_seconds', 30.0)), command_timeout_seconds=float(payload.get('command_timeout_seconds', 30.0)),
max_output_chars=int(payload.get('max_output_chars', 12000)), max_output_chars=int(payload.get('max_output_chars', 12000)),
stream_model_responses=bool(payload.get('stream_model_responses', False)), stream_model_responses=bool(payload.get('stream_model_responses', False)),
+1 -1
View File
@@ -360,7 +360,7 @@ class TestRuntimeConfigSerialization(unittest.TestCase):
payload = {'cwd': '/home/user'} payload = {'cwd': '/home/user'}
config = deserialize_runtime_config(payload) config = deserialize_runtime_config(payload)
self.assertEqual(config.max_turns, 12) self.assertEqual(config.max_turns, 50)
self.assertAlmostEqual(config.command_timeout_seconds, 30.0) self.assertAlmostEqual(config.command_timeout_seconds, 30.0)
self.assertEqual(config.max_output_chars, 12000) self.assertEqual(config.max_output_chars, 12000)
self.assertFalse(config.stream_model_responses) self.assertFalse(config.stream_model_responses)