prizmkit 1.1.145 → 1.1.146
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.
- package/bundled/VERSION.json +3 -3
- package/bundled/dev-pipeline/prizmkit_runtime/runner_bookkeeping.py +25 -13
- package/bundled/dev-pipeline/prizmkit_runtime/sessions.py +145 -49
- package/bundled/dev-pipeline/scripts/parse-stream-progress.py +566 -4
- package/bundled/dev-pipeline/tests/live_ai_cli_log_smoke.py +206 -0
- package/bundled/dev-pipeline/tests/test_python_runner_parity.py +279 -12
- package/bundled/dev-pipeline/tests/test_unified_cli.py +455 -0
- package/bundled/skills/_metadata.json +1 -1
- package/package.json +1 -1
|
@@ -994,6 +994,328 @@ def test_progress_parser_does_not_mark_failed_pi_agent_end_as_success():
|
|
|
994
994
|
assert "provider request failed" in data["errors"]
|
|
995
995
|
|
|
996
996
|
|
|
997
|
+
def test_progress_parser_recovers_from_retrying_pi_attempt_before_final_success():
|
|
998
|
+
module = _load_progress_parser_module()
|
|
999
|
+
tracker = module.ProgressTracker()
|
|
1000
|
+
tracker.process_event({"type": "session", "version": 3, "id": "pi-session", "cwd": "/tmp/project"})
|
|
1001
|
+
tracker.process_event({"type": "agent_start"})
|
|
1002
|
+
tracker.process_event({
|
|
1003
|
+
"type": "message_end",
|
|
1004
|
+
"message": {
|
|
1005
|
+
"role": "assistant",
|
|
1006
|
+
"content": [],
|
|
1007
|
+
"stopReason": "error",
|
|
1008
|
+
"errorMessage": "Error Code internal_server_error: unexpected EOF",
|
|
1009
|
+
},
|
|
1010
|
+
})
|
|
1011
|
+
tracker.process_event({"type": "agent_end", "willRetry": True, "messages": []})
|
|
1012
|
+
tracker.process_event({
|
|
1013
|
+
"type": "auto_retry_start",
|
|
1014
|
+
"attempt": 1,
|
|
1015
|
+
"delayMs": 2000,
|
|
1016
|
+
"errorMessage": "Error Code internal_server_error: unexpected EOF",
|
|
1017
|
+
})
|
|
1018
|
+
tracker.process_event({"type": "agent_start"})
|
|
1019
|
+
tracker.process_event({"type": "auto_retry_end", "attempt": 1})
|
|
1020
|
+
tracker.process_event({
|
|
1021
|
+
"type": "message_end",
|
|
1022
|
+
"message": {
|
|
1023
|
+
"role": "assistant",
|
|
1024
|
+
"content": [{"type": "text", "text": "WORKFLOW_COMPLETED"}],
|
|
1025
|
+
"stopReason": "stop",
|
|
1026
|
+
},
|
|
1027
|
+
})
|
|
1028
|
+
tracker.process_event({
|
|
1029
|
+
"type": "agent_end",
|
|
1030
|
+
"willRetry": False,
|
|
1031
|
+
"messages": [{
|
|
1032
|
+
"role": "assistant",
|
|
1033
|
+
"content": [{"type": "text", "text": "WORKFLOW_COMPLETED"}],
|
|
1034
|
+
"stopReason": "stop",
|
|
1035
|
+
}],
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
data = tracker.to_dict()
|
|
1039
|
+
|
|
1040
|
+
assert data["result_subtype"] == "success"
|
|
1041
|
+
assert data["last_result_is_error"] is False
|
|
1042
|
+
assert data["stop_reason"] == "stop"
|
|
1043
|
+
assert data["fatal_error_code"] == ""
|
|
1044
|
+
assert data["terminal_success_at"]
|
|
1045
|
+
assert any("unexpected EOF" in str(error) for error in data["errors"])
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def test_progress_parser_keeps_final_error_after_internal_retry_exhausts():
|
|
1049
|
+
module = _load_progress_parser_module()
|
|
1050
|
+
tracker = module.ProgressTracker()
|
|
1051
|
+
tracker.process_event({"type": "session", "id": "pi-session"})
|
|
1052
|
+
tracker.process_event({"type": "agent_start"})
|
|
1053
|
+
tracker.process_event({
|
|
1054
|
+
"type": "message_end",
|
|
1055
|
+
"message": {"role": "assistant", "content": [], "stopReason": "error", "errorMessage": "temporary error"},
|
|
1056
|
+
})
|
|
1057
|
+
tracker.process_event({"type": "agent_end", "willRetry": True, "messages": []})
|
|
1058
|
+
tracker.process_event({"type": "auto_retry_start", "attempt": 1, "errorMessage": "temporary error"})
|
|
1059
|
+
tracker.process_event({"type": "agent_start"})
|
|
1060
|
+
tracker.process_event({
|
|
1061
|
+
"type": "message_end",
|
|
1062
|
+
"message": {"role": "assistant", "content": [], "stopReason": "error", "errorMessage": "final error"},
|
|
1063
|
+
})
|
|
1064
|
+
tracker.process_event({
|
|
1065
|
+
"type": "agent_end",
|
|
1066
|
+
"willRetry": False,
|
|
1067
|
+
"messages": [{"role": "assistant", "content": [], "stopReason": "error", "errorMessage": "final error"}],
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
data = tracker.to_dict()
|
|
1071
|
+
|
|
1072
|
+
assert data["result_subtype"] == "error"
|
|
1073
|
+
assert data["last_result_is_error"] is True
|
|
1074
|
+
assert data["stop_reason"] == "error"
|
|
1075
|
+
assert data["terminal_success_at"] == ""
|
|
1076
|
+
assert "final error" in data["terminal_result_text"]
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def test_progress_parser_clears_retrying_fatal_marker_on_new_pi_attempt():
|
|
1080
|
+
module = _load_progress_parser_module()
|
|
1081
|
+
tracker = module.ProgressTracker()
|
|
1082
|
+
tracker.process_event({"type": "session", "id": "pi-session"})
|
|
1083
|
+
tracker.process_event({"type": "agent_start"})
|
|
1084
|
+
tracker.process_event({
|
|
1085
|
+
"type": "message_end",
|
|
1086
|
+
"message": {
|
|
1087
|
+
"role": "assistant",
|
|
1088
|
+
"content": [],
|
|
1089
|
+
"stopReason": "error",
|
|
1090
|
+
"errorMessage": "API Error: Your input exceeds the context window",
|
|
1091
|
+
},
|
|
1092
|
+
})
|
|
1093
|
+
tracker.process_event({"type": "agent_end", "willRetry": True, "messages": []})
|
|
1094
|
+
failed_attempt = tracker.to_dict()
|
|
1095
|
+
|
|
1096
|
+
tracker.process_event({"type": "agent_start"})
|
|
1097
|
+
next_attempt = tracker.to_dict()
|
|
1098
|
+
|
|
1099
|
+
assert failed_attempt["fatal_error_code"] == "context_overflow"
|
|
1100
|
+
assert failed_attempt["last_result_is_error"] is True
|
|
1101
|
+
assert next_attempt["fatal_error_code"] == ""
|
|
1102
|
+
assert next_attempt["result_subtype"] == ""
|
|
1103
|
+
assert next_attempt["last_result_is_error"] is False
|
|
1104
|
+
assert any("context window" in str(error) for error in next_attempt["errors"])
|
|
1105
|
+
assert next_attempt["is_active"] is True
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def test_semantic_log_normalizer_compacts_pi_cumulative_stream():
|
|
1109
|
+
module = _load_progress_parser_module()
|
|
1110
|
+
normalizer = module.SemanticLogNormalizer()
|
|
1111
|
+
events = [
|
|
1112
|
+
{"type": "session", "id": "session-1"},
|
|
1113
|
+
{"type": "message_start", "message": {"role": "user"}},
|
|
1114
|
+
{"type": "message_end", "message": {"role": "user", "content": [{"type": "text", "text": "SECRET PROMPT"}]}},
|
|
1115
|
+
{"type": "turn_start"},
|
|
1116
|
+
{"type": "message_update", "assistantMessageEvent": {"type": "thinking_start", "contentIndex": 0}},
|
|
1117
|
+
{"type": "message_update", "assistantMessageEvent": {"type": "thinking_delta", "contentIndex": 0, "delta": "Inspecting"}},
|
|
1118
|
+
{"type": "message_update", "assistantMessageEvent": {"type": "thinking_delta", "contentIndex": 0, "delta": " state"}},
|
|
1119
|
+
{"type": "message_update", "assistantMessageEvent": {"type": "thinking_end", "contentIndex": 0, "content": "Inspecting state"}},
|
|
1120
|
+
{"type": "tool_execution_start", "toolCallId": "call-1", "toolName": "bash", "args": {"command": "printf ABC"}},
|
|
1121
|
+
{"type": "tool_execution_update", "toolCallId": "call-1", "toolName": "bash", "partialResult": {"content": [{"type": "text", "text": "A"}]}},
|
|
1122
|
+
{"type": "tool_execution_update", "toolCallId": "call-1", "toolName": "bash", "partialResult": {"content": [{"type": "text", "text": "AB"}]}},
|
|
1123
|
+
{"type": "tool_execution_update", "toolCallId": "call-1", "toolName": "bash", "partialResult": {"content": [{"type": "text", "text": "ABC"}]}},
|
|
1124
|
+
{"type": "tool_execution_end", "toolCallId": "call-1", "toolName": "bash", "result": {"content": [{"type": "text", "text": "ABC"}]}, "isError": False},
|
|
1125
|
+
{"type": "message_start", "message": {"role": "toolResult"}},
|
|
1126
|
+
{"type": "message_end", "message": {"role": "toolResult", "toolCallId": "call-1", "toolName": "bash", "content": [{"type": "text", "text": "ABC"}]}},
|
|
1127
|
+
{"type": "message_update", "assistantMessageEvent": {"type": "text_start", "contentIndex": 1}},
|
|
1128
|
+
{"type": "message_update", "assistantMessageEvent": {"type": "text_delta", "contentIndex": 1, "delta": "Done"}},
|
|
1129
|
+
{"type": "message_update", "assistantMessageEvent": {"type": "text_end", "contentIndex": 1, "content": "Done"}},
|
|
1130
|
+
{"type": "message_end", "message": {"role": "assistant", "content": [{"type": "text", "text": "Done"}], "stopReason": "stop"}},
|
|
1131
|
+
{"type": "turn_end", "message": {"role": "assistant", "content": [{"type": "text", "text": "Done"}], "stopReason": "stop"}},
|
|
1132
|
+
{"type": "agent_end", "willRetry": False, "messages": [{"role": "assistant", "content": [{"type": "text", "text": "Done"}], "stopReason": "stop"}]},
|
|
1133
|
+
{"type": "agent_settled"},
|
|
1134
|
+
]
|
|
1135
|
+
|
|
1136
|
+
rendered = "".join(normalizer.process_event(event) for event in events) + normalizer.finish()
|
|
1137
|
+
|
|
1138
|
+
assert rendered.count("[THINK]") == 1
|
|
1139
|
+
assert "[THINK] Inspecting state\n" in rendered
|
|
1140
|
+
assert rendered.count("[ACTION]") == 1
|
|
1141
|
+
assert "bash" in rendered and "printf ABC" in rendered
|
|
1142
|
+
assert rendered.count("[OBSERVATION]") == 1
|
|
1143
|
+
assert "[OBSERVATION] ABC\n" in rendered
|
|
1144
|
+
assert rendered.count("[FINAL]") == 1
|
|
1145
|
+
assert "[FINAL] Done\n" in rendered
|
|
1146
|
+
assert rendered.count("[SESSION_END]") == 1
|
|
1147
|
+
assert "message_update" not in rendered
|
|
1148
|
+
assert "SECRET PROMPT" not in rendered
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def test_semantic_log_normalizer_ignores_user_prompt_but_keeps_nested_tool_results():
|
|
1152
|
+
module = _load_progress_parser_module()
|
|
1153
|
+
normalizer = module.SemanticLogNormalizer()
|
|
1154
|
+
rendered = normalizer.process_event({
|
|
1155
|
+
"type": "user",
|
|
1156
|
+
"message": {
|
|
1157
|
+
"content": [
|
|
1158
|
+
{"type": "text", "text": "SECRET PROMPT"},
|
|
1159
|
+
{"type": "tool_result", "tool_use_id": "tool-1", "content": "tool output"},
|
|
1160
|
+
]
|
|
1161
|
+
},
|
|
1162
|
+
})
|
|
1163
|
+
rendered += normalizer.finish()
|
|
1164
|
+
|
|
1165
|
+
assert "SECRET PROMPT" not in rendered
|
|
1166
|
+
assert "[OBSERVATION] tool output" in rendered
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def test_semantic_log_normalizer_preserves_repeated_plain_and_tool_output_records():
|
|
1170
|
+
module = _load_progress_parser_module()
|
|
1171
|
+
normalizer = module.SemanticLogNormalizer()
|
|
1172
|
+
rendered = normalizer.process_text("same\n")
|
|
1173
|
+
rendered += normalizer.process_text("same\n")
|
|
1174
|
+
rendered += normalizer.process_event({"type": "function_call_result", "id": "one", "output": "same result"})
|
|
1175
|
+
rendered += normalizer.process_event({"type": "function_call_result", "id": "two", "output": "same result"})
|
|
1176
|
+
rendered += normalizer.finish()
|
|
1177
|
+
|
|
1178
|
+
assert rendered.count("[OUTPUT] same") == 2
|
|
1179
|
+
assert rendered.count("[OBSERVATION] same result") == 2
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def test_semantic_log_normalizer_emits_terminal_metadata_once():
|
|
1183
|
+
module = _load_progress_parser_module()
|
|
1184
|
+
normalizer = module.SemanticLogNormalizer()
|
|
1185
|
+
rendered = normalizer.process_event({"type": "turn.completed"})
|
|
1186
|
+
rendered += normalizer.process_event({"type": "result", "subtype": "success", "result": "Done"})
|
|
1187
|
+
rendered += normalizer.finish()
|
|
1188
|
+
|
|
1189
|
+
assert rendered.count("[SESSION_END]") == 1
|
|
1190
|
+
assert "[FINAL] Done" in rendered
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def test_semantic_log_normalizer_preserves_identical_text_from_distinct_turns():
|
|
1194
|
+
module = _load_progress_parser_module()
|
|
1195
|
+
normalizer = module.SemanticLogNormalizer()
|
|
1196
|
+
events = [
|
|
1197
|
+
{"type": "turn_start"},
|
|
1198
|
+
{"type": "message_end", "message": {"role": "assistant", "content": [{"type": "text", "text": "Done"}], "stopReason": "stop"}},
|
|
1199
|
+
{"type": "turn_start"},
|
|
1200
|
+
{"type": "message_end", "message": {"role": "assistant", "content": [{"type": "text", "text": "Done"}], "stopReason": "stop"}},
|
|
1201
|
+
]
|
|
1202
|
+
|
|
1203
|
+
rendered = "".join(normalizer.process_event(event) for event in events) + normalizer.finish()
|
|
1204
|
+
|
|
1205
|
+
assert rendered.count("[FINAL] Done") == 2
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def test_semantic_log_normalizer_size_tracks_unique_cumulative_content():
|
|
1209
|
+
module = _load_progress_parser_module()
|
|
1210
|
+
normalizer = module.SemanticLogNormalizer()
|
|
1211
|
+
chunks = []
|
|
1212
|
+
payload = ""
|
|
1213
|
+
chunks.append(normalizer.process_event({"type": "tool_execution_start", "toolCallId": "call-1", "toolName": "read", "args": {"path": "sample"}}))
|
|
1214
|
+
for index in range(1, 501):
|
|
1215
|
+
payload += chr(64 + ((index - 1) % 26) + 1)
|
|
1216
|
+
chunks.append(normalizer.process_event({
|
|
1217
|
+
"type": "tool_execution_update",
|
|
1218
|
+
"toolCallId": "call-1",
|
|
1219
|
+
"toolName": "read",
|
|
1220
|
+
"partialResult": {"content": [{"type": "text", "text": payload}]},
|
|
1221
|
+
}))
|
|
1222
|
+
assert sum(len(value) for value in normalizer._snapshots.values()) == len(payload)
|
|
1223
|
+
assert not any(stream_id.startswith("tool:call-1:output") for stream_id in normalizer._stream_buffers)
|
|
1224
|
+
chunks.append(normalizer.process_event({
|
|
1225
|
+
"type": "tool_execution_end",
|
|
1226
|
+
"toolCallId": "call-1",
|
|
1227
|
+
"toolName": "read",
|
|
1228
|
+
"result": {"content": [{"type": "text", "text": payload}]},
|
|
1229
|
+
"isError": False,
|
|
1230
|
+
}))
|
|
1231
|
+
rendered = "".join(chunks) + normalizer.finish()
|
|
1232
|
+
|
|
1233
|
+
assert rendered.count(payload) == 1
|
|
1234
|
+
assert len(rendered.encode("utf-8")) <= len(payload.encode("utf-8")) + 256
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
@pytest.mark.parametrize(
|
|
1238
|
+
("events", "expected_labels"),
|
|
1239
|
+
[
|
|
1240
|
+
(
|
|
1241
|
+
[
|
|
1242
|
+
{"type": "assistant", "message": {"content": [{"type": "text", "text": "Answer"}, {"type": "tool_use", "id": "tool-1", "name": "Read", "input": {"path": "a"}}]}},
|
|
1243
|
+
{"type": "user", "message": {"content": [{"type": "tool_result", "tool_use_id": "tool-1", "content": "result"}]}},
|
|
1244
|
+
{"type": "result", "subtype": "success", "stop_reason": "end_turn", "result": "Answer"},
|
|
1245
|
+
],
|
|
1246
|
+
{"[FINAL]", "[ACTION]", "[OBSERVATION]", "[SESSION_END]"},
|
|
1247
|
+
),
|
|
1248
|
+
(
|
|
1249
|
+
[
|
|
1250
|
+
{"type": "message", "sessionId": "cb-1", "role": "assistant", "content": [{"type": "text", "text": "Answer"}]},
|
|
1251
|
+
{"type": "function_call", "sessionId": "cb-1", "id": "call-1", "name": "read", "arguments": "{\"path\":\"a\"}"},
|
|
1252
|
+
{"type": "function_call_result", "sessionId": "cb-1", "id": "call-1", "output": "result"},
|
|
1253
|
+
{"type": "result", "subtype": "success", "result": "Answer"},
|
|
1254
|
+
],
|
|
1255
|
+
{"[FINAL]", "[ACTION]", "[OBSERVATION]", "[SESSION_END]"},
|
|
1256
|
+
),
|
|
1257
|
+
(
|
|
1258
|
+
[
|
|
1259
|
+
{"type": "turn.started"},
|
|
1260
|
+
{"type": "item.completed", "item": {"id": "reason-1", "type": "reasoning", "text": "Think"}},
|
|
1261
|
+
{"type": "item.started", "item": {"id": "cmd-1", "type": "command_execution", "command": "printf result"}},
|
|
1262
|
+
{"type": "item.completed", "item": {"id": "cmd-1", "type": "command_execution", "command": "printf result", "aggregated_output": "result", "status": "completed"}},
|
|
1263
|
+
{"type": "item.completed", "item": {"id": "answer-1", "type": "agent_message", "text": "Answer"}},
|
|
1264
|
+
{"type": "turn.completed", "usage": {"input_tokens": 1}},
|
|
1265
|
+
],
|
|
1266
|
+
{"[THINK]", "[FINAL]", "[ACTION]", "[OBSERVATION]", "[SESSION_END]"},
|
|
1267
|
+
),
|
|
1268
|
+
],
|
|
1269
|
+
)
|
|
1270
|
+
def test_semantic_log_normalizer_has_cross_format_contract(events, expected_labels):
|
|
1271
|
+
module = _load_progress_parser_module()
|
|
1272
|
+
normalizer = module.SemanticLogNormalizer()
|
|
1273
|
+
|
|
1274
|
+
rendered = "".join(normalizer.process_event(event) for event in events) + normalizer.finish()
|
|
1275
|
+
|
|
1276
|
+
for label in expected_labels:
|
|
1277
|
+
assert label in rendered
|
|
1278
|
+
assert not any(json.dumps(event, ensure_ascii=False) in rendered for event in events)
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
def test_live_smoke_harness_reports_capability_skips_without_running(tmp_path, monkeypatch, capsys):
|
|
1282
|
+
import importlib.util
|
|
1283
|
+
|
|
1284
|
+
script = PIPELINE_ROOT / "tests" / "live_ai_cli_log_smoke.py"
|
|
1285
|
+
spec = importlib.util.spec_from_file_location("live_ai_cli_log_smoke_test", script)
|
|
1286
|
+
module = importlib.util.module_from_spec(spec)
|
|
1287
|
+
assert spec.loader is not None
|
|
1288
|
+
sys.modules[spec.name] = module
|
|
1289
|
+
try:
|
|
1290
|
+
spec.loader.exec_module(module)
|
|
1291
|
+
finally:
|
|
1292
|
+
sys.modules.pop(spec.name, None)
|
|
1293
|
+
|
|
1294
|
+
fake_command = tmp_path / "fake-cli"
|
|
1295
|
+
fake_command.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
|
1296
|
+
fake_command.chmod(0o755)
|
|
1297
|
+
profile = module.SmokeProfile("example", str(fake_command), "custom")
|
|
1298
|
+
monkeypatch.setattr(module, "discover_profiles", lambda: ([profile], [module.SmokeResult("missing", "skipped", "executable not found")]))
|
|
1299
|
+
monkeypatch.delenv("PRIZMKIT_RUN_LIVE_AI_SMOKE", raising=False)
|
|
1300
|
+
|
|
1301
|
+
code = module.main([])
|
|
1302
|
+
payload = json.loads(capsys.readouterr().out)
|
|
1303
|
+
|
|
1304
|
+
assert code == 0
|
|
1305
|
+
assert {item["name"]: item["status"] for item in payload["results"]} == {
|
|
1306
|
+
"example": "skipped",
|
|
1307
|
+
"missing": "skipped",
|
|
1308
|
+
}
|
|
1309
|
+
reasons = {item["name"]: item["reason"] for item in payload["results"]}
|
|
1310
|
+
assert "not authorized" in reasons["example"]
|
|
1311
|
+
assert "executable not found" in reasons["missing"]
|
|
1312
|
+
assert "Do not read, create, edit, or delete project files" in module.smoke_prompt()
|
|
1313
|
+
assert module._looks_capability_blocked("login required") is True
|
|
1314
|
+
assert module._looks_capability_blocked("network is unreachable") is True
|
|
1315
|
+
assert module._looks_capability_blocked("model not found") is True
|
|
1316
|
+
assert module._looks_capability_blocked("ordinary tool failure") is False
|
|
1317
|
+
|
|
1318
|
+
|
|
997
1319
|
def test_progress_parser_normalizes_claude_subagents_without_double_counting(tmp_path):
|
|
998
1320
|
module = _load_progress_parser_module()
|
|
999
1321
|
tracker = module.ProgressTracker()
|
|
@@ -1090,10 +1412,14 @@ def test_progress_parser_normalizes_codex_collab_metadata_with_missing_fields():
|
|
|
1090
1412
|
},
|
|
1091
1413
|
})
|
|
1092
1414
|
tracker.process_event({"type": "item.completed", "item": {"type": "collab_tool_call", "tool": "delegate"}})
|
|
1415
|
+
tracker.process_event({"type": "turn.completed", "usage": {"input_tokens": 1}})
|
|
1093
1416
|
|
|
1094
1417
|
data = tracker.to_dict()
|
|
1095
1418
|
|
|
1096
1419
|
assert data["event_format"] == "codex-json"
|
|
1420
|
+
assert data["result_subtype"] == "success"
|
|
1421
|
+
assert data["stop_reason"] == "completed"
|
|
1422
|
+
assert data["is_active"] is False
|
|
1097
1423
|
assert data["child_thread_ids"] == ["thread-a", "thread-b"]
|
|
1098
1424
|
assert data["subagent_spawn_count"] == 2
|
|
1099
1425
|
assert data["active_subagent_count"] == 1
|
|
@@ -1184,6 +1510,135 @@ def test_session_dual_writes_progress_and_recovers_backup(tmp_path):
|
|
|
1184
1510
|
assert second_log.read_text(encoding="utf-8") == "larger recovered log"
|
|
1185
1511
|
|
|
1186
1512
|
|
|
1513
|
+
def test_session_reader_does_not_retain_or_reread_raw_history():
|
|
1514
|
+
import inspect
|
|
1515
|
+
import prizmkit_runtime.sessions as sessions
|
|
1516
|
+
|
|
1517
|
+
reader_source = inspect.getsource(sessions._reader_thread)
|
|
1518
|
+
run_source = inspect.getsource(sessions.AISessionLauncher.run)
|
|
1519
|
+
|
|
1520
|
+
assert "lines.append" not in reader_source
|
|
1521
|
+
assert "output_queue" not in reader_source
|
|
1522
|
+
assert "session_log.read_text" not in run_source
|
|
1523
|
+
assert "splitlines()" not in run_source
|
|
1524
|
+
assert "maxsize=1" in run_source
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def test_session_reader_contains_parser_failures_and_continues_draining(tmp_path, monkeypatch):
|
|
1528
|
+
import io
|
|
1529
|
+
import queue
|
|
1530
|
+
import prizmkit_runtime.sessions as sessions
|
|
1531
|
+
|
|
1532
|
+
class FailingTracker:
|
|
1533
|
+
def __init__(self, _session_log):
|
|
1534
|
+
pass
|
|
1535
|
+
|
|
1536
|
+
def process_event(self, _event):
|
|
1537
|
+
raise ValueError("unsupported shape")
|
|
1538
|
+
|
|
1539
|
+
def to_dict(self):
|
|
1540
|
+
return {"event_type_counts": {}, "errors": []}
|
|
1541
|
+
|
|
1542
|
+
class FailingNormalizer:
|
|
1543
|
+
def process_event(self, _event):
|
|
1544
|
+
raise ValueError("unsupported shape")
|
|
1545
|
+
|
|
1546
|
+
def process_text(self, text):
|
|
1547
|
+
return f"[OUTPUT] {text.strip()}\n"
|
|
1548
|
+
|
|
1549
|
+
def finish(self):
|
|
1550
|
+
return "[SESSION_END] status=stream_closed\n"
|
|
1551
|
+
|
|
1552
|
+
monkeypatch.setattr(sessions, "_load_stream_parser_classes", lambda: (FailingTracker, FailingNormalizer))
|
|
1553
|
+
session_log = tmp_path / "session.log"
|
|
1554
|
+
backup_log = tmp_path / "backup.log"
|
|
1555
|
+
session_log.write_text("", encoding="utf-8")
|
|
1556
|
+
backup_log.write_text("", encoding="utf-8")
|
|
1557
|
+
result_queue = queue.Queue(maxsize=1)
|
|
1558
|
+
pipe = io.StringIO('{"type":"result","subtype":"success","result":"done"}\nplain\n')
|
|
1559
|
+
|
|
1560
|
+
sessions._reader_thread(pipe, session_log, backup_log, result_queue)
|
|
1561
|
+
|
|
1562
|
+
text = session_log.read_text(encoding="utf-8")
|
|
1563
|
+
assert "[ERROR] progress parser rejected event=result: ValueError" in text
|
|
1564
|
+
assert "[ERROR] semantic log adapter rejected event=result: ValueError" in text
|
|
1565
|
+
assert "[OUTPUT] done" in text
|
|
1566
|
+
assert "[OUTPUT] plain" in text
|
|
1567
|
+
assert "[SESSION_END] status=stream_closed" in text
|
|
1568
|
+
assert backup_log.read_text(encoding="utf-8") == text
|
|
1569
|
+
assert result_queue.get_nowait()["event_type_counts"] == {}
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def test_session_persists_normalized_stream_without_raw_history(tmp_path, monkeypatch):
|
|
1573
|
+
import prizmkit_runtime.sessions as sessions
|
|
1574
|
+
|
|
1575
|
+
fake_cli = tmp_path / "pi_like_cli.py"
|
|
1576
|
+
fake_cli.write_text(
|
|
1577
|
+
"#!/usr/bin/env python3\n"
|
|
1578
|
+
"import json, sys\n"
|
|
1579
|
+
"_prompt = sys.stdin.read()\n"
|
|
1580
|
+
"events = [\n"
|
|
1581
|
+
" {'type':'session','id':'s1'},\n"
|
|
1582
|
+
" {'type':'agent_start'},\n"
|
|
1583
|
+
" {'type':'turn_start'},\n"
|
|
1584
|
+
" {'type':'message_update','assistantMessageEvent':{'type':'thinking_start','contentIndex':0}},\n"
|
|
1585
|
+
" {'type':'message_update','assistantMessageEvent':{'type':'thinking_delta','contentIndex':0,'delta':'Think'}},\n"
|
|
1586
|
+
" {'type':'message_update','assistantMessageEvent':{'type':'thinking_end','contentIndex':0,'content':'Think'}},\n"
|
|
1587
|
+
" {'type':'tool_execution_start','toolCallId':'c1','toolName':'bash','args':{'command':'printf ABC'}},\n"
|
|
1588
|
+
" {'type':'tool_execution_update','toolCallId':'c1','toolName':'bash','partialResult':{'content':[{'type':'text','text':'A'}]}},\n"
|
|
1589
|
+
" {'type':'tool_execution_update','toolCallId':'c1','toolName':'bash','partialResult':{'content':[{'type':'text','text':'ABC'}]}},\n"
|
|
1590
|
+
" {'type':'tool_execution_end','toolCallId':'c1','toolName':'bash','result':{'content':[{'type':'text','text':'ABC'}]},'isError':False},\n"
|
|
1591
|
+
" {'type':'message_update','assistantMessageEvent':{'type':'text_start','contentIndex':1}},\n"
|
|
1592
|
+
" {'type':'message_update','assistantMessageEvent':{'type':'text_delta','contentIndex':1,'delta':'Done'}},\n"
|
|
1593
|
+
" {'type':'message_update','assistantMessageEvent':{'type':'text_end','contentIndex':1,'content':'Done'}},\n"
|
|
1594
|
+
" {'type':'message_end','message':{'role':'assistant','content':[{'type':'text','text':'Done'}],'stopReason':'stop'}},\n"
|
|
1595
|
+
" {'type':'agent_end','willRetry':False,'messages':[{'role':'assistant','content':[{'type':'text','text':'Done'}],'stopReason':'stop'}]},\n"
|
|
1596
|
+
"]\n"
|
|
1597
|
+
"for event in events: print(json.dumps(event), flush=True)\n",
|
|
1598
|
+
encoding="utf-8",
|
|
1599
|
+
)
|
|
1600
|
+
fake_cli.chmod(0o755)
|
|
1601
|
+
prompt = tmp_path / "prompt.md"
|
|
1602
|
+
prompt.write_text("hello", encoding="utf-8")
|
|
1603
|
+
session_log = tmp_path / "state" / "sessions" / "sess-normalized" / "logs" / "session.log"
|
|
1604
|
+
backup_log = tmp_path / "home" / ".prizmkit" / "session-backups" / "sess-normalized.log"
|
|
1605
|
+
progress = session_log.with_name("progress.json")
|
|
1606
|
+
captured_backup = {}
|
|
1607
|
+
original_recover = sessions.recover_session_log
|
|
1608
|
+
|
|
1609
|
+
def capture_recovery(primary, backup, min_size=1024):
|
|
1610
|
+
captured_backup["text"] = backup.read_text(encoding="utf-8")
|
|
1611
|
+
return original_recover(primary, backup, min_size)
|
|
1612
|
+
|
|
1613
|
+
monkeypatch.setattr(sessions, "recover_session_log", capture_recovery)
|
|
1614
|
+
result = sessions.AISessionLauncher(
|
|
1615
|
+
sessions.AISessionConfig(
|
|
1616
|
+
cli_command=str(fake_cli),
|
|
1617
|
+
platform="pi",
|
|
1618
|
+
model=None,
|
|
1619
|
+
prompt_path=prompt,
|
|
1620
|
+
cwd=tmp_path,
|
|
1621
|
+
log_path=session_log,
|
|
1622
|
+
backup_log_path=backup_log,
|
|
1623
|
+
progress_path=progress,
|
|
1624
|
+
)
|
|
1625
|
+
).run()
|
|
1626
|
+
|
|
1627
|
+
text = session_log.read_text(encoding="utf-8")
|
|
1628
|
+
assert result.exit_code == 0
|
|
1629
|
+
assert result.progress_summary.result_subtype == "success"
|
|
1630
|
+
assert "[THINK] Think" in text
|
|
1631
|
+
assert "[ACTION] bash" in text
|
|
1632
|
+
assert "[OBSERVATION] ABC" in text
|
|
1633
|
+
assert "[FINAL] Done" in text
|
|
1634
|
+
assert "[SESSION_END] status=success" in text
|
|
1635
|
+
assert text.count("ABC") == 2 # one action argument and one observation
|
|
1636
|
+
assert '"type": "message_update"' not in text
|
|
1637
|
+
assert '"type":"message_update"' not in text
|
|
1638
|
+
assert captured_backup["text"] == text
|
|
1639
|
+
assert not backup_log.exists()
|
|
1640
|
+
|
|
1641
|
+
|
|
1187
1642
|
def test_custom_session_launch_writes_existing_logs_and_progress(tmp_path):
|
|
1188
1643
|
from prizmkit_runtime.launch_profiles import HeadlessLaunchProfile
|
|
1189
1644
|
from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
|