taskflow-agent 0.3.0__tar.gz → 0.4.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: taskflow-agent
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Lightweight project and task manager with MCP tools for Claude Code
5
5
  Project-URL: Repository, https://github.com/henrysouchien/taskflow-agent
6
6
  Author: Henry Chien
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "taskflow-agent"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Lightweight project and task manager with MCP tools for Claude Code"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1241,7 +1241,6 @@ MUTATING_TOOL_NAMES = {
1241
1241
  _SUB_AGENT_EXCLUDED_TOOLS: set[str] = {
1242
1242
  "run_agent",
1243
1243
  "run_shell",
1244
- "load_tools",
1245
1244
  "tf_memory_read",
1246
1245
  "tf_memory_update",
1247
1246
  } | MUTATING_TOOL_NAMES
@@ -1261,7 +1260,7 @@ _SUB_AGENT_MAX_TURNS = 15
1261
1260
  _SUB_AGENT_TIMEOUT = int(os.getenv("SUB_AGENT_TIMEOUT", "300"))
1262
1261
  _SUB_AGENT_CLIENT_TIMEOUT = 90
1263
1262
  _SUB_AGENT_MAX_TOKENS = 32_000
1264
- _SUB_AGENT_DEFAULT_MODEL = "claude-sonnet-4-6"
1263
+ _SUB_AGENT_DEFAULT_MODEL = "claude-haiku-4-5-20251001"
1265
1264
 
