licos-agent-runtime 0.3.2__tar.gz → 0.3.4__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 (79) hide show
  1. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/PKG-INFO +1 -1
  2. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/pyproject.toml +1 -1
  3. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/app.py +0 -25
  4. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/harness_config.py +0 -5
  5. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/harness_verification.py +3 -0
  6. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/service.py +2 -16
  7. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/session_memory.py +7 -43
  8. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/verify.py +4 -1
  9. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_harness.py +9 -102
  10. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_runtime_config.py +7 -0
  11. licos_agent_runtime-0.3.2/src/licos_agent_runtime/project_history.py +0 -69
  12. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/.gitignore +0 -0
  13. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/README.md +0 -0
  14. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/__init__.py +0 -0
  15. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/__main__.py +0 -0
  16. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/agent_canvas.py +0 -0
  17. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/agent_config.py +0 -0
  18. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/agent_graph.py +0 -0
  19. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/agent_input.py +0 -0
  20. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/attachments.py +0 -0
  21. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/context.py +0 -0
  22. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/credit_availability.py +0 -0
  23. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/errors.py +0 -0
  24. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/event_store.py +0 -0
  25. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/event_subscriptions.py +0 -0
  26. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/graph_contract.py +0 -0
  27. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/graph_loader.py +0 -0
  28. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/harness_approval.py +0 -0
  29. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/harness_execution.py +0 -0
  30. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/harness_planning.py +0 -0
  31. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/logging.py +0 -0
  32. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/model_identity.py +0 -0
  33. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/model_usage.py +0 -0
  34. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/observability.py +0 -0
  35. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/openai.py +0 -0
  36. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/output_files.py +0 -0
  37. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/parser.py +0 -0
  38. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/payload.py +0 -0
  39. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/project_agent.py +0 -0
  40. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/project_files.py +0 -0
  41. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/project_skills.py +0 -0
  42. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/run_config.py +0 -0
  43. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/runtime_config.py +0 -0
  44. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/session_files.py +0 -0
  45. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/stream_protocol.py +0 -0
  46. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/stream_runner.py +0 -0
  47. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/telemetry.py +0 -0
  48. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/trace_records.py +0 -0
  49. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/workflow_canvas.py +0 -0
  50. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/src/licos_agent_runtime/workflow_protocol.py +0 -0
  51. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_agent_config.py +0 -0
  52. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_agent_graph.py +0 -0
  53. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_app_cors.py +0 -0
  54. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_attachment_inputs.py +0 -0
  55. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_context.py +0 -0
  56. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_credit_availability.py +0 -0
  57. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_credit_availability_routes.py +0 -0
  58. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_deployment_layout.py +0 -0
  59. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_event_delivery_api.py +0 -0
  60. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_event_subscriptions.py +0 -0
  61. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_execution_errors.py +0 -0
  62. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_graph_contract.py +0 -0
  63. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_graph_loader.py +0 -0
  64. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_model_identity.py +0 -0
  65. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_model_usage.py +0 -0
  66. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_observability.py +0 -0
  67. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_openai.py +0 -0
  68. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_parser.py +0 -0
  69. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_payload.py +0 -0
  70. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_project_files.py +0 -0
  71. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_project_skills.py +0 -0
  72. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_run_config.py +0 -0
  73. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_service_stream_protocol.py +0 -0
  74. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_static_assets.py +0 -0
  75. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_stream_protocol.py +0 -0
  76. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_stream_runner.py +0 -0
  77. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_trace_records.py +0 -0
  78. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_verify.py +0 -0
  79. {licos_agent_runtime-0.3.2 → licos_agent_runtime-0.3.4}/tests/test_workflow_protocol.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: licos-agent-runtime
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: LICOS Python agent runtime for LangGraph/LangChain projects
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: fastapi<1,>=0.121
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "licos-agent-runtime"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "LICOS Python agent runtime for LangGraph/LangChain projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -469,31 +469,6 @@ def create_app(service: GraphService | None = None) -> FastAPI:
469
469
  del request
470
470
  return runtime_service.agent_config_info()
471
471
 
