qlogicagent 2.11.2 → 2.11.3
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/dist/cli.js +290 -289
- package/dist/index.js +292 -291
- package/dist/pet-contracts.js +1 -1
- package/dist/protocol.js +1 -1
- package/dist/types/cli/handlers/session-handler.d.ts +3 -3
- package/dist/types/cli/media-runtime-facade.d.ts +8 -0
- package/dist/types/cli/pet-runtime.d.ts +1 -3
- package/dist/types/cli/session-query-service.d.ts +1 -0
- package/dist/types/protocol/wire/acp-protocol.d.ts +6 -0
- package/dist/types/protocol/wire/gateway-rpc.d.ts +1 -0
- package/dist/types/protocol/wire/pet-contracts.d.ts +3 -2
- package/dist/types/runtime/infra/acp-detector.d.ts +21 -0
- package/dist/types/runtime/infra/project-store.d.ts +1 -0
- package/dist/types/runtime/pet/hatch-pet-runner.d.ts +18 -0
- package/dist/types/runtime/pet/index.d.ts +12 -5
- package/dist/types/runtime/pet/pet-file-loader.d.ts +1 -1
- package/dist/types/runtime/pet/pet-growth-engine.d.ts +2 -58
- package/dist/types/runtime/pet/pet-profile-service.d.ts +5 -7
- package/dist/types/runtime/pet/pet-reaction-engine.d.ts +11 -0
- package/dist/types/runtime/pet/pet-soul-service.d.ts +2 -2
- package/dist/types/runtime/pet/pet-types.d.ts +1 -32
- package/dist/types/runtime/pet/petdex-asset.d.ts +3 -109
- package/dist/types/runtime/pet/petdex-forge-service.d.ts +3 -94
- package/dist/types/runtime/prompt/fresh-workspace-evidence.d.ts +1 -0
- package/dist/types/runtime/session/session-locator.d.ts +2 -0
- package/dist/types/runtime/session/session-persistence.d.ts +17 -3
- package/dist/types/skills/tools/petdex-create-tool.d.ts +2 -16
- package/dist/vendor/hatch-pet/LICENSE.txt +201 -0
- package/dist/vendor/hatch-pet/NOTICE.md +25 -0
- package/dist/vendor/hatch-pet/references/animation-rows.md +29 -0
- package/dist/vendor/hatch-pet/references/codex-pet-contract.md +35 -0
- package/dist/vendor/hatch-pet/references/qa-rubric.md +66 -0
- package/dist/vendor/hatch-pet/scripts/compose_atlas.py +169 -0
- package/dist/vendor/hatch-pet/scripts/derive_running_left_from_running_right.py +150 -0
- package/dist/vendor/hatch-pet/scripts/extract_strip_frames.py +408 -0
- package/dist/vendor/hatch-pet/scripts/inspect_frames.py +256 -0
- package/dist/vendor/hatch-pet/scripts/make_contact_sheet.py +96 -0
- package/dist/vendor/hatch-pet/scripts/prepare_pet_run.py +830 -0
- package/dist/vendor/hatch-pet/scripts/render_animation_previews.py +78 -0
- package/dist/vendor/hatch-pet/scripts/validate_atlas.py +157 -0
- package/package.json +5 -3
- package/dist/types/runtime/pet/pet-reaction-service.d.ts +0 -33
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Render lightweight animated QA previews from extracted Codex pet frames."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from PIL import Image
|
|
11
|
+
|
|
12
|
+
ROW_DURATIONS = {
|
|
13
|
+
"idle": [280, 110, 110, 140, 140, 320],
|
|
14
|
+
"running-right": [120, 120, 120, 120, 120, 120, 120, 220],
|
|
15
|
+
"running-left": [120, 120, 120, 120, 120, 120, 120, 220],
|
|
16
|
+
"waving": [140, 140, 140, 280],
|
|
17
|
+
"jumping": [140, 140, 140, 140, 280],
|
|
18
|
+
"failed": [140, 140, 140, 140, 140, 140, 140, 240],
|
|
19
|
+
"waiting": [150, 150, 150, 150, 150, 260],
|
|
20
|
+
"running": [120, 120, 120, 120, 120, 220],
|
|
21
|
+
"review": [150, 150, 150, 150, 150, 280],
|
|
22
|
+
}
|
|
23
|
+
IMAGE_SUFFIXES = {".png", ".webp", ".jpg", ".jpeg"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def frame_files(state_dir: Path) -> list[Path]:
|
|
27
|
+
if not state_dir.is_dir():
|
|
28
|
+
return []
|
|
29
|
+
return sorted(path for path in state_dir.iterdir() if path.suffix.lower() in IMAGE_SUFFIXES)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_frames(frames_root: Path, state: str, expected_count: int) -> list[Image.Image]:
|
|
33
|
+
files = frame_files(frames_root / state)
|
|
34
|
+
if len(files) != expected_count:
|
|
35
|
+
raise SystemExit(
|
|
36
|
+
f"{state} preview needs {expected_count} frames, found {len(files)} under {frames_root / state}"
|
|
37
|
+
)
|
|
38
|
+
frames = []
|
|
39
|
+
for path in files:
|
|
40
|
+
with Image.open(path) as opened:
|
|
41
|
+
frames.append(opened.convert("RGBA"))
|
|
42
|
+
return frames
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def save_preview(frames: list[Image.Image], durations: list[int], output: Path) -> None:
|
|
46
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
frames[0].save(
|
|
48
|
+
output,
|
|
49
|
+
save_all=True,
|
|
50
|
+
append_images=frames[1:],
|
|
51
|
+
duration=durations,
|
|
52
|
+
loop=0,
|
|
53
|
+
disposal=2,
|
|
54
|
+
optimize=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main() -> None:
|
|
59
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
60
|
+
parser.add_argument("--frames-root", required=True)
|
|
61
|
+
parser.add_argument("--output-dir", required=True)
|
|
62
|
+
args = parser.parse_args()
|
|
63
|
+
|
|
64
|
+
frames_root = Path(args.frames_root).expanduser().resolve()
|
|
65
|
+
output_dir = Path(args.output_dir).expanduser().resolve()
|
|
66
|
+
previews = []
|
|
67
|
+
for state, durations in ROW_DURATIONS.items():
|
|
68
|
+
frames = load_frames(frames_root, state, len(durations))
|
|
69
|
+
output = output_dir / f"{state}.gif"
|
|
70
|
+
save_preview(frames, durations, output)
|
|
71
|
+
previews.append({"state": state, "path": str(output), "frames": len(frames)})
|
|
72
|
+
|
|
73
|
+
result = {"ok": True, "output_dir": str(output_dir), "previews": previews}
|
|
74
|
+
print(json.dumps(result, indent=2))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
main()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate a Codex pet spritesheet atlas."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from PIL import Image
|
|
12
|
+
|
|
13
|
+
COLUMNS = 8
|
|
14
|
+
ROWS = 9
|
|
15
|
+
CELL_WIDTH = 192
|
|
16
|
+
CELL_HEIGHT = 208
|
|
17
|
+
ATLAS_WIDTH = COLUMNS * CELL_WIDTH
|
|
18
|
+
ATLAS_HEIGHT = ROWS * CELL_HEIGHT
|
|
19
|
+
ROW_BY_INDEX = {
|
|
20
|
+
0: ("idle", 6),
|
|
21
|
+
1: ("running-right", 8),
|
|
22
|
+
2: ("running-left", 8),
|
|
23
|
+
3: ("waving", 4),
|
|
24
|
+
4: ("jumping", 5),
|
|
25
|
+
5: ("failed", 8),
|
|
26
|
+
6: ("waiting", 6),
|
|
27
|
+
7: ("running", 6),
|
|
28
|
+
8: ("review", 6),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def alpha_nonzero_count(image: Image.Image) -> int:
|
|
33
|
+
alpha = image.getchannel("A")
|
|
34
|
+
return sum(alpha.histogram()[1:])
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def transparent_rgb_residue_count(image: Image.Image) -> int:
|
|
38
|
+
rgba = image.convert("RGBA")
|
|
39
|
+
data = rgba.tobytes()
|
|
40
|
+
count = 0
|
|
41
|
+
for index in range(0, len(data), 4):
|
|
42
|
+
red, green, blue, alpha = data[index : index + 4]
|
|
43
|
+
if alpha == 0 and (red or green or blue):
|
|
44
|
+
count += 1
|
|
45
|
+
return count
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> None:
|
|
49
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
50
|
+
parser.add_argument("atlas")
|
|
51
|
+
parser.add_argument("--json-out")
|
|
52
|
+
parser.add_argument("--min-used-pixels", type=int, default=50)
|
|
53
|
+
parser.add_argument("--near-opaque-threshold", type=float, default=0.95)
|
|
54
|
+
parser.add_argument("--allow-opaque", action="store_true")
|
|
55
|
+
parser.add_argument("--allow-near-opaque-used-cells", action="store_true")
|
|
56
|
+
args = parser.parse_args()
|
|
57
|
+
|
|
58
|
+
atlas_path = Path(args.atlas).expanduser().resolve()
|
|
59
|
+
errors: list[str] = []
|
|
60
|
+
warnings: list[str] = []
|
|
61
|
+
near_opaque_used_cells: dict[str, list[int]] = defaultdict(list)
|
|
62
|
+
cells: list[dict[str, object]] = []
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
with Image.open(atlas_path) as opened:
|
|
66
|
+
source_mode = opened.mode
|
|
67
|
+
source_format = opened.format
|
|
68
|
+
image = opened.convert("RGBA")
|
|
69
|
+
except Exception as exc: # noqa: BLE001
|
|
70
|
+
result = {"ok": False, "errors": [f"could not open atlas: {exc}"], "warnings": []}
|
|
71
|
+
print(json.dumps(result, indent=2))
|
|
72
|
+
raise SystemExit(1)
|
|
73
|
+
|
|
74
|
+
if image.size != (ATLAS_WIDTH, ATLAS_HEIGHT):
|
|
75
|
+
errors.append(f"expected {ATLAS_WIDTH}x{ATLAS_HEIGHT}, got {image.width}x{image.height}")
|
|
76
|
+
|
|
77
|
+
if source_format not in {"PNG", "WEBP"}:
|
|
78
|
+
errors.append(f"expected PNG or WebP, got {source_format}")
|
|
79
|
+
|
|
80
|
+
if "A" not in source_mode and not args.allow_opaque:
|
|
81
|
+
errors.append("atlas does not have an alpha channel")
|
|
82
|
+
|
|
83
|
+
for row_index in range(ROWS):
|
|
84
|
+
state, frame_count = ROW_BY_INDEX[row_index]
|
|
85
|
+
for column_index in range(COLUMNS):
|
|
86
|
+
left = column_index * CELL_WIDTH
|
|
87
|
+
top = row_index * CELL_HEIGHT
|
|
88
|
+
cell = image.crop((left, top, left + CELL_WIDTH, top + CELL_HEIGHT))
|
|
89
|
+
nontransparent = alpha_nonzero_count(cell)
|
|
90
|
+
used = column_index < frame_count
|
|
91
|
+
cell_info = {
|
|
92
|
+
"state": state,
|
|
93
|
+
"row": row_index,
|
|
94
|
+
"column": column_index,
|
|
95
|
+
"used": used,
|
|
96
|
+
"nontransparent_pixels": nontransparent,
|
|
97
|
+
}
|
|
98
|
+
cells.append(cell_info)
|
|
99
|
+
if used and nontransparent < args.min_used_pixels:
|
|
100
|
+
errors.append(
|
|
101
|
+
f"{state} row {row_index} column {column_index} is empty or too sparse ({nontransparent} pixels)"
|
|
102
|
+
)
|
|
103
|
+
if used and nontransparent > CELL_WIDTH * CELL_HEIGHT * args.near_opaque_threshold:
|
|
104
|
+
near_opaque_used_cells[f"{state} row {row_index}"].append(column_index)
|
|
105
|
+
if not used and nontransparent != 0:
|
|
106
|
+
errors.append(
|
|
107
|
+
f"{state} row {row_index} unused column {column_index} is not transparent ({nontransparent} pixels)"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
for row_label, columns in near_opaque_used_cells.items():
|
|
111
|
+
message = (
|
|
112
|
+
f"{row_label} has {len(columns)} nearly opaque used cells; "
|
|
113
|
+
"this usually means the sprite has a non-transparent background"
|
|
114
|
+
)
|
|
115
|
+
if args.allow_near_opaque_used_cells:
|
|
116
|
+
warnings.append(message)
|
|
117
|
+
else:
|
|
118
|
+
errors.append(message)
|
|
119
|
+
|
|
120
|
+
alpha_count = alpha_nonzero_count(image)
|
|
121
|
+
if alpha_count == ATLAS_WIDTH * ATLAS_HEIGHT:
|
|
122
|
+
message = "atlas is fully opaque; custom pets require a transparent sprite background"
|
|
123
|
+
if args.allow_opaque:
|
|
124
|
+
warnings.append(message)
|
|
125
|
+
else:
|
|
126
|
+
errors.append(message)
|
|
127
|
+
|
|
128
|
+
transparent_rgb_residue = transparent_rgb_residue_count(image)
|
|
129
|
+
if transparent_rgb_residue:
|
|
130
|
+
errors.append(
|
|
131
|
+
f"atlas has {transparent_rgb_residue} fully transparent pixels with non-zero RGB residue"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
result = {
|
|
135
|
+
"ok": not errors,
|
|
136
|
+
"file": str(atlas_path),
|
|
137
|
+
"format": source_format,
|
|
138
|
+
"mode": source_mode,
|
|
139
|
+
"width": image.width,
|
|
140
|
+
"height": image.height,
|
|
141
|
+
"transparent_rgb_residue_pixels": transparent_rgb_residue,
|
|
142
|
+
"errors": errors,
|
|
143
|
+
"warnings": warnings,
|
|
144
|
+
"cells": cells,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if args.json_out:
|
|
148
|
+
Path(args.json_out).expanduser().resolve().write_text(
|
|
149
|
+
json.dumps(result, indent=2) + "\n", encoding="utf-8"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
print(json.dumps({k: v for k, v in result.items() if k != "cells"}, indent=2))
|
|
153
|
+
raise SystemExit(0 if result["ok"] else 1)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qlogicagent",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.3",
|
|
4
4
|
"description": "XiaozhiClaw Agent CLI — subprocess architecture (JSON-RPC over stdio)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -68,11 +68,12 @@
|
|
|
68
68
|
"build:tsc": "tsc --noCheck",
|
|
69
69
|
"start": "node dist/cli.js",
|
|
70
70
|
"test": "vitest run",
|
|
71
|
-
"check": "tsc --noEmit && pnpm test && pnpm run check:architecture-boundaries && pnpm run check:provider-core-boundary && pnpm run check:workspace-hygiene && pnpm run check:package-artifact",
|
|
71
|
+
"check": "tsc --noEmit && pnpm test && pnpm run check:architecture-boundaries && pnpm run check:provider-core-boundary && pnpm run check:pet-core-boundary && pnpm run check:workspace-hygiene && pnpm run check:package-artifact",
|
|
72
72
|
"test:watch": "vitest",
|
|
73
73
|
"lint": "oxlint .",
|
|
74
74
|
"check:architecture-boundaries": "node scripts/check-architecture-boundaries.mjs",
|
|
75
75
|
"check:provider-core-boundary": "node scripts/check-provider-core-boundary.mjs",
|
|
76
|
+
"check:pet-core-boundary": "node scripts/check-pet-core-boundary.mjs",
|
|
76
77
|
"check:workspace-hygiene": "node scripts/check-workspace-hygiene.mjs",
|
|
77
78
|
"check:workspace-hygiene:strict": "node scripts/check-workspace-hygiene.mjs --strict",
|
|
78
79
|
"check:package-artifact": "npm run build && node scripts/check-package-artifact.mjs",
|
|
@@ -86,7 +87,8 @@
|
|
|
86
87
|
},
|
|
87
88
|
"dependencies": {
|
|
88
89
|
"@napi-rs/canvas": "0.1.100",
|
|
89
|
-
"@xiaozhiclaw/
|
|
90
|
+
"@xiaozhiclaw/pet-core": "0.1.0",
|
|
91
|
+
"@xiaozhiclaw/provider-core": "0.1.4",
|
|
90
92
|
"better-sqlite3": "^12.10.0",
|
|
91
93
|
"dotenv": "^17.3.1",
|
|
92
94
|
"nanoid": "^5.1.5",
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PetReactionService — probabilistic reaction generation after turns.
|
|
3
|
-
*
|
|
4
|
-
* P0: Local template pools (no LLM).
|
|
5
|
-
* P1+: LLM-generated personalized reactions via small model.
|
|
6
|
-
*/
|
|
7
|
-
import type { PetSoul } from "./pet-types.js";
|
|
8
|
-
export type ReactionPool = "greeting" | "completion" | "error" | "curiosity" | "bored" | "memory" | "tool_success";
|
|
9
|
-
export type ReactionStyle = "normal" | "thinking" | "excited" | "sleepy";
|
|
10
|
-
/**
|
|
11
|
-
* Determine whether a reaction should fire, and if so, return it.
|
|
12
|
-
* P0: uses local template pools.
|
|
13
|
-
*/
|
|
14
|
-
export declare function maybeGenerateReaction(eventMethod: string): {
|
|
15
|
-
text: string;
|
|
16
|
-
style: ReactionStyle;
|
|
17
|
-
} | null;
|
|
18
|
-
export interface LLMReactionContext {
|
|
19
|
-
turnSummary: string;
|
|
20
|
-
soul: PetSoul;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Generate a personalized reaction via small LLM call.
|
|
24
|
-
* Returns null on failure or timeout (graceful degradation to template pool).
|
|
25
|
-
* Max 50 tokens, under 2s.
|
|
26
|
-
*
|
|
27
|
-
* @param ctx Context for the reaction
|
|
28
|
-
* @param llmGenerate External function to call LLM (injected to avoid coupling)
|
|
29
|
-
*/
|
|
30
|
-
export declare function generateLLMReaction(ctx: LLMReactionContext, llmGenerate: (prompt: string, maxTokens: number) => Promise<string | null>): Promise<{
|
|
31
|
-
text: string;
|
|
32
|
-
style: ReactionStyle;
|
|
33
|
-
} | null>;
|