mindcode 0.5.1__tar.gz → 0.5.3__tar.gz

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.
Files changed (101) hide show
  1. {mindcode-0.5.1 → mindcode-0.5.3}/PKG-INFO +1 -1
  2. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/_version.py +1 -1
  3. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/acp_agent/agent.py +65 -6
  4. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/schema.py +5 -3
  5. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/__init__.py +10 -3
  6. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/commands/bench.py +15 -3
  7. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/commands/chat.py +36 -6
  8. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/commands/shell.py +38 -6
  9. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/commands/task.py +26 -8
  10. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/shell/input_pipeline.py +13 -0
  11. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/shell/repl.py +124 -40
  12. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/shell/slash.py +6 -24
  13. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/shell/tui.py +128 -76
  14. mindcode-0.5.3/mindcode/config/settings.toml +41 -0
  15. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/config.py +173 -2
  16. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/render.py +13 -4
  17. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/runtime.py +43 -16
  18. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/tasking.py +8 -5
  19. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode.egg-info/PKG-INFO +1 -1
  20. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode.egg-info/SOURCES.txt +1 -0
  21. {mindcode-0.5.1 → mindcode-0.5.3}/pyproject.toml +4 -1
  22. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_acp_agent.py +56 -3
  23. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_bench.py +0 -12
  24. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_cli_commands.py +95 -0
  25. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_config.py +43 -1
  26. mindcode-0.5.3/tests/test_provider_retry_defaults.py +185 -0
  27. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_render_exec.py +62 -0
  28. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_shell_channels.py +61 -55
  29. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_shell_interrupt.py +72 -19
  30. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_tasking.py +2 -0
  31. mindcode-0.5.1/tests/test_provider_retry_defaults.py +0 -80
  32. {mindcode-0.5.1 → mindcode-0.5.3}/README.md +0 -0
  33. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/__init__.py +0 -0
  34. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/__main__.py +0 -0
  35. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/acp_agent/__init__.py +0 -0
  36. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/acp_agent/__main__.py +0 -0
  37. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/approval.py +0 -0
  38. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/__init__.py +0 -0
  39. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/adapters/__init__.py +0 -0
  40. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/adapters/base.py +0 -0
  41. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/adapters/mini_bfcl.py +0 -0
  42. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/adapters/mini_gaia.py +0 -0
  43. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/adapters/mini_terminal.py +0 -0
  44. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/adapters/swe_bench.py +0 -0
  45. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/adapters/terminal_bench.py +0 -0
  46. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/fake_tools.py +0 -0
  47. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/jsonutil.py +0 -0
  48. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/paths.py +0 -0
  49. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/predictions.py +0 -0
  50. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/report.py +0 -0
  51. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/runner.py +0 -0
  52. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/scorers/base.py +0 -0
  53. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/scorers/composite.py +0 -0
  54. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/scorers/exact.py +0 -0
  55. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/scorers/json_call.py +0 -0
  56. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/scorers/swe.py +0 -0
  57. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/scorers/terminal.py +0 -0
  58. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/scorers/terminal_bench.py +0 -0
  59. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/trace.py +0 -0
  60. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/bench/workspace.py +0 -0
  61. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/_shared.py +0 -0
  62. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/commands/__init__.py +0 -0
  63. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/commands/config_cmd.py +0 -0
  64. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/commands/remote.py +0 -0
  65. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/commands/status.py +0 -0
  66. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/commands/terminal.py +0 -0
  67. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/shell/__init__.py +0 -0
  68. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/shell/completion.py +0 -0
  69. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/shell/menu.py +0 -0
  70. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/cli/shell/startup.py +0 -0
  71. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/mcp.py +0 -0
  72. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/policy.py +0 -0
  73. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/remote.py +0 -0
  74. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/skills.py +0 -0
  75. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/subagents.py +0 -0
  76. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/terminal/__init__.py +0 -0
  77. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/terminal/__main__.py +0 -0
  78. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode/terminal_core.py +0 -0
  79. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode.egg-info/dependency_links.txt +0 -0
  80. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode.egg-info/entry_points.txt +0 -0
  81. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode.egg-info/requires.txt +0 -0
  82. {mindcode-0.5.1 → mindcode-0.5.3}/mindcode.egg-info/top_level.txt +0 -0
  83. {mindcode-0.5.1 → mindcode-0.5.3}/setup.cfg +0 -0
  84. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_approval.py +0 -0
  85. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_diff_capture.py +0 -0
  86. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_exec_command_guard.py +0 -0
  87. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_external_file_approval.py +0 -0
  88. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_policy_choice.py +0 -0
  89. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_policy_timeout.py +0 -0
  90. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_reasoner_compat.py +0 -0
  91. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_remote.py +0 -0
  92. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_shell_completion.py +0 -0
  93. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_shell_session.py +0 -0
  94. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_shell_slash_priority.py +0 -0
  95. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_shell_tui.py +0 -0
  96. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_skills_mcp.py +0 -0
  97. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_slash_resume.py +0 -0
  98. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_subagents.py +0 -0
  99. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_system_prompt.py +0 -0
  100. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_task_resume.py +0 -0
  101. {mindcode-0.5.1 → mindcode-0.5.3}/tests/test_terminal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mindcode