472
- @app.get("/agent/history/{session_id}")
473
- async def http_agent_history(
474
- session_id: str,
475
- request: Request,
476
- limit: int = Query(20, ge=1, le=100),
477
- offset: int = Query(0, ge=0),
478
- ) -> dict[str, Any]:
479
- ctx = new_context(method="agent_history", headers=request.headers)
480
- try:
481
- return await runtime_service.conversation_history(session_id, ctx, limit, offset)
482
- except LookupError as exc:
483
- raise HTTPException(status_code=404, detail=str(exc)) from exc
484
- except ValueError as exc:
485
- raise HTTPException(status_code=401, detail=str(exc)) from exc
486
-
487
- @app.delete("/agent/history/{session_id}")
488
- async def http_agent_history_delete(session_id: str, request: Request) -> dict[str, Any]:
489
- ctx = new_context(method="agent_history_delete", headers=request.headers)
490
- try:
491
- return await runtime_service.delete_conversation_history(session_id, ctx)
492
- except LookupError as exc:
493
- raise HTTPException(status_code=404, detail=str(exc)) from exc
494
- except ValueError as exc:
495
- raise HTTPException(status_code=401, detail=str(exc)) from exc
496
-
497
472
  @app.get("/run_records")
498
473
  async def http_run_records(
499
474
  request: Request,
@@ -25,10 +25,6 @@ class MemorySettings(_StrictModel):
25
25
  idle_ttl_seconds: int = Field(default=3600, ge=1)
26
26
 
27
27
 
28
- class HistorySettings(_StrictModel):
29
- enabled: bool = False
30
-
31
-
32
28
  class PlanningSettings(_StrictModel):
33
29
  mode: Literal["off", "auto", "always"] = "auto"
34
30
 
@@ -81,7 +77,6 @@ class ThinkingSettings(_StrictModel):
81
77
  class HarnessSettings(_StrictModel):
82
78
  version: Literal[1]
83
79
  memory: MemorySettings = Field(default_factory=MemorySettings)
84
- history: HistorySettings = Field(default_factory=HistorySettings)
85
80
  planning: PlanningSettings = Field(default_factory=PlanningSettings)
86
81
  context: ContextSettings = Field(default_factory=ContextSettings)
87
82
  execution: ExecutionSettings = Field(default_factory=ExecutionSettings)
@@ -18,11 +18,14 @@ _LINK = re.compile(r"\[([^\]]+)\]\((https?://[^\s)]+)\)")
18
18
  _FILE_LINK_LABEL = re.compile(r"下载|文件|附件|文档|报告|download|file|document|report", re.I)
19
19
  _DELIVERY_CLAIM = re.compile(r"已上传|上传成功|可下载|下载链接\s*[::]")
20
20
  _NEGATION = re.compile(r"未|没有|无法|不能|不会|尚无|尚未|无可用|不提供")
21
+ _CONDITIONAL = re.compile(r"若|如果|假如|假设|如需")
21
22
 
22
23
 
23
24
  def _claims_file_delivery(answer: str) -> bool:
24
25
  for clause in re.split(r"[,。;;\n]", answer):
25
26
  for claim in _DELIVERY_CLAIM.finditer(clause):
27
+ if _CONDITIONAL.search(clause[:claim.start()]):
28
+ continue
26
29
  if not _NEGATION.search(clause[max(0, claim.start() - 12):claim.start()]):
27
30
  return True
28
31
  return False
@@ -300,7 +300,7 @@ class GraphService:
300
300
  result = await asyncio.wait_for(
301
301
  invocation, timeout=snapshot.harness.execution.max_duration_seconds
302
302
  ) if snapshot and snapshot.harness else await invocation
303
- await turn.commit(extract_answer(result))
303
+ turn.commit(extract_answer(result))
304
304
  else:
305
305
  payload = materialize_input_files(payload)
306
306
  recorder.set_root_input(payload)
@@ -511,7 +511,7 @@ class GraphService:
511
511
  else:
512
512
  await consume()
513
513
  if answers:
514
- await turn.commit("".join(answers) if run_opt.stream_mode == "messages" else answers[-1])
514
+ turn.commit("".join(answers) if run_opt.stream_mode == "messages" else answers[-1])
515
515
  except BaseException as exc:
516
516
  await queue.put(exc)
517
517
  finally:
@@ -906,20 +906,6 @@ class GraphService:
906
906
  **read_agent_config(),
907
907
  }
