Improve active run timing and turn limits
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user