1266
1265
  mcp_manager = McpClientManager(
1267
1266
  allowed_servers=None,
@@ -1819,6 +1818,7 @@ def make_run_agent_handler(
1819
1818
  runner_ref: list[Any],
1820
1819
  local_tool_handlers: dict[str, Any],
1821
1820
  mcp_manager: McpClientManager,
1821
+ event_log: EventLog,
1822
1822
  ):
1823
1823
  async def _handle_run_agent(
1824
1824
  tool_input: dict,
@@ -1834,7 +1834,7 @@ def make_run_agent_handler(
1834
1834
  return None, {"code": "invalid_input", "message": "task is required"}
1835
1835
 
1836
1836
  raw_model = tool_input.get("model")
1837
- allowed = {"claude-sonnet-4-6", "claude-opus-4-6"}
1837
+ allowed = {"claude-haiku-4-5-20251001", "claude-sonnet-4-6", "claude-opus-4-6"}
1838
1838
  if raw_model is not None and raw_model not in allowed:
1839
1839
  return None, {"code": "invalid_input", "message": f"Invalid model: {raw_model}"}
1840
1840
 
@@ -1855,6 +1855,43 @@ def make_run_agent_handler(
1855
1855
  needs_approval=lambda _: False,
1856
1856
  )
1857
1857
 
1858
+ def on_sub_event(event: dict[str, Any], session_id: str) -> None:
1859
+ del session_id
1860
+ event_type = event.get("type")
1861
+ if event_type == "tool_call_start":
1862
+ event_log.append(
1863
+ {
1864
+ "type": "sub_agent_progress",
1865
+ "call_index": call_index,
1866
+ "sub_event": "tool_start",
1867
+ "tool_name": event.get("tool_name", ""),
1868
+ "sub_tool_call_id": event.get("tool_call_id", ""),
1869
+ }
1870
+ )
1871
+ return
1872
+ if event_type == "tool_call_complete":
1873
+ event_log.append(
1874
+ {
1875
+ "type": "sub_agent_progress",
1876
+ "call_index": call_index,
1877
+ "sub_event": "tool_done",
1878
+ "tool_name": event.get("tool_name", ""),
1879
+ "sub_tool_call_id": event.get("tool_call_id", ""),
1880
+ "duration_ms": event.get("duration_ms"),
1881
+ "error": bool(event.get("error")),
1882
+ }
1883
+ )
1884
+ return
1885
+ if event_type == "error":
1886
+ event_log.append(
1887
+ {
1888
+ "type": "sub_agent_progress",
1889
+ "call_index": call_index,
1890
+ "sub_event": "error",
1891
+ "error_message": str(event.get("error", "Sub-agent error")),
1892
+ }
1893
+ )
1894
+
1858
1895
  result, error = await runner.spawn_sub_agent(
1859
1896
  task,
1860
1897
  model=effective_model,
@@ -1866,6 +1903,7 @@ def make_run_agent_handler(
1866
1903
  client_timeout=_SUB_AGENT_CLIENT_TIMEOUT,
1867
1904
  max_tokens=_SUB_AGENT_MAX_TOKENS,
1868
1905
  call_index=call_index,
1906
+ on_sub_event=on_sub_event,
1869
1907
  )
1870
1908
  return result, error
1871
1909
 
@@ -2248,6 +2286,7 @@ async def sse_generator(
2248
2286
  runner_task: asyncio.Task[None],
2249
2287
  request_id: str,
2250
2288
  ):
2289
+ run_agent_map: dict[int, str] = {}
2251
2290
  tool_inputs: dict[str, dict[str, Any]] = {}
2252
2291
  tool_calls: dict[str, dict[str, Any]] = {}
2253
2292
  turn_tools: list[dict[str, Any]] = []
@@ -2292,6 +2331,10 @@ async def sse_generator(
2292
2331
  }
2293
2332
  tool_calls[tool_call_id] = tool_entry
2294
2333
  turn_tools.append(tool_entry)
2334
+ if tool_name == "run_agent":
2335
+ call_index = event.get("call_index")
2336
+ if call_index is not None:
2337
+ run_agent_map[call_index] = tool_call_id
2295
2338
  yield _sse(
2296
2339
  {
2297
2340
  "type": "tool_call_start",
@@ -2334,6 +2377,24 @@ async def sse_generator(
2334
2377
  pending_mutations.append(mutation)
2335
2378
  continue
2336
2379
 
2380
+ if event_type == "sub_agent_progress":
2381
+ call_index = event.get("call_index")
2382
+ parent_tool_call_id = run_agent_map.get(call_index) if call_index is not None else None
2383
+ if parent_tool_call_id:
2384
+ yield _sse(
2385
+ {
2386
+ "type": "sub_agent_progress",
2387
+ "parent_tool_call_id": parent_tool_call_id,
2388
+ "sub_event": event.get("sub_event"),
2389
+ "tool_name": event.get("tool_name", ""),
2390
+ "sub_tool_call_id": event.get("sub_tool_call_id", ""),
2391
+ "duration_ms": event.get("duration_ms"),
2392
+ "error": event.get("error"),
2393
+ "error_message": event.get("error_message"),
2394
+ }
2395
+ )
2396
+ continue
2397
+
2337
2398
  if event_type == "stream_complete":
2338
2399
  assistant_text = "".join(assistant_chunks).strip()
2339
2400
  full_content = assistant_text + _build_tool_summary(turn_tools)
@@ -2938,6 +2999,7 @@ async def chat(body: ChatRequest, request: Request):
2938
2999
  runner_ref,
2939
3000
  request_handlers,
2940
3001
  mcp_manager,
3002
+ event_log,
2941
3003
  )
2942
3004
  dispatcher = ToolDispatcher(
2943
3005
  mcp_client=mcp_manager,
@@ -1218,6 +1218,22 @@ button { cursor: pointer; }
1218
1218
  white-space: pre-wrap;
1219
1219
  word-break: break-word;
1220
1220
  }
1221
+ .chat-sub-progress {
1222
+ padding: 4px 12px 8px;
1223
+ font-family: var(--font-mono);
1224
+ font-size: 10px;
1225
+ line-height: 1.8;
1226
+ color: var(--text-muted);
1227
+ }
1228
+ .chat-sub-progress-line {
1229
+ display: flex;
1230
+ align-items: center;
1231
+ gap: 4px;
1232
+ }
1233
+ .chat-sub-progress-line .marker { color: var(--accent); }
1234
+ .chat-sub-progress-line .marker.done { color: var(--green); }
1235
+ .chat-sub-progress-line .marker.err { color: var(--red); }
1236
+ .chat-sub-progress-line .dur { opacity: 0.6; }
1221
1237
 
1222
1238
  .chat-compose {
1223
1239
  border-top: 1px solid var(--border-subtle);
@@ -2744,6 +2760,102 @@ function completeToolCard(toolCallId, toolName, result, error) {
2744
2760
  scrollChatToBottom()
2745
2761
  }
2746
2762
 
2763
+ function _ensureSubProgressContainer(card) {
2764
+ let container = card.querySelector('.chat-sub-progress')
2765
+ if (container) return container
2766
+ container = document.createElement('div')
2767
+ container.className = 'chat-sub-progress'
2768
+ const bodyEl = card.querySelector('.chat-tool-body')
2769
+ if (bodyEl) {
2770
+ card.insertBefore(container, bodyEl)
2771
+ } else {
2772
+ card.appendChild(container)
2773
+ }
2774
+ return container
2775
+ }
2776
+
2777
+ function _findSubProgressLine(container, subToolCallId) {
2778
+ if (!subToolCallId) return null
2779
+ return Array.from(container.querySelectorAll('.chat-sub-progress-line'))
2780
+ .find(line => line.dataset.subCallId === subToolCallId) || null
2781
+ }
2782
+
2783
+ function _formatSubProgressDuration(durationMs) {
2784
+ const ms = Number(durationMs)
2785
+ if (!Number.isFinite(ms) || ms < 0) return ''
2786
+ return `${(ms / 1000).toFixed(1)}s`
2787
+ }
2788
+
2789
+ function _renderSubProgressLine(line, markerText, markerClass, text, durationText = '') {
2790
+ line.textContent = ''
2791
+
2792
+ const markerEl = document.createElement('span')
2793
+ markerEl.className = markerClass
2794
+ markerEl.textContent = markerText
2795
+ line.appendChild(markerEl)
2796
+
2797
+ const textEl = document.createElement('span')
2798
+ textEl.textContent = text || 'tool'
2799
+ line.appendChild(textEl)
2800
+
2801
+ if (durationText) {
2802
+ const durationEl = document.createElement('span')
2803
+ durationEl.className = 'dur'
2804
+ durationEl.textContent = durationText
2805
+ line.appendChild(durationEl)
2806
+ }
2807
+ }
2808
+
2809
+ function updateSubAgentProgress(parentToolCallId, subEvent, toolName, subToolCallId, durationMs, error, errorMessage) {
2810
+ const card = S.chat.toolCards.get(parentToolCallId)
2811
+ if (!card) return
2812
+
2813
+ const container = _ensureSubProgressContainer(card)
2814
+ const label = toolName || 'tool'
2815
+
2816
+ if (subEvent === 'tool_start') {
2817
+ if (!subToolCallId) return
2818
+ let line = _findSubProgressLine(container, subToolCallId)
2819
+ if (!line) {
2820
+ line = document.createElement('div')
2821
+ line.className = 'chat-sub-progress-line'
2822
+ line.dataset.subCallId = subToolCallId
2823
+ container.appendChild(line)
2824
+ }
2825
+ _renderSubProgressLine(line, '▸', 'marker', label)
2826
+ scrollChatToBottom()
2827
+ return
2828
+ }
2829
+
2830
+ if (subEvent === 'tool_done') {
2831
+ if (!subToolCallId) return
2832
+ let line = _findSubProgressLine(container, subToolCallId)
2833
+ if (!line) {
2834
+ line = document.createElement('div')
2835
+ line.className = 'chat-sub-progress-line'
2836
+ line.dataset.subCallId = subToolCallId
2837
+ container.appendChild(line)
2838
+ }
2839
+ _renderSubProgressLine(
2840
+ line,
2841
+ error ? '✗' : '✓',
2842
+ error ? 'marker err' : 'marker done',
2843
+ label,
2844
+ _formatSubProgressDuration(durationMs),
2845
+ )
2846
+ scrollChatToBottom()
2847
+ return
2848
+ }
2849
+
2850
+ if (subEvent === 'error') {
2851
+ const line = document.createElement('div')
2852
+ line.className = 'chat-sub-progress-line'
2853
+ _renderSubProgressLine(line, '✗', 'marker err', errorMessage || 'Sub-agent error')
2854
+ container.appendChild(line)
2855
+ scrollChatToBottom()
2856
+ }
2857
+ }
2858
+
2747
2859
  function formatToolPayload(value) {
2748
2860
  if (value == null) return '{}'
2749
2861
  let text = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
@@ -3040,6 +3152,19 @@ async function handleChatEvent(event) {
3040
3152
  return false
3041
3153
  }
3042
3154
 
3155
+ if (event.type === 'sub_agent_progress') {
3156
+ updateSubAgentProgress(
3157
+ event.parent_tool_call_id,
3158
+ event.sub_event,
3159
+ event.tool_name,
3160
+ event.sub_tool_call_id,
3161
+ event.duration_ms,
3162
+ event.error,
3163
+ event.error_message,
3164
+ )
3165
+ return false
3166
+ }
3167
+
3043
3168
  if (event.type === 'taskflow_mutation') {
3044
3169
  await handleChatMutation(event)
3045
3170
  return false
File without changes
File without changes
File without changes
File without changes