908
908
 
909
- async def conversation_history(
910
- self, session_id: str, ctx: Context, limit: int, offset: int
911
- ) -> dict[str, Any]:
912
- snapshot = self._agent_snapshot(ctx)
913
- if snapshot is None or snapshot.harness is None or not snapshot.harness.history.enabled:
914
- raise LookupError("Project database conversation history is disabled")
915
- return await self._session_memory.history_page(ctx, snapshot.harness, session_id, limit, offset)
916
-
917
- async def delete_conversation_history(self, session_id: str, ctx: Context) -> dict[str, Any]:
918
- snapshot = self._agent_snapshot(ctx)
919
- if snapshot is None or snapshot.harness is None or not snapshot.harness.history.enabled:
920
- raise LookupError("Project database conversation history is disabled")
921
- return await self._session_memory.history_delete(ctx, snapshot.harness, session_id)
922
-
923
909
  def agent_canvas(self) -> dict[str, Any]:
924
910
  if is_agent_project():
925
911
  return agent_canvas_response()
@@ -12,7 +12,6 @@ from typing import Any, AsyncIterator
12
12
 
13
13
  from .context import Context
14
14
  from .harness_config import HarnessSettings
15
- from .project_history import ProjectHistory
16
15
  from .session_files import session_user_id
17
16
  from .stream_protocol import extract_prompt_text
18
17
 
@@ -35,17 +34,10 @@ class MemoryTurn:
35
34
  user_text: str | None = None
36
35
  _session: _Session | None = None
37
36
  _window: int = 0
38
- _history: ProjectHistory | None = None
39
- _user_id: str = ""
40
- _session_id: str = ""
41
- _run_id: str = ""
42
37
 
43
- async def commit(self, answer: str) -> None:
38
+ def commit(self, answer: str) -> None:
44
39
  if self._session is None or not self.user_text or not answer.strip():
45
40
  return
46
- if self._history is not None:
47
- await self._history.save(self._user_id, self._session_id, self._run_id, self.user_text, answer)
48
- return
49
41
  self._session.turns.append((self.user_text, answer))
50
42
  while len(self._session.turns) > self._window:
51
43
  self._session.turns.popleft()
@@ -56,13 +48,13 @@ class SessionMemory:
56
48
  def __init__(self) -> None:
57
49
  self._sessions: OrderedDict[SessionKey, _Session] = OrderedDict()
58
50
  self._generation = 0
59
- self._history = ProjectHistory()
60
51
 
61
52
  @staticmethod
62
53
  def _eligible(request: dict[str, Any], settings: HarnessSettings | None) -> bool:
63
54
  return bool(
64
55
  settings
65
- and ((settings.memory.enabled and settings.memory.recent_turns) or settings.history.enabled)
56
+ and settings.memory.enabled
57
+ and settings.memory.recent_turns
66
58
  and isinstance(request.get("session_id"), str)
67
59
  and request["session_id"].strip()
68
60
  and not request.get("messages")
@@ -115,21 +107,11 @@ class SessionMemory:
115
107
  if session.generation != self._generation or now - session.touched > settings.memory.idle_ttl_seconds:
116
108
  session.turns.clear()
117
109
  session.generation = self._generation
118
- if settings.history.enabled:
119
- user_id = session_user_id(ctx)
120
- session.turns.clear()
121
- turns = (
122
- await self._history.recent(user_id, session_id, settings.memory.recent_turns)
123
- if settings.memory.enabled and settings.memory.recent_turns else []
124
- )
125
- else:
126
- user_id = ""
127
- while len(session.turns) > settings.memory.recent_turns:
128
- session.turns.popleft()
129
- turns = session.turns if settings.memory.enabled else []
110
+ while len(session.turns) > settings.memory.recent_turns:
111
+ session.turns.popleft()
130
112
  session.touched = now
131
113
  history: list[dict[str, str]] = []
132
- for user, assistant in turns:
114
+ for user, assistant in session.turns:
133
115
  history.extend(({"role": "user", "content": user}, {"role": "assistant", "content": assistant}))
134
116
  current = payload.get("messages")
135
117
  prepared = dict(payload)
@@ -142,25 +124,7 @@ class SessionMemory:
142
124
  if text is None:
143
125
  value = payload.get("input")
144
126
  text = value if isinstance(value, str) else None
145
- yield MemoryTurn(
146
- prepared, text, session, settings.memory.recent_turns,
147
- self._history if settings.history.enabled else None,
148
- user_id, session_id, ctx.run_id,
149
- )
150
-
151
- async def history_page(
152
- self, ctx: Context, settings: HarnessSettings, session_id: str, limit: int, offset: int
153
- ) -> dict[str, Any]:
154
- if not settings.history.enabled:
155
- raise ValueError("Project database conversation history is disabled")
156
- return await self._history.page(session_user_id(ctx), session_id, limit, offset)
157
-
158
- async def history_delete(
159
- self, ctx: Context, settings: HarnessSettings, session_id: str
160
- ) -> dict[str, Any]:
161
- if not settings.history.enabled:
162
- raise ValueError("Project database conversation history is disabled")
163
- return await self._history.delete(session_user_id(ctx), session_id)
127
+ yield MemoryTurn(prepared, text, session, settings.memory.recent_turns)
164
128
 
165
129
  def clear(self) -> None:
166
130
  self._generation += 1
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import os
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
  from uuid import uuid4
@@ -116,7 +117,9 @@ def verify_project(*, cases_path: str, base_url: str, output_path: str, request_
116
117
  report["request_timeout_seconds"] = timeout
117
118
  report["project"] = validate_project()
118
119
  cases = json.loads(Path(cases_path).read_text(encoding="utf-8"))
119
- with httpx.Client(base_url=base_url.rstrip("/") + "/", timeout=timeout, trust_env=False) as client:
120
+ token = os.environ.get("LICOS_AGENT_VERIFY_BEARER_TOKEN")
121
+ headers = {"Authorization": f"Bearer {token}"} if token else {}
122
+ with httpx.Client(base_url=base_url.rstrip("/") + "/", timeout=timeout, headers=headers, trust_env=False) as client:
120
123
  report["cases"] = verify_cases(client, cases)
121
124
  report["status"] = "passed"
122
125
  return report
@@ -230,7 +230,7 @@ class HarnessTests(unittest.TestCase):
230
230
  {"input": "first"}, ctx, settings, root) as turn:
231
231
  started.set()
232
232
  await release.wait()
233
- await turn.commit("answer-first")
233
+ turn.commit("answer-first")
234
234
 
235
235
  async def second():
236
236
  async with memory.use({"session_id": "same", "input": "second"},
@@ -348,107 +348,6 @@ class HarnessTests(unittest.TestCase):
348
348
 
349
349
  asyncio.run(run())
350
350
 
351
- def test_project_database_history_persists_across_runtime_restart_and_isolates_users(self):
352
- rows = []
353
-
354
- def query(_table, *, filters, limit, offset=0, **_kwargs):
355
- scoped = [row for row in rows if all(row[item["field"]] == item["value"] for item in filters)]
356
- scoped.reverse()
357
- return {"data": scoped[offset:offset + limit], "count": len(scoped)}
358
-
359
- def insert(_table, *, row, returning):
360
- self.assertFalse(returning)
361
- rows.append({**row, "created_at": len(rows)})
362
- return {"affectedRows": 1}
363
-
364
- def delete(_table, *, filters, returning):
365
- self.assertFalse(returning)
366
- before = len(rows)
367
- rows[:] = [row for row in rows if not all(row[item["field"]] == item["value"] for item in filters)]
368
- return {"affectedRows": before - len(rows)}
369
-
370
- async def run():
371
- with tempfile.TemporaryDirectory(dir=".tmp") as directory:
372
- root = Path(directory)
373
- (root / "config").mkdir()
374
- (root / "config/agent_llm_config.json").write_text(json.dumps({
375
- "harness": {"version": 1, "planning": {"mode": "off"},
376
- "history": {"enabled": True}},
377
- }), encoding="utf-8")
378
- model = HarnessModel()
379
- graph = build_agent_graph(model, system_prompt="test")
380
- service = GraphService()
381
-
382
- async def send(text, user="user-1", session="same"):
383
- ctx = new_context("run")
384
- ctx.identity.current_user_id = user
385
- await service.run({"input": text, "session_id": session}, ctx)
386
-
387
- with (
388
- patch.dict(os.environ, {"LICOS_PROJECT_PATH": str(root), "LICOS_WORKSPACE_PATH": str(root)}),
389
- patch("licos_agent_runtime.service.is_agent_project", return_value=True),
390
- patch("licos_agent_runtime.project_history.database.query", side_effect=query),
391
- patch("licos_agent_runtime.project_history.database.insert", side_effect=insert),
392
- patch("licos_agent_runtime.project_history.database.delete", side_effect=delete),
393
- patch.object(GraphService, "_get_graph", return_value=graph),
394
- ):
395
- for number in range(1, 5):
396
- await send(f"turn-{number}")
397
- self.assertEqual(len(rows), 4)
398
- service = GraphService()
399
- await send("turn-5")
400
- self.assertEqual(
401
- [message.content for message in model.seen[-1] if isinstance(message, HumanMessage)],
402
- ["turn-2", "turn-3", "turn-4", "turn-5"],
403
- )
404
- await send("another-user", user="user-2")
405
- self.assertEqual(
406
- [message.content for message in model.seen[-1] if isinstance(message, HumanMessage)],
407
- ["another-user"],
408
- )
409
- await send("another-session", session="other")
410
- self.assertEqual(
411
- [message.content for message in model.seen[-1] if isinstance(message, HumanMessage)],
412
- ["another-session"],
413
- )
414
- ctx = new_context("history")
415
- ctx.identity.current_user_id = "user-1"
416
- page = await service.conversation_history("same", ctx, 2, 1)
417
- self.assertEqual(page["count"], 5)
418
- self.assertEqual([row["user_text"] for row in page["data"]], ["turn-4", "turn-3"])
419
- ctx.identity.current_user_id = "user-2"
420
- page = await service.conversation_history("same", ctx, 20, 0)
421
- self.assertEqual([row["user_text"] for row in page["data"]], ["another-user"])
422
- self.assertEqual((await service.delete_conversation_history("same", ctx))["affectedRows"], 1)
423
- ctx.identity.current_user_id = "user-1"
424
- self.assertEqual((await service.conversation_history("same", ctx, 20, 0))["count"], 5)
425
-
426
- asyncio.run(run())
427
-
428
- def test_enabled_project_database_history_write_failure_fails_the_run(self):
429
- async def run():
430
- with tempfile.TemporaryDirectory(dir=".tmp") as directory:
431
- root = Path(directory)
432
- (root / "config").mkdir()
433
- (root / "config/agent_llm_config.json").write_text(json.dumps({
434
- "harness": {"version": 1, "planning": {"mode": "off"},
435
- "history": {"enabled": True}},
436
- }), encoding="utf-8")
437
- service = GraphService()
438
- ctx = new_context("run")
439
- ctx.identity.current_user_id = "user-1"
440
- with (
441
- patch.dict(os.environ, {"LICOS_PROJECT_PATH": str(root), "LICOS_WORKSPACE_PATH": str(root)}),
442
- patch("licos_agent_runtime.service.is_agent_project", return_value=True),
443
- patch("licos_agent_runtime.project_history.database.query", return_value={"data": []}),
444
- patch("licos_agent_runtime.project_history.database.insert", side_effect=RuntimeError("write failed")),
445
- patch.object(service, "_get_graph", return_value=build_agent_graph(HarnessModel(), system_prompt="test")),
446
- ):
447
- with self.assertRaisesRegex(RuntimeError, "write failed"):
448
- await service.run({"input": "hello", "session_id": "same"}, ctx)
449
-
450
- asyncio.run(run())
451
-
452
351
  def test_stream_commits_only_finished_response(self):
453
352
  async def run():
454
353
  with tempfile.TemporaryDirectory(dir=".tmp") as directory:
@@ -625,6 +524,14 @@ class HarnessTests(unittest.TestCase):
625
524
  ToolMessage("Wrote 18 bytes", name="write_file", tool_call_id="w1"),
626
525
  AIMessage("文件已写入并核对;没有生成下载链接,也未上传。"),
627
526
  ]))