3
- Version: 0.5.1
3
+ Version: 0.5.3
4
4
  Summary: codex-style interactive coding agent on top of mindagent
5
5
  Author: mindcode
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  """mindcode 版本与品牌信息。"""
2
2
 
3
- __version__ = "0.5.1"
3
+ __version__ = "0.5.3"
4
4
 
5
5
  __logo__ = r"""
6
6
  [bold cyan] _ _[/bold cyan]
@@ -34,7 +34,6 @@ from acp.schema import (
34
34
  PermissionOption,
35
35
  PromptCapabilities,
36
36
  PromptResponse,
37
- RequestPermissionResponse,
38
37
  TextContentBlock,
39
38
  ToolCallStart,
40
39
  )
@@ -43,6 +42,8 @@ from mindagent.core import (
43
42
  AgentSession,
44
43
  AgentRuntime,
45
44
  EventType,
45
+ ExternalFileReadDecision,
46
+ ExternalFileReadRequest,
46
47
  LocalConversationStore,
47
48
  RunOutcome,
48
49
  SessionEnvironment,
@@ -50,7 +51,7 @@ from mindagent.core import (
50
51
  SessionPolicy,
51
52
  )
52
53
  from ..approval import describe_action
53
- from ..config import ConfigError, ConfigStore, ProviderConfig
54
+ from ..config import BUILTIN_SETTINGS, ConfigError, ConfigStore, ProviderConfig
54
55
  from ..policy import ApprovalMode
55
56
  from ..runtime import build_runtime
56
57
 
@@ -58,9 +59,6 @@ logger = logging.getLogger("mindcode.acp_agent")
58
59
 
59
60
  PROTOCOL_VERSION = 1 # stable ACP v1; see docs/acp-layering.md §5
60
61
 
61
- _PERMISSION_TIMEOUT_S = 120.0
62
-
63
-
64
62
  def _stop_reason(result: Any) -> str:
65
63
  """Map a native run result to an ACP v1 stop reason (layering §3)."""
66
64
  outcome = getattr(result, "outcome", None)
@@ -123,12 +121,22 @@ class _AgentHarness:
123
121
  config_store: ConfigStore | None = None,
124
122
  cwd: Path | None = None,
125
123
  subagents: bool = False,
126
- permission_timeout_s: float = _PERMISSION_TIMEOUT_S,
124
+ permission_timeout_s: float | None = None,
127
125
  ) -> None:
128
126
  self._conn = conn
129
127
  self._config_store = config_store or ConfigStore()
130
128
  self._cwd = cwd or Path.cwd()
131
129
  self._subagents = subagents
130
+ self._permission_timeout_from_config = permission_timeout_s is None
131
+ if permission_timeout_s is None:
132
+ try:
133
+ permission_timeout_s = (
134
+ self._config_store.read()
135
+ .runtime_settings.permission_timeout_s
136
+ )
137
+ except AttributeError:
138
+ # Lightweight test/fake stores may only implement resolve().
139
+ permission_timeout_s = BUILTIN_SETTINGS.permission_timeout_s
132
140
  self._permission_timeout_s = permission_timeout_s
133
141
  self._sessions: dict[str, _ManagedSession] = {}
134
142
  self._new_session_lock = asyncio.Lock()
@@ -171,6 +179,10 @@ class _AgentHarness:
171
179
  resolved = self._config_store.resolve()
172
180
  except ConfigError as exc:
173
181
  raise RequestError.internal_error(str(exc)) from exc
182
+ if self._permission_timeout_from_config:
183
+ self._permission_timeout_s = (
184
+ resolved.runtime_settings.permission_timeout_s
185
+ )
174
186
  store = LocalConversationStore(
175
187
  self._config_store.root / "sessions"
176
188
  )
@@ -181,6 +193,7 @@ class _AgentHarness:
181
193
  stream=True,
182
194
  event_handler=None,
183
195
  approval_callback=self._make_approval(resolved),
196
+ file_access_callback=self._make_file_access_approval(),
184
197
  )
185
198
  try:
186
199
  session = runtime.create_session(
@@ -309,6 +322,52 @@ class _AgentHarness:
309
322
 
310
323
  return approve
311
324
 
325
+ def _make_file_access_approval(self) -> Any:
326
+ async def approve(
327
+ context: Any,
328
+ request: ExternalFileReadRequest,
329
+ ) -> ExternalFileReadDecision:
330
+ managed = self._managed(context.session_id)
331
+ started = helpers.start_tool_call(
332
+ tool_call_id=f"external-read-{request.request_id}",
333
+ title=f"Read external file {request.path}",
334
+ kind="read",
335
+ status="pending",
336
+ )
337
+ await self._notify_update(managed, started)
338
+ options = [
339
+ PermissionOption(
340
+ option_id="allow", name="Allow once", kind="allow_once"
341
+ ),
342
+ PermissionOption(
343
+ option_id="reject", name="Reject", kind="reject_once"
344
+ ),
345
+ ]
346
+ approved = False
347
+ try:
348
+ response = await asyncio.wait_for(
349
+ self._conn.request_permission(
350
+ session_id=managed.session_id,
351
+ tool_call=started,
352
+ options=options,
353
+ ),
354
+ timeout=self._permission_timeout_s,
355
+ )
356
+ except asyncio.TimeoutError:
357
+ logger.warning(
358
+ "external read permission timed out after %.0fs → deny",
359
+ self._permission_timeout_s,
360
+ )
361
+ else:
362
+ outcome = response.outcome
363
+ approved = (
364
+ getattr(outcome, "outcome", None) != "cancelled"
365
+ and getattr(outcome, "option_id", None) == "allow"
366
+ )
367
+ return ExternalFileReadDecision(request.request_id, approved)
368
+
369
+ return approve
370
+
312
371
  # ----- event-stream translation (native port 5 → session/update) -----
313
372
 
314
373
  async def _notify_update(self, managed: _ManagedSession, update: Any) -> None:
@@ -4,6 +4,8 @@ from dataclasses import dataclass, field
4
4
  from pathlib import Path
5
5
  from typing import Any
6
6
 
7
+ from ..config import BUILTIN_SETTINGS
8
+
7
9
 
8
10
  @dataclass(frozen=True)
9
11
  class BenchmarkFile:
@@ -59,9 +61,9 @@ class BenchmarkRunConfig:
59
61
  limit: int | None = None
60
62
  case_ids: tuple[str, ...] = ()
61
63
  concurrency: int = 1
62
- max_steps: int = 50
63
- step_timeout_s: float | None = 300.0
64
- total_timeout_s: float | None = 1800.0
64
+ max_steps: int = BUILTIN_SETTINGS.benchmark.max_steps
65
+ step_timeout_s: float | None = BUILTIN_SETTINGS.benchmark.step_timeout_s
66
+ total_timeout_s: float | None = BUILTIN_SETTINGS.benchmark.total_timeout_s
65
67
  enable_subagents: bool = False
66
68
  enable_background_tasks: bool = False
67
69
  seed: int | None = None
@@ -20,7 +20,13 @@
20
20
  from __future__ import annotations
21
21
 
22
22
  import typer
23
- from click import UsageError
23
+
24
+ try:
25
+ # Typer 0.27 vendors Click to keep its exception types aligned with its
26
+ # renderer; older Typer releases use the public Click package directly.
27
+ from typer.core import _click
28
+ except ImportError: # pragma: no cover - compatibility with older Typer
29
+ import click as _click
24
30
 
25
31
  from .._version import __version__
26
32
  from ._shared import console
@@ -46,9 +52,10 @@ class DefaultShellGroup(typer.core.TyperGroup):
46
52
  if args:
47
53
  first = args[0]
48
54
  if first == "shell":
49
- raise UsageError(
55
+ raise _click.exceptions.UsageError(
50
56
  "`mindcode shell` 已移除;请改用 "
51
- "`mindcode [PROMPT] [OPTIONS]`"
57
+ "`mindcode [PROMPT] [OPTIONS]`",
58
+ ctx,
52
59
  )
53
60
  if (
54
61
  first in PUBLIC_SUBCOMMANDS
@@ -86,15 +86,22 @@ def run(
86
86
  "--benchmark-root",
87
87
  help="benchmark case 根目录",
88
88
  ),
89
- max_steps: int = typer.Option(50, "--max-steps", help="每 case 最大 step"),
89
+ max_steps: int | None = typer.Option(
90
+ None, "--max-steps", help="每 case 最大 step"
91
+ ),
90
92
  total_timeout: float | None = typer.Option(
91
- 600.0,
93
+ None,
92
94
  "--total-timeout",
93
- help="每 case 总超时秒数",
95
+ help="每 case 总超时秒数,0 表示禁用",
94
96
  ),
95
97
  ) -> None:
96
98
  run_id = f"{suite}-{time.strftime('%Y%m%d-%H%M%S')}"
97
99
  try:
100
+ total_timeout_disabled = total_timeout == 0
101
+ if total_timeout is not None and total_timeout < 0:
102
+ raise ValueError("total timeout 必须为 0 或大于 0")
103
+ if total_timeout_disabled:
104
+ total_timeout = None
98
105
  approval_override = (
99
106
  ApprovalMode(approve_mode).value
100
107
  if approve_mode is not None
@@ -106,6 +113,11 @@ def run(
106
113
  approval_override=approval_override,
107
114
  )
108
115
  selected_approve_mode = resolved.approval_mode
116
+ benchmark_defaults = resolved.runtime_settings.benchmark
117
+ if max_steps is None:
118
+ max_steps = benchmark_defaults.max_steps
119
+ if total_timeout is None and not total_timeout_disabled:
120
+ total_timeout = benchmark_defaults.total_timeout_s
109
121
  output_root = Path(output).expanduser()
110
122
  run_output_dir = _run_output_dir(output_root, run_id)
111
123
  run_config = BenchmarkRunConfig(
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import typer
8
8
 
9
+ from ...config import BUILTIN_SETTINGS
9
10
  from ...policy import ApprovalMode
10
11
  from .._shared import console
11
12
  from ..shell import run_shell
@@ -41,15 +42,25 @@ def chat(
41
42
  False, "--no-stream", help="关闭流式输出"
42
43
  ),
43
44
  max_steps: int | None = typer.Option(
44
- None, "--max-steps", help="最大 step 数(默认 50)"
45
+ None,
46
+ "--max-steps",
47
+ help=f"最大 step 数(默认 {BUILTIN_SETTINGS.one_shot.max_steps})",
45
48
  ),
46
49
  step_timeout: float | None = typer.Option(
47
50
  None,
48
51
  "--step-timeout",
49
- help="单步超时秒数(默认 300,必须大于 0)",
52
+ help=(
53
+ "单步超时秒数(默认 "
54
+ f"{BUILTIN_SETTINGS.one_shot.step_timeout_s:g},必须大于 0)"
55
+ ),
50
56
  ),
51
57
  total_timeout: float | None = typer.Option(
52
- None, "--total-timeout", help="总超时秒数,0 表示禁用(默认 1800)"
58
+ None,
59
+ "--total-timeout",
60
+ help=(
61
+ "总超时秒数,0 表示禁用(默认 "
62
+ f"{BUILTIN_SETTINGS.one_shot.total_timeout_s:g})"
63
+ ),
53
64
  ),
54
65
  ) -> None:
55
66
  """发送单条消息,不进入交互模式。"""
@@ -78,13 +89,32 @@ def chat(
78
89
  resume=resume,
79
90
  stream=not no_stream,
80
91
  one_shot_prompt=message,
81
- max_steps=max_steps if max_steps is not None else 50,
92
+ max_steps=(
93
+ max_steps
94
+ if max_steps is not None
95
+ else BUILTIN_SETTINGS.one_shot.max_steps
96
+ ),
82
97
  step_timeout_s=(
83
- step_timeout if step_timeout is not None else 300.0
98
+ step_timeout
99
+ if step_timeout is not None
100
+ else BUILTIN_SETTINGS.one_shot.step_timeout_s
84
101
  ),
85
102
  total_timeout_s=None
86
103
  if total_timeout == 0
87
- else (total_timeout if total_timeout is not None else 1800.0),
104
+ else (
105
+ total_timeout
106
+ if total_timeout is not None
107
+ else BUILTIN_SETTINGS.one_shot.total_timeout_s
108
+ ),
109
+ settings_defaults=frozenset(
110
+ name
111
+ for name, value in (
112
+ ("max_steps", max_steps),
113
+ ("step_timeout_s", step_timeout),
114
+ ("total_timeout_s", total_timeout),
115
+ )
116
+ if value is None
117
+ ),
88
118
  )
89
119
  except ValueError as exc:
90
120
  console.print(f"[red]配置错误:[/red] {exc}")
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import typer
8
8
 
9
+ from ...config import BUILTIN_SETTINGS
9
10
  from ...policy import ApprovalMode
10
11
  from .._shared import console
11
12
  from ..shell import run_shell
@@ -52,17 +53,25 @@ def shell(
52
53
  help="使用 master/coder 委派模式(await_result)",
53
54
  ),
54
55
  max_steps: int | None = typer.Option(
55
- None, "--max-steps", help="最大 step 数(默认 50)"
56
+ None,
57
+ "--max-steps",
58
+ help=f"最大 step 数(默认 {BUILTIN_SETTINGS.one_shot.max_steps})",
56
59
  ),
57
60
  step_timeout: float | None = typer.Option(
58
61
  None,
59
62
  "--step-timeout",
60
- help="单步超时秒数(交互默认不限制;one-shot 默认 300,必须大于 0)",
63
+ help=(
64
+ "单步超时秒数(交互默认不限制;one-shot 默认 "
65
+ f"{BUILTIN_SETTINGS.one_shot.step_timeout_s:g},必须大于 0)"
66
+ ),
61
67
  ),
62
68
  total_timeout: float | None = typer.Option(
63
69
  None,
64
70
  "--total-timeout",
65
- help="总超时秒数,0 表示禁用(交互默认禁用;一次性/子代理默认 1800)",
71
+ help=(
72
+ "总超时秒数,0 表示禁用(交互默认禁用;一次性/子代理默认 "
73
+ f"{BUILTIN_SETTINGS.one_shot.total_timeout_s:g})"
74
+ ),
66
75
  ),
67
76
  ) -> None:
68
77
  """启动顶层 `mindcode` 交互或 one-shot 模式。"""
@@ -95,7 +104,9 @@ def shell(
95
104
  selected_total_timeout = None
96
105
  else:
97
106
  selected_total_timeout = (
98
- total_timeout if total_timeout is not None else 1800.0
107
+ total_timeout
108
+ if total_timeout is not None
109
+ else BUILTIN_SETTINGS.one_shot.total_timeout_s
99
110
  )
100
111
 
101
112
  try:
@@ -109,13 +120,34 @@ def shell(
109
120
  stream=not no_stream,
110
121
  subagents=subagents,
111
122
  one_shot_prompt=prompt,
112
- max_steps=max_steps if max_steps is not None else 50,
123
+ max_steps=(
124
+ max_steps
125
+ if max_steps is not None
126
+ else (
127
+ BUILTIN_SETTINGS.interactive.max_steps
128
+ if prompt is None
129
+ else BUILTIN_SETTINGS.one_shot.max_steps
130
+ )
131
+ ),
113
132
  step_timeout_s=(
114
133
  step_timeout
115
134
  if step_timeout is not None
116
- else (None if prompt is None else 300.0)
135
+ else (
136
+ BUILTIN_SETTINGS.interactive.step_timeout_s
137
+ if prompt is None
138
+ else BUILTIN_SETTINGS.one_shot.step_timeout_s
139
+ )
117
140
  ),
118
141
  total_timeout_s=selected_total_timeout,
142
+ settings_defaults=frozenset(
143
+ name
144
+ for name, value in (
145
+ ("max_steps", max_steps),
146
+ ("step_timeout_s", step_timeout),
147
+ ("total_timeout_s", total_timeout),
148
+ )
149
+ if value is None
150
+ ),
119
151
  )
120
152
  except ValueError as exc:
121
153
  console.print(f"[red]配置错误:[/red] {exc}")
@@ -22,7 +22,7 @@ from mindagent.core import (
22
22
  from mindagent.tools import MemoryReadTool
23
23
  from rich.table import Table
24
24
 
25
- from ...config import ConfigStore
25
+ from ...config import BUILTIN_SETTINGS, ConfigStore
26
26
  from ...policy import ApprovalMode
27
27
  from ...runtime import build_runtime
28
28
  from ...tasking import TaskEventRecorder, TaskStatus, TaskStore
@@ -44,16 +44,16 @@ def run_task(
44
44
  case_sensitive=False,
45
45
  help="审批模式",
46
46
  ),
47
- max_steps: int = typer.Option(50, "--max-steps", help="最大 step 数"),
47
+ max_steps: int | None = typer.Option(None, "--max-steps", help="最大 step 数"),
48
48
  step_timeout: float | None = typer.Option(
49
- 300.0,
49
+ None,
50
50
  "--step-timeout",
51
51
  help="单步超时秒数(默认 300,必须大于 0)",
52
52
  ),
53
53
  total_timeout: float | None = typer.Option(
54
- 1800.0,
54
+ None,
55
55
  "--total-timeout",
56
- help="总超时秒数",
56
+ help="总超时秒数,0 表示禁用",
57
57
  ),
58
58
  allow_dangerous: bool = typer.Option(
59
59
  False,
@@ -70,6 +70,12 @@ def run_task(
70
70
  if step_timeout is not None and step_timeout <= 0:
71
71
  console.print("[red]无效 step timeout:[/red] 必须大于 0")
72
72
  raise typer.Exit(2)
73
+ total_timeout_disabled = total_timeout == 0
74
+ if total_timeout is not None and total_timeout < 0:
75
+ console.print("[red]无效 total timeout:[/red] 必须为 0 或大于 0")
76
+ raise typer.Exit(2)
77
+ if total_timeout_disabled:
78
+ total_timeout = None
73
79
 
74
80
  ws = Path(workspace).expanduser().resolve()
75
81
  if not ws.is_dir():
@@ -87,6 +93,13 @@ def run_task(
87
93
  raise typer.Exit(2)
88
94
  model_name = provider_config.model
89
95
  selected_approve_mode = ApprovalMode(provider_config.approval_mode)
96
+ task_defaults = provider_config.runtime_settings.task
97
+ if max_steps is None:
98
+ max_steps = task_defaults.max_steps
99
+ if step_timeout is None:
100
+ step_timeout = task_defaults.step_timeout_s
101
+ if total_timeout is None and not total_timeout_disabled:
102
+ total_timeout = task_defaults.total_timeout_s
90
103
  store = TaskStore()
91
104
  record = store.create(
92
105
  goal=goal,
@@ -96,6 +109,7 @@ def run_task(
96
109
  approve_mode=selected_approve_mode.value,
97
110
  background=background,
98
111
  max_steps=max_steps,
112
+ step_timeout_s=step_timeout,
99
113
  total_timeout_s=total_timeout,
100
114
  allow_dangerous=allow_dangerous,
101
115
  )
@@ -163,7 +177,9 @@ def task_worker(task_id: str) -> None:
163
177
  model=record.model or "",
164
178
  approve_mode=ApprovalMode(record.approve_mode),
165
179
  max_steps=record.max_steps,
166
- step_timeout=getattr(record, "step_timeout_s", 300.0),
180
+ step_timeout=getattr(
181
+ record, "step_timeout_s", BUILTIN_SETTINGS.task.step_timeout_s
182
+ ),
167
183
  total_timeout=record.total_timeout_s,
168
184
  allow_dangerous=record.allow_dangerous,
169
185
  )
@@ -199,7 +215,9 @@ def resume_task(
199
215
  model=record.model or "",
200
216
  approve_mode=ApprovalMode(record.approve_mode),
201
217
  max_steps=record.max_steps,
202
- step_timeout=getattr(record, "step_timeout_s", 300.0),
218
+ step_timeout=getattr(
219
+ record, "step_timeout_s", BUILTIN_SETTINGS.task.step_timeout_s
220
+ ),
203
221
  total_timeout=record.total_timeout_s,
204
222
  allow_dangerous=record.allow_dangerous,
205
223
  resume=True,
@@ -225,7 +243,7 @@ async def _run_task(
225
243
  max_steps: int,
226
244
  total_timeout: float | None,
227
245
  allow_dangerous: bool,
228
- step_timeout: float | None = 300.0,
246
+ step_timeout: float | None = BUILTIN_SETTINGS.task.step_timeout_s,
229
247
  resume: bool = False,
230
248
  user_input: str | None = None,
231
249
  ) -> bool:
@@ -34,6 +34,7 @@ class InputPipeline:
34
34
  def __init__(self) -> None:
35
35
  self._seq = 0
36
36
  self._pending: UserInput | None = None
37
+ self._pending_items: list[str] = []
37
38
 
38
39
  def has_pending(self) -> bool:
39
40
  return self._pending is not None
@@ -49,6 +50,12 @@ class InputPipeline:
49
50
  """派生显示文本:合并文本单卡,不多卡不丢卡。"""
50
51
  return self.peek_text()
51
52
 
53
+ def display_items(self) -> list[str] | None:
54
+ """Independent pending texts for TUI cards, in FIFO order."""
55
+ if self._pending is None:
56
+ return None
57
+ return list(self._pending_items) or [self._pending.text]
58
+
52
59
  def display_status(self) -> str:
53
60
  """strip 状态:appending 显示发送中,其余显示等待中。"""
54
61
  if self._pending is not None and self._pending.exec == "appending":
@@ -59,9 +66,11 @@ class InputPipeline:
59
66
  """兼容旧测试的整包赋值;None 表示清空。"""
60
67
  if text is None:
61
68
  self._pending = None
69
+ self._pending_items = []
62
70
  return
63
71
  self._seq += 1
64
72
  self._pending = UserInput(id=self._seq, text=text, kind=kind)
73
+ self._pending_items = [text]
65
74
 
66
75
  def stage(self, text: str, kind: str = "normal") -> str:
67
76
  """忙时追问暂存:无 pending 则建卡,有则 ``\\n\\n`` 并入同一对象。"""
@@ -72,7 +81,9 @@ class InputPipeline:
72
81
  if self._pending is None:
73
82
  self._seq += 1
74
83
  self._pending = UserInput(id=self._seq, text=text, kind=kind)
84
+ self._pending_items = [text]
75
85
  elif text not in (None, ""):
86
+ self._pending_items.append(text)
76
87
  if self._pending.text:
77
88
  self._pending.text = self._pending.text + "\n\n" + text
78
89
  else:
@@ -95,6 +106,7 @@ class InputPipeline:
95
106
  self._pending.exec = "accepted"
96
107
  text = self._pending.text
97
108
  self._pending = None
109
+ self._pending_items = []
98
110
  return text
99
111
 
100
112
  def discard(self) -> str | None:
@@ -103,6 +115,7 @@ class InputPipeline:
103
115
  return None
104
116
  text = self._pending.text
105
117
  self._pending = None
118
+ self._pending_items = []
106
119
  return text
107
120
 
108
121