forgexa-cli 1.56.4__tar.gz → 1.57.1__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.
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/PKG-INFO +1 -1
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli/__init__.py +1 -1
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli/agent_core.py +42 -1
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli/daemon.py +79 -7
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli/main.py +58 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli/zcode_protocol.py +21 -2
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli.egg-info/PKG-INFO +1 -1
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/pyproject.toml +1 -1
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/README.md +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli/_build_config.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli/_local_bind.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli/autoupgrade.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli/py.typed +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli/runtime_evidence.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli.egg-info/SOURCES.txt +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli.egg-info/dependency_links.txt +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli.egg-info/entry_points.txt +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli.egg-info/requires.txt +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/forgexa_cli.egg-info/top_level.txt +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/setup.cfg +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/tests/test_auth_and_runtime_commands.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/tests/test_autoupgrade.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/tests/test_check_command.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/tests/test_expiry_warnings_and_revoke.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/tests/test_local_bind_commands.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/tests/test_runtime_credentials.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/tests/test_session_credentials.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/tests/test_silent_install.py +0 -0
- {forgexa_cli-1.56.4 → forgexa_cli-1.57.1}/tests/test_upgrade_observability.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""forgexa-cli — Forgexa command-line client."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.57.1"
|
|
@@ -2386,6 +2386,39 @@ _UNVERIFIED_AGENT_LIVE_GATE_REASONS = {
|
|
|
2386
2386
|
),
|
|
2387
2387
|
}
|
|
2388
2388
|
|
|
2389
|
+
AGENT_LIVE_VERIFIED_FILE = Path.home() / ".forgexa" / "agent-live-verified"
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
def agent_live_gate_approved(agent_id: str, env: dict[str, str] | None = None) -> bool:
|
|
2393
|
+
"""Whether an operator recorded a live-acceptance pass for this agent.
|
|
2394
|
+
|
|
2395
|
+
Fail-closed by default: the gate only opens through an explicit
|
|
2396
|
+
``forgexa agent approve-live <id>`` (persistent file, readable by
|
|
2397
|
+
GUI-launched runtimes) or the FORGEXA_AGENT_LIVE_VERIFIED env var —
|
|
2398
|
+
both mean a human actually ran the live start/resume/cancel/isolation
|
|
2399
|
+
acceptance against a real account/build and accepted the result.
|
|
2400
|
+
Agents without a live gate entry are NOT approved here: their gate is
|
|
2401
|
+
the class-level ``integration_verified`` flag, and returning True
|
|
2402
|
+
would silently disarm it for any driver marked unverified.
|
|
2403
|
+
"""
|
|
2404
|
+
if agent_id.lower() not in _UNVERIFIED_AGENT_LIVE_GATE_REASONS:
|
|
2405
|
+
return False
|
|
2406
|
+
source_env = os.environ if env is None else env
|
|
2407
|
+
verified = {
|
|
2408
|
+
value.strip().lower()
|
|
2409
|
+
for value in source_env.get("FORGEXA_AGENT_LIVE_VERIFIED", "").split(",")
|
|
2410
|
+
if value.strip()
|
|
2411
|
+
}
|
|
2412
|
+
try:
|
|
2413
|
+
lines = AGENT_LIVE_VERIFIED_FILE.read_text(encoding="utf-8").splitlines()
|
|
2414
|
+
except OSError:
|
|
2415
|
+
lines = []
|
|
2416
|
+
for line in lines:
|
|
2417
|
+
value = line.split("#", 1)[0].strip()
|
|
2418
|
+
if value:
|
|
2419
|
+
verified.add(value.lower())
|
|
2420
|
+
return agent_id.lower() in verified
|
|
2421
|
+
|
|
2389
2422
|
|
|
2390
2423
|
def agent_execution_verification_issue(agent_id: str, binary: str) -> str | None:
|
|
2391
2424
|
"""Return a fail-closed build/live-acceptance gate for local agents."""
|
|
@@ -2393,7 +2426,15 @@ def agent_execution_verification_issue(agent_id: str, binary: str) -> str | None
|
|
|
2393
2426
|
build_issue = zcode_build_verification_issue(binary)
|
|
2394
2427
|
if build_issue:
|
|
2395
2428
|
return build_issue
|
|
2396
|
-
|
|
2429
|
+
if agent_live_gate_approved(agent_id):
|
|
2430
|
+
return None
|
|
2431
|
+
reason = _UNVERIFIED_AGENT_LIVE_GATE_REASONS.get(agent_id)
|
|
2432
|
+
if reason:
|
|
2433
|
+
return (
|
|
2434
|
+
f"{reason}; run `forgexa agent approve-live {agent_id}` after "
|
|
2435
|
+
"completing the acceptance checklist"
|
|
2436
|
+
)
|
|
2437
|
+
return None
|
|
2397
2438
|
|
|
2398
2439
|
|
|
2399
2440
|
_FORGEXA_CODING_AGENT_TOOLS = ("Read", "Glob", "Grep", "Edit", "Write", "Bash")
|
|
@@ -1150,7 +1150,7 @@ except (ImportError, ModuleNotFoundError):
|
|
|
1150
1150
|
# DAEMON_VERSION is the protocol/logic version of the daemon code.
|
|
1151
1151
|
# Kept in sync with pyproject.toml version via bump-version.sh.
|
|
1152
1152
|
# CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
|
|
1153
|
-
DAEMON_VERSION = "1.
|
|
1153
|
+
DAEMON_VERSION = "1.57.1"
|
|
1154
1154
|
|
|
1155
1155
|
|
|
1156
1156
|
def _detect_client_type() -> str:
|
|
@@ -1215,6 +1215,38 @@ logging.basicConfig(
|
|
|
1215
1215
|
# Routine poll/heartbeat requests otherwise consume the entire 500-line remote
|
|
1216
1216
|
# log tail in about 20 minutes, hiding the task failure operators need to see.
|
|
1217
1217
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
1218
|
+
|
|
1219
|
+
|
|
1220
|
+
def _pid_looks_like_forgexa_daemon(pid: int) -> bool:
|
|
1221
|
+
"""Best-effort identity check before signalling a lock-file PID.
|
|
1222
|
+
|
|
1223
|
+
Reads the process cmdline (Linux /proc, macOS `ps`) and accepts the PID
|
|
1224
|
+
only when it mentions this daemon. Unknown/unreadable identity returns
|
|
1225
|
+
False — the conservative direction is leaving the stale lock to the
|
|
1226
|
+
operator, never killing an unrelated reused PID (audit AGT-05).
|
|
1227
|
+
"""
|
|
1228
|
+
if pid <= 0:
|
|
1229
|
+
return False
|
|
1230
|
+
if pid == os.getpid():
|
|
1231
|
+
return True
|
|
1232
|
+
try:
|
|
1233
|
+
cmdline = ""
|
|
1234
|
+
try:
|
|
1235
|
+
with open(f"/proc/{pid}/cmdline", "rb") as fh:
|
|
1236
|
+
cmdline = fh.read().replace(b"\0", b" ").decode("utf-8", "replace")
|
|
1237
|
+
except OSError:
|
|
1238
|
+
import subprocess as _sp
|
|
1239
|
+
|
|
1240
|
+
result = _sp.run(
|
|
1241
|
+
["ps", "-p", str(pid), "-o", "command="],
|
|
1242
|
+
capture_output=True, text=True, timeout=5,
|
|
1243
|
+
)
|
|
1244
|
+
cmdline = result.stdout or ""
|
|
1245
|
+
lowered = cmdline.lower()
|
|
1246
|
+
return "forgexa" in lowered or "daemon.py" in lowered
|
|
1247
|
+
except Exception:
|
|
1248
|
+
return False
|
|
1249
|
+
|
|
1218
1250
|
logger = logging.getLogger("daemon")
|
|
1219
1251
|
logger.info("Daemon log: %s", DAEMON_LOG_PATH)
|
|
1220
1252
|
|
|
@@ -6894,7 +6926,7 @@ class ProcessManager:
|
|
|
6894
6926
|
task_id,
|
|
6895
6927
|
stdin_input=agent_core.qoder_stdin_prompt(prompt),
|
|
6896
6928
|
on_chunk=on_chunk,
|
|
6897
|
-
env=agent_core.qoder_base_env
|
|
6929
|
+
env=await asyncio.to_thread(agent_core.qoder_base_env, os.environ.copy()),
|
|
6898
6930
|
)
|
|
6899
6931
|
finally:
|
|
6900
6932
|
agent_core.release_file_lock(profile_lock)
|
|
@@ -6959,7 +6991,7 @@ class ProcessManager:
|
|
|
6959
6991
|
task_id,
|
|
6960
6992
|
stdin_input=prompt,
|
|
6961
6993
|
on_chunk=on_chunk,
|
|
6962
|
-
env=agent_core.trae_code_base_env
|
|
6994
|
+
env=await asyncio.to_thread(agent_core.trae_code_base_env, os.environ.copy()),
|
|
6963
6995
|
)
|
|
6964
6996
|
finally:
|
|
6965
6997
|
agent_core.release_file_lock(profile_lock)
|
|
@@ -7020,6 +7052,9 @@ class ProcessManager:
|
|
|
7020
7052
|
if on_chunk is None or event.get("type") not in {"model.streaming", "tool.updated"}:
|
|
7021
7053
|
return
|
|
7022
7054
|
payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
|
|
7055
|
+
# AGT-11 (audit): zcode 0.16.x never emits "model.streaming" —
|
|
7056
|
+
# final text is read back from session/messages after the turn
|
|
7057
|
+
# terminates. This branch is forward-compatibility only.
|
|
7023
7058
|
if event.get("type") == "model.streaming":
|
|
7024
7059
|
text = payload.get("text") or payload.get("delta") or payload.get("content")
|
|
7025
7060
|
line = text.strip() if isinstance(text, str) else ""
|
|
@@ -7037,7 +7072,7 @@ class ProcessManager:
|
|
|
7037
7072
|
client = ZCodeProtocolSession(
|
|
7038
7073
|
[agent.command, "app-server"],
|
|
7039
7074
|
cwd=cwd,
|
|
7040
|
-
env=agent_core.zcode_base_env
|
|
7075
|
+
env=await asyncio.to_thread(agent_core.zcode_base_env, os.environ.copy()),
|
|
7041
7076
|
on_event=on_event,
|
|
7042
7077
|
permission_handler=agent_core.zcode_permission_response,
|
|
7043
7078
|
)
|
|
@@ -7095,7 +7130,10 @@ class ProcessManager:
|
|
|
7095
7130
|
stdout="",
|
|
7096
7131
|
stderr=client.stderr_tail[-10000:] if client is not None else "",
|
|
7097
7132
|
error=f"Timed out after {timeout}s (absolute limit)",
|
|
7098
|
-
|
|
7133
|
+
# AGT-12 (audit): this is the run_turn absolute cap, not an
|
|
7134
|
+
# idle gap — a distinct code keeps retry/degrade policies
|
|
7135
|
+
# from misreading a too-long task as a stuck one.
|
|
7136
|
+
failure_code="zcode_turn_timeout",
|
|
7099
7137
|
)
|
|
7100
7138
|
except ZCodeProtocolError as exc:
|
|
7101
7139
|
return TaskResult(
|
|
@@ -10916,8 +10954,22 @@ class RuntimeDaemon:
|
|
|
10916
10954
|
# Lock held by another process — try to read its PID and kill it
|
|
10917
10955
|
try:
|
|
10918
10956
|
old_pid = int(lock_path.read_text().strip())
|
|
10919
|
-
|
|
10920
|
-
|
|
10957
|
+
if not _pid_looks_like_forgexa_daemon(old_pid):
|
|
10958
|
+
# AGT-05 (audit): a stale PID reused by an unrelated
|
|
10959
|
+
# process must never be signalled — verify identity from
|
|
10960
|
+
# the process cmdline before killing (mirrors the Rust
|
|
10961
|
+
# daemon manager's check).
|
|
10962
|
+
logger.error(
|
|
10963
|
+
"Lock file names PID %d but it is not a Forgexa daemon — "
|
|
10964
|
+
"refusing to signal it; remove the stale lock manually",
|
|
10965
|
+
old_pid,
|
|
10966
|
+
)
|
|
10967
|
+
old_pid = 0
|
|
10968
|
+
if old_pid:
|
|
10969
|
+
logger.warning("Another daemon is running (PID %d). Sending SIGTERM...", old_pid)
|
|
10970
|
+
os.kill(old_pid, signal.SIGTERM)
|
|
10971
|
+
if not old_pid:
|
|
10972
|
+
raise ValueError("no verified daemon PID")
|
|
10921
10973
|
# Wait up to 5 seconds for it to exit
|
|
10922
10974
|
for _ in range(50):
|
|
10923
10975
|
try:
|
|
@@ -15571,6 +15623,26 @@ class RuntimeDaemon:
|
|
|
15571
15623
|
)
|
|
15572
15624
|
return {"push_error": f"Would push to default branch {current_branch}"}
|
|
15573
15625
|
|
|
15626
|
+
# AGT-10 (audit): last line of defense after a collision was
|
|
15627
|
+
# manually released at gate review — the executed branch must be
|
|
15628
|
+
# this requirement's canonical branch (or the workflow's recorded
|
|
15629
|
+
# analysis branch), never another requirement's.
|
|
15630
|
+
if task.requirement_key and current_branch and expected_branch:
|
|
15631
|
+
if current_branch != expected_branch:
|
|
15632
|
+
logger.error(
|
|
15633
|
+
"CRITICAL: requirement %s is about to push branch '%s' "
|
|
15634
|
+
"which is not its canonical branch '%s' — aborting",
|
|
15635
|
+
task.requirement_key, current_branch, expected_branch,
|
|
15636
|
+
)
|
|
15637
|
+
return {
|
|
15638
|
+
"push_error": (
|
|
15639
|
+
f"Push blocked: branch '{current_branch}' does not belong to "
|
|
15640
|
+
f"requirement {task.requirement_key} (expected '{expected_branch}'). "
|
|
15641
|
+
"A cross-requirement branch collision was likely released at review; "
|
|
15642
|
+
"re-run the fix on the correct branch."
|
|
15643
|
+
),
|
|
15644
|
+
}
|
|
15645
|
+
|
|
15574
15646
|
if task.requirement_key and current_branch:
|
|
15575
15647
|
forbidden_merges = await self._list_unpushed_merge_commits(
|
|
15576
15648
|
workspace_path, current_branch, default_branch,
|
|
@@ -3612,6 +3612,48 @@ def cmd_zcode_approve_build(args: argparse.Namespace) -> None:
|
|
|
3612
3612
|
print("running daemons and desktop sidecars pick this up on their next probe (no restart needed)")
|
|
3613
3613
|
|
|
3614
3614
|
|
|
3615
|
+
def cmd_agent_approve_live(args: argparse.Namespace) -> None:
|
|
3616
|
+
from datetime import datetime, timezone
|
|
3617
|
+
|
|
3618
|
+
from . import agent_core
|
|
3619
|
+
|
|
3620
|
+
agent_id = args.agent_id.strip().lower()
|
|
3621
|
+
gated = set(agent_core._UNVERIFIED_AGENT_LIVE_GATE_REASONS)
|
|
3622
|
+
if agent_id not in gated:
|
|
3623
|
+
print(
|
|
3624
|
+
f"error: unknown gated agent '{agent_id}'; gated agents: {', '.join(sorted(gated))}",
|
|
3625
|
+
file=sys.stderr,
|
|
3626
|
+
)
|
|
3627
|
+
return
|
|
3628
|
+
path = agent_core.AGENT_LIVE_VERIFIED_FILE
|
|
3629
|
+
existing = ""
|
|
3630
|
+
try:
|
|
3631
|
+
existing = path.read_text(encoding="utf-8")
|
|
3632
|
+
except OSError:
|
|
3633
|
+
pass
|
|
3634
|
+
approved = {
|
|
3635
|
+
line.split("#", 1)[0].strip().lower()
|
|
3636
|
+
for line in existing.splitlines() if line.split("#", 1)[0].strip()
|
|
3637
|
+
}
|
|
3638
|
+
if agent_id in approved:
|
|
3639
|
+
print(f"already live-verified: {agent_id}")
|
|
3640
|
+
print(f"allowlist file: {path}")
|
|
3641
|
+
return
|
|
3642
|
+
who = getpass.getuser()
|
|
3643
|
+
when = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
3644
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
3645
|
+
suffix = "" if (not existing or existing.endswith("\n")) else "\n"
|
|
3646
|
+
path.write_text(
|
|
3647
|
+
existing + suffix
|
|
3648
|
+
+ f"{agent_id} # live gate accepted by {who} at {when} via `forgexa agent approve-live`\n",
|
|
3649
|
+
encoding="utf-8",
|
|
3650
|
+
)
|
|
3651
|
+
print(f"live gate approved for agent: {agent_id}")
|
|
3652
|
+
print(f"accepted by: {who} at {when}")
|
|
3653
|
+
print(f"written to: {path}")
|
|
3654
|
+
print("only do this after completing the live start/resume/cancel/isolation acceptance checklist")
|
|
3655
|
+
|
|
3656
|
+
|
|
3615
3657
|
def main() -> None:
|
|
3616
3658
|
global _SERVER_URL_OVERRIDE, _output_format
|
|
3617
3659
|
|
|
@@ -3841,6 +3883,19 @@ def main() -> None:
|
|
|
3841
3883
|
help="Path to the zcode binary (default: $FACTORY_ZCODE_PATH or `zcode` on PATH)",
|
|
3842
3884
|
)
|
|
3843
3885
|
|
|
3886
|
+
# agent
|
|
3887
|
+
agent_p = sub.add_parser("agent", help="Agent gate management")
|
|
3888
|
+
agent_sub = agent_p.add_subparsers(dest="agent_cmd")
|
|
3889
|
+
agent_approve_live_p = agent_sub.add_parser(
|
|
3890
|
+
"approve-live",
|
|
3891
|
+
help="Record that an agent passed its live acceptance gate "
|
|
3892
|
+
"(opens the fail-closed live gate for daemon dispatch and desktop probes)",
|
|
3893
|
+
)
|
|
3894
|
+
agent_approve_live_p.add_argument(
|
|
3895
|
+
"agent_id", metavar="AGENT_ID",
|
|
3896
|
+
help="Gated agent id (e.g. trae, zcode)",
|
|
3897
|
+
)
|
|
3898
|
+
|
|
3844
3899
|
# runtimes
|
|
3845
3900
|
rt_p = sub.add_parser("runtimes", help="Runtime management")
|
|
3846
3901
|
rt_sub = rt_p.add_subparsers(dest="rt_cmd")
|
|
@@ -3973,6 +4028,9 @@ def main() -> None:
|
|
|
3973
4028
|
"zcode": lambda a: {
|
|
3974
4029
|
"approve-build": cmd_zcode_approve_build,
|
|
3975
4030
|
}.get(a.zcode_cmd, lambda _: zcode_p.print_help())(a),
|
|
4031
|
+
"agent": lambda a: {
|
|
4032
|
+
"approve-live": cmd_agent_approve_live,
|
|
4033
|
+
}.get(a.agent_cmd, lambda _: agent_p.print_help())(a),
|
|
3976
4034
|
"runtimes": lambda a: {
|
|
3977
4035
|
"list": cmd_runtimes_list,
|
|
3978
4036
|
}.get(a.rt_cmd, lambda _: rt_p.print_help())(a),
|
|
@@ -278,7 +278,18 @@ class ZCodeProtocolSession:
|
|
|
278
278
|
text = ""
|
|
279
279
|
usage: dict[str, int] = {}
|
|
280
280
|
if terminal.status == "success":
|
|
281
|
-
|
|
281
|
+
try:
|
|
282
|
+
text, usage = await self._fetch_turn_transcript(terminal.turn_id)
|
|
283
|
+
except ZCodeProtocolError as exc:
|
|
284
|
+
# AGT-06 (audit): the work already landed on disk — a
|
|
285
|
+
# transcript read-back failure must not flip a terminal
|
|
286
|
+
# success into a protocol failure (and a pointless re-run).
|
|
287
|
+
# Degrade to empty text with the reason surfaced.
|
|
288
|
+
logger.warning(
|
|
289
|
+
"zcode transcript read-back failed for session %s: %s",
|
|
290
|
+
self.session_id, exc,
|
|
291
|
+
)
|
|
292
|
+
text = ""
|
|
282
293
|
return ZCodeTurnResult(
|
|
283
294
|
status=terminal.status,
|
|
284
295
|
session_id=self.session_id or "",
|
|
@@ -297,6 +308,7 @@ class ZCodeProtocolSession:
|
|
|
297
308
|
return "", {}
|
|
298
309
|
texts: list[str] = []
|
|
299
310
|
totals = {"input_tokens": 0, "output_tokens": 0}
|
|
311
|
+
turn_known = isinstance(turn_id, str) and bool(turn_id)
|
|
300
312
|
for message in messages:
|
|
301
313
|
if not isinstance(message, dict):
|
|
302
314
|
continue
|
|
@@ -305,7 +317,14 @@ class ZCodeProtocolSession:
|
|
|
305
317
|
continue
|
|
306
318
|
anchor = info.get("anchor") if isinstance(info.get("anchor"), dict) else {}
|
|
307
319
|
message_turn = anchor.get("turnId")
|
|
308
|
-
if
|
|
320
|
+
if turn_known and isinstance(message_turn, str) and message_turn != turn_id:
|
|
321
|
+
continue
|
|
322
|
+
if not turn_known and message_turn is not None:
|
|
323
|
+
# AGT-06 (audit): the ack/op-event channel never yielded a
|
|
324
|
+
# turnId — attributing the whole resumed session's assistant
|
|
325
|
+
# history to this turn would pollute the answer. Only
|
|
326
|
+
# un-anchored messages (the current in-flight exchange) may
|
|
327
|
+
# be attributed; anchored history belongs to earlier turns.
|
|
309
328
|
continue
|
|
310
329
|
parts = message.get("parts") if isinstance(message.get("parts"), list) else []
|
|
311
330
|
for part in parts:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|