527
+ self.assertIsNone(verify_tool_evidence([
528
+ HumanMessage("explain message input"),
529
+ AIMessage("或作为附件上传(若已上传,可继续处理)。如果确实需要一份可下载的文件,我会先上传。"),
530
+ ]))
531
+ self.assertIn("verified upload URL", verify_tool_evidence([
532
+ HumanMessage("deliver a file"),
533
+ AIMessage("文件已上传,可以下载。"),
534
+ ]))
628
535
  self.assertIn("lack a successful upload", verify_tool_evidence([
629
536
  HumanMessage("deliver a report"),
630
537
  ToolMessage("Wrote 18 bytes", name="write_file", tool_call_id="w1"),
@@ -44,3 +44,10 @@ class RuntimeConfigTests(unittest.TestCase):
44
44
  report = json.loads(target.read_text(encoding="utf-8"))
45
45
  self.assertEqual(report["status"], "failed")
46
46
  self.assertIn("positive finite", report["error"])
47
+
48
+ def test_verification_sends_authenticated_user_token(self):
49
+ with tempfile.TemporaryDirectory() as root, patch.dict(os.environ, {"LICOS_AGENT_VERIFY_BEARER_TOKEN": "dev-user-token"}), patch("licos_agent_runtime.verify.validate_project", return_value={}), patch("licos_agent_runtime.verify.verify_cases", return_value=[]), patch("licos_agent_runtime.verify.httpx.Client") as client:
50
+ cases = Path(root) / "cases.json"
51
+ cases.write_text("[]", encoding="utf-8")
52
+ verify_project(cases_path=str(cases), base_url="http://localhost:5000", output_path=str(Path(root) / "report.json"))
53
+ self.assertEqual(client.call_args.kwargs["headers"], {"Authorization": "Bearer dev-user-token"})
@@ -1,69 +0,0 @@
1
- """Opt-in completed-turn history stored in the current project database."""
2
- from __future__ import annotations
3
-
4
- import asyncio
5
- from typing import Any
6
-
7
- from licos_platform_sdk import database
8
-
9
-
10
- TABLE = "agent_conversation_turns"
11
-
12
-
13
- def _scope(user_id: str, session_id: str) -> list[dict[str, str]]:
14
- return [
15
- {"field": "user_id", "op": "eq", "value": user_id},
16
- {"field": "session_id", "op": "eq", "value": session_id},
17
- ]
18
-
19
-
20
- class ProjectHistory:
21
- async def recent(self, user_id: str, session_id: str, limit: int) -> list[tuple[str, str]]:
22
- result = await asyncio.to_thread(
23
- database.query,
24
- TABLE,
25
- select=["user_text", "assistant_text"],
26
- filters=_scope(user_id, session_id),
27
- order_by=[
28
- {"field": "created_at", "direction": "desc"},
29
- {"field": "id", "direction": "desc"},
30
- ],
31
- limit=limit,
32
- )
33
- return [(row["user_text"], row["assistant_text"]) for row in reversed(result["data"])]
34
-
35
- async def save(
36
- self, user_id: str, session_id: str, run_id: str, user_text: str, assistant_text: str
37
- ) -> None:
38
- await asyncio.to_thread(
39
- database.insert,
40
- TABLE,
41
- row={
42
- "id": run_id,
43
- "user_id": user_id,
44
- "session_id": session_id,
45
- "user_text": user_text,
46
- "assistant_text": assistant_text,
47
- },
48
- returning=False,
49
- )
50
-
51
- async def page(self, user_id: str, session_id: str, limit: int, offset: int) -> dict[str, Any]:
52
- return await asyncio.to_thread(
53
- database.query,
54
- TABLE,
55
- select=["id", "user_text", "assistant_text", "created_at"],
56
- filters=_scope(user_id, session_id),
57
- order_by=[
58
- {"field": "created_at", "direction": "desc"},
59
- {"field": "id", "direction": "desc"},
60
- ],
61
- limit=limit,
62
- offset=offset,
63
- count=True,
64
- )
65
-
66
- async def delete(self, user_id: str, session_id: str) -> dict[str, Any]:
67
- return await asyncio.to_thread(
68
- database.delete, TABLE, filters=_scope(user_id, session_id), returning=False
69
- )