@tea-agent/loop-agent 0.44.0-next.13 → 0.44.0-next.14
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/CHANGELOG.md +12 -0
- package/README.md +12 -12
- package/dist/adapters/loop-agent.js +34 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +5 -5
- package/dist/cli/program.js +1 -4
- package/dist/commands/init-legacy-audit.js +330 -0
- package/dist/commands/init-upgrade.js +644 -104
- package/dist/commands/init.js +685 -330
- package/dist/commands/operator-guide.js +99 -0
- package/dist/commands/operator.js +16 -3
- package/dist/executors/pi-sdk-executor.js +3 -1
- package/dist/governance/harness.js +2 -7
- package/dist/governance/manifest-types.js +28 -9
- package/dist/governance/project-capability-scanner.js +263 -0
- package/dist/governance/project-config.js +96 -0
- package/dist/governance/resource-catalog.js +151 -0
- package/dist/infrastructure/console/app-data.js +6 -2
- package/dist/shared/operator/capabilities.js +2 -2
- package/dist/shared/package-metadata.js +23 -7
- package/dist/shared/pi-context-pressure/extension.js +23 -0
- package/dist/shared/pi-context-pressure/index.js +1 -0
- package/dist/shared/pi-context-pressure/telemetry.js +4 -0
- package/dist/shared/pi-context-pressure/tool-result-pruner.js +98 -0
- package/dist/shared/update/npm-client.js +49 -1
- package/dist/worker/cli.js +3 -5
- package/dist/worker/console/chat/browser-automation.js +114 -5
- package/dist/worker/console/chat/browser-routes.js +21 -0
- package/dist/worker/console/chat/chat-event-store.js +7 -6
- package/dist/worker/console/chat/context-insights.js +84 -19
- package/dist/worker/console/chat/pet/pet-affinity.js +169 -0
- package/dist/worker/console/chat/pet/pet-persist.js +109 -0
- package/dist/worker/console/chat/pet/pet-registry.js +142 -0
- package/dist/worker/console/chat/pet/pet-remarks.js +48 -0
- package/dist/worker/console/chat/pet/pet-routes.js +125 -0
- package/dist/worker/console/chat/pet/pet-service.js +262 -0
- package/dist/worker/console/chat/pet/pet-state.js +99 -0
- package/dist/worker/console/chat/pi-mode-loop-isolation.js +17 -2
- package/dist/worker/console/chat/pi-runtime/custom-tools/browser-tools.js +2 -2
- package/dist/worker/console/chat/pi-runtime.js +7 -2
- package/dist/worker/console/chat/routes.js +43 -7
- package/dist/worker/console/chat/scheduled-goal-request.js +14 -6
- package/dist/worker/console/chat/session-store.js +20 -3
- package/dist/worker/console/console-shutdown.js +34 -0
- package/dist/worker/console/server.js +2 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BA7qCL_r.js → abnfDiagram-N423BO3Z-DASc69AM.js} +1 -1
- package/dist/worker/console/static/assets/{arc-Df09ajwL.js → arc-h09p-LAc.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CpF9sw5R.js → architectureDiagram-T3A2C74G-CZ3dtLkh.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BtPys8ja.js → blockDiagram-VBNYF7ZC-DMrRhMXG.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DZPnTWqT.js → c4Diagram-5PPSVZJV-DQxisHV6.js} +1 -1
- package/dist/worker/console/static/assets/channel-D1zgKZfg.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BD9D4h1p.js → chunk-2GRJ4B5K-CefVN9zh.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-Svn8yBmn.js → chunk-2Q5K7J3B-DmR1Koq2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-B8EbQj-S.js → chunk-5RXB4S5H-NlzrSW7L.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-tENZpcpF.js → chunk-5VM5RSS4-DfelJv-0.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DBx71Lco.js → chunk-6Q2QTUOP-BK6RFy58.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-1v_QamMz.js → chunk-GF5L2VYU-BAeh9lz4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BAMCNNBG.js → chunk-JWPE2WC7-ChPS5WzG.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BvuEury-.js → chunk-KBJHAD2P-BoVe5IM6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-YqFuHp8s.js → chunk-RYQCIY6F-C4q_0Lw9.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-ouI5T7Mr.js → chunk-XXDRQBXY-Bb9TvjeY.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CXNNsY2M.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CXNNsY2M.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-Bri0_Izk.js → cose-bilkent-JH36ORCC-DmNnL8Ff.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-e8Vhhi4X.js → cynefin-VYW2F7L2-Contvx58.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-tSIXGQiJ.js → cynefinDiagram-MW4NZA55-Cigq7cjk.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Rl-sF5pt.js → dagre-VZM6K2ZE-dEbC0nxb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Db6ddPz8.js → diagram-7IWD3JNH-CzIztESV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Jwd_Xw5S.js → diagram-B4RE2ZJO-Bo14zbaY.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CeC_8InC.js → diagram-LBJQPF4R-C4xUyil6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LKPRaNq8.js → diagram-Q27KOJAE-CaBY_uYa.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-DlVQytMW.js → diagram-UB23O5K3-ClUXUeiu.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DnfJZwO-.js → ebnfDiagram-BXEA7PRR-BbGCgjC6.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-3axVSXca.js → erDiagram-JOGREHBK-Dr-AcwSl.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-xe04-_L-.js → flowDiagram-UKHOOZJN-BG8Gcfyc.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-hpmRINiD.js → ganttDiagram-PKOTCBZU-CFVaPs5r.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-6HSNz9Ko.js → gitGraphDiagram-DS77QQ5N-ChJ8pm-O.js} +1 -1
- package/dist/worker/console/static/assets/index-B_5GhVIO.js +488 -0
- package/dist/worker/console/static/assets/index-C7YMBluX.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BF6ksWTX.js → infoDiagram-6WML65LV-C5J9-uCZ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DXisdeD7.js → ishikawaDiagram-WSZJBQD7-BcfjqzDv.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BWu65wmu.js → journeyDiagram-NVQOT4AX-BzzCO-rD.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-hfNhHqAI.js → kanban-definition-27J2QSJJ-NCHUZ46h.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D6HTfQcp.js → linear-BKZlWCUU.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-Iv91R1WP.js → mermaid.core-DbTaQjFE.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-CfPChwjR.js → mindmap-definition-FAOFIHXS-CroYl5xQ.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-B7TcoqHz.js → pegDiagram-VL7TDLO6-DgMBx-9k.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-ouyPKMtQ.js → pieDiagram-7S7Q4E2Y-CyWDmbq1.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B9tuyLbh.js → quadrantDiagram-CIZ2JOQS-BgGEOqSP.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Ck4q9nJs.js → railroadDiagram-AXF67PYL-BMZ7oeeV.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-wXLX5Dnp.js → requirementDiagram-LRYGKXZP-DxqyGhcc.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D2XWw6_m.js → sankeyDiagram-W5VNT64P-DnSvNCG8.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DzxKiug4.js → sequenceDiagram-SI44F4Z6-BiZh6f-R.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DWUqK5Us.js → sizeCapture-X5ZJPWSS-BKXSI8WB.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-D5Pa4O5g.js → stateDiagram-OKZ733FA-CfSgoYFm.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DgHi3vjb.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-VH7cRnsJ.js → swimlanes-SLNWSIFB-CyGADcHe.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bgvdt1Y0.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-C4HM5NoZ.js → timeline-definition-Z64GVDOM-HmPaSPOK.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Bu-4hOlA.js → vennDiagram-T6HMQDX7-VA2VBbez.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CasFEvvl.js → wardleyDiagram-T6FBY63Y-BxjiBPZa.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D-gpwPSA.js → xychartDiagram-ELKLHX3M--hUY-qFj.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static/pets/ember/pet.json +167 -0
- package/dist/worker/console/static/pets/ember/spritesheet.png +0 -0
- package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +4 -3
- package/dist/worker/console/static-src/operator-chat/use-stat-dialog.js +95 -0
- package/dist/worker/console/workspace-context.js +14 -0
- package/dist/worker/console/workspace-initialization.js +25 -16
- package/dist/worker/delivery/package.js +4 -1
- package/dist/worker/observe/static/operator-chrome.js +1 -42
- package/dist/workflows/dag/backend-test-pytest-collection.js +4 -1
- package/dist/workflows/dag/final-verification.js +45 -10
- package/dist/workflows/dag/hybrid/sources.js +14 -14
- package/dist/workflows/dag/hybrid/templates/backend-test.js +5 -5
- package/dist/workflows/dag/hybrid/templates/frontend-test.js +4 -4
- package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +6 -6
- package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +5 -5
- package/dist/workflows/dag/skill-instructions.js +102 -32
- package/dist/workflows/dag/skill-snapshot.js +3 -0
- package/dist/workflows/dag/skills.js +7 -7
- package/dist/workflows/dag/validate.js +2 -2
- package/dist/workflows/dag/workspace-checkpoint.js +13 -1
- package/docs/architecture/runtime-boundaries.md +28 -22
- package/docs/init-surface.manifest.json +43 -295
- package/docs/templates/agent-dag.base.json +9 -9
- package/docs/templates/agent-dag.final-verification.json +6 -6
- package/docs/templates/agent-dag.supervised-implementation.json +12 -12
- package/docs/templates/frontend-implementation-dag.json +2 -2
- package/docs/templates/frontend-test-dag.json +2 -2
- package/docs/templates/hybrid-dag.json +9 -9
- package/docs/templates/knowledge-sync-dag.json +8 -8
- package/package.json +6 -2
- package/resource-catalog.v1.json +1973 -0
- package/resources/agent-bridge/loop-agent/SKILL.md +40 -0
- package/resources/init-history/legacy-surface.v1.json +33 -0
- package/skills/init-update/SKILL.md +34 -0
- package/skills/init-update/agents/openai.yaml +6 -0
- package/dist/worker/console/static/assets/channel-DiTNLZUm.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgL8aiyZ.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgL8aiyZ.js +0 -1
- package/dist/worker/console/static/assets/index-C_8_gWal.css +0 -1
- package/dist/worker/console/static/assets/index-DybcVXCu.js +0 -488
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-C0QIbqO4.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BB9CzRHB.js +0 -8
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pet persistence — ported from @linxin666/dsh-pet persist.ts, retargeted
|
|
3
|
+
* from $DSH_HOME to the console data directory. One JSON file, atomic
|
|
4
|
+
* rename write, tolerant read (corrupt file falls back to defaults).
|
|
5
|
+
* @module loop-agent/console/chat/pet/pet-persist
|
|
6
|
+
*/
|
|
7
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { PET_AFFINITY_MAX, emptyPetAffinity, } from "./pet-affinity.js";
|
|
10
|
+
import { defaultPetTreatConfig, emptyPetTreatLedger, } from "./pet-affinity.js";
|
|
11
|
+
export const DEFAULT_PET_ID = "ember";
|
|
12
|
+
export const DEFAULT_PET_NAME = "小橘灵";
|
|
13
|
+
export const defaultPetDisplayConfig = {
|
|
14
|
+
visible: false,
|
|
15
|
+
size: 160,
|
|
16
|
+
right: 24,
|
|
17
|
+
bottom: 120,
|
|
18
|
+
};
|
|
19
|
+
export const PET_DISPLAY_SIZE_MIN = 32;
|
|
20
|
+
export const PET_DISPLAY_SIZE_MAX = 1024;
|
|
21
|
+
export const PET_DISPLAY_INSET_MAX = 10_000;
|
|
22
|
+
export const PET_NAME_MAX_LENGTH = 20;
|
|
23
|
+
export function emptyPetPersist() {
|
|
24
|
+
return {
|
|
25
|
+
petPersistVersion: 2,
|
|
26
|
+
petId: DEFAULT_PET_ID,
|
|
27
|
+
names: {},
|
|
28
|
+
affinity: emptyPetAffinity(),
|
|
29
|
+
treats: emptyPetTreatLedger(),
|
|
30
|
+
display: { ...defaultPetDisplayConfig },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function finiteNum(value, fallback) {
|
|
34
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
35
|
+
? value
|
|
36
|
+
: fallback;
|
|
37
|
+
}
|
|
38
|
+
function clamp(value, max) {
|
|
39
|
+
return Math.min(max, Math.max(0, value));
|
|
40
|
+
}
|
|
41
|
+
function loadPetNames(parsed) {
|
|
42
|
+
const names = {};
|
|
43
|
+
if (typeof parsed.names !== "object" || parsed.names === null)
|
|
44
|
+
return names;
|
|
45
|
+
for (const [id, value] of Object.entries(parsed.names)) {
|
|
46
|
+
if (id === "" || typeof value !== "string")
|
|
47
|
+
continue;
|
|
48
|
+
const name = value.trim();
|
|
49
|
+
if (name === "")
|
|
50
|
+
continue;
|
|
51
|
+
names[id] = name.slice(0, PET_NAME_MAX_LENGTH);
|
|
52
|
+
}
|
|
53
|
+
return names;
|
|
54
|
+
}
|
|
55
|
+
/** Load persisted state; missing or corrupt files fall back to defaults. */
|
|
56
|
+
export function loadPetPersist(dir) {
|
|
57
|
+
try {
|
|
58
|
+
const raw = readFileSync(join(dir, "pet.json"), "utf8");
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
const base = emptyPetPersist();
|
|
61
|
+
const rawAffinity = (parsed.affinity ?? {});
|
|
62
|
+
const affinity = {
|
|
63
|
+
points: clamp(finiteNum(rawAffinity.points, 0), PET_AFFINITY_MAX),
|
|
64
|
+
lastPetAt: clamp(finiteNum(rawAffinity.lastPetAt, 0), Number.MAX_SAFE_INTEGER),
|
|
65
|
+
lastFeedAt: clamp(finiteNum(rawAffinity.lastFeedAt, 0), Number.MAX_SAFE_INTEGER),
|
|
66
|
+
pets: clamp(finiteNum(rawAffinity.pets, 0), Number.MAX_SAFE_INTEGER),
|
|
67
|
+
feeds: clamp(finiteNum(rawAffinity.feeds, 0), Number.MAX_SAFE_INTEGER),
|
|
68
|
+
petRejects: clamp(finiteNum(rawAffinity.petRejects, 0), Number.MAX_SAFE_INTEGER),
|
|
69
|
+
feedRejects: clamp(finiteNum(rawAffinity.feedRejects, 0), Number.MAX_SAFE_INTEGER),
|
|
70
|
+
turns: clamp(finiteNum(rawAffinity.turns, 0), Number.MAX_SAFE_INTEGER),
|
|
71
|
+
};
|
|
72
|
+
const rawTreats = (parsed.treats ?? {});
|
|
73
|
+
const treats = {
|
|
74
|
+
treats: clamp(finiteNum(rawTreats.treats, 0), defaultPetTreatConfig.maxTreats),
|
|
75
|
+
lastTreatGrantAt: clamp(finiteNum(rawTreats.lastTreatGrantAt, 0), Number.MAX_SAFE_INTEGER),
|
|
76
|
+
turnsAtLastTreatGrant: clamp(finiteNum(rawTreats.turnsAtLastTreatGrant, 0), Number.MAX_SAFE_INTEGER),
|
|
77
|
+
};
|
|
78
|
+
const rawDisplay = (parsed.display ?? {});
|
|
79
|
+
const display = {
|
|
80
|
+
visible: parsed.petPersistVersion === 2 && typeof rawDisplay.visible === "boolean"
|
|
81
|
+
? rawDisplay.visible
|
|
82
|
+
: base.display.visible,
|
|
83
|
+
size: Math.round(Math.min(PET_DISPLAY_SIZE_MAX, Math.max(PET_DISPLAY_SIZE_MIN, finiteNum(rawDisplay.size, base.display.size)))),
|
|
84
|
+
right: Math.round(clamp(finiteNum(rawDisplay.right, base.display.right), PET_DISPLAY_INSET_MAX)),
|
|
85
|
+
bottom: Math.round(clamp(finiteNum(rawDisplay.bottom, base.display.bottom), PET_DISPLAY_INSET_MAX)),
|
|
86
|
+
};
|
|
87
|
+
const petId = typeof parsed.petId === "string" && parsed.petId.trim() !== ""
|
|
88
|
+
? parsed.petId.trim()
|
|
89
|
+
: base.petId;
|
|
90
|
+
const names = loadPetNames(parsed);
|
|
91
|
+
if (typeof parsed.name === "string" &&
|
|
92
|
+
parsed.name.trim() !== "" &&
|
|
93
|
+
names[petId] === undefined) {
|
|
94
|
+
names[petId] = parsed.name.trim().slice(0, PET_NAME_MAX_LENGTH);
|
|
95
|
+
}
|
|
96
|
+
return { petPersistVersion: 2, petId, names, affinity, treats, display };
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return emptyPetPersist();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Atomically persist state (write temp + rename). */
|
|
103
|
+
export function savePetPersist(data, dir) {
|
|
104
|
+
mkdirSync(dir, { recursive: true });
|
|
105
|
+
const target = join(dir, "pet.json");
|
|
106
|
+
const tmp = `${target}.tmp`;
|
|
107
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
108
|
+
renameSync(tmp, target);
|
|
109
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pet registry — manifest v2 loading, ported from @linxin666/dsh-pet
|
|
3
|
+
* registry.ts (trimmed to the sprite2d path this port ships). Pets live as
|
|
4
|
+
* <petsRoot>/<petId>/{pet.json,spritesheet.webp}; the manifest declares the
|
|
5
|
+
* 9-track animation contract and sequence playlists.
|
|
6
|
+
* @module loop-agent/console/chat/pet/pet-registry
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
/** Animation tracks every manifest must provide (Codex 9-row contract). */
|
|
11
|
+
export const PET_TRACK_ORDER = [
|
|
12
|
+
"idle",
|
|
13
|
+
"running-right",
|
|
14
|
+
"running-left",
|
|
15
|
+
"waving",
|
|
16
|
+
"jumping",
|
|
17
|
+
"failed",
|
|
18
|
+
"waiting",
|
|
19
|
+
"running",
|
|
20
|
+
"review",
|
|
21
|
+
];
|
|
22
|
+
function readPositiveInt(value, fallback) {
|
|
23
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0
|
|
24
|
+
? value
|
|
25
|
+
: fallback;
|
|
26
|
+
}
|
|
27
|
+
function isPetTrackName(name) {
|
|
28
|
+
return PET_TRACK_ORDER.includes(name);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse + validate one manifest document. Invalid manifests throw with a
|
|
32
|
+
* descriptive message; the loader skips them (best-effort registry).
|
|
33
|
+
*/
|
|
34
|
+
export function parsePetManifest(raw) {
|
|
35
|
+
if (typeof raw !== "object" || raw === null) {
|
|
36
|
+
throw new Error("manifest must be an object");
|
|
37
|
+
}
|
|
38
|
+
const doc = raw;
|
|
39
|
+
if (doc.petManifestVersion !== 2) {
|
|
40
|
+
throw new Error("petManifestVersion must be 2");
|
|
41
|
+
}
|
|
42
|
+
if (typeof doc.id !== "string" || doc.id.trim() === "") {
|
|
43
|
+
throw new Error("manifest id must be a non-empty string");
|
|
44
|
+
}
|
|
45
|
+
if (doc.renderer !== "sprite2d") {
|
|
46
|
+
throw new Error(`unsupported renderer: ${String(doc.renderer)}`);
|
|
47
|
+
}
|
|
48
|
+
const sprite = doc.sprite2d;
|
|
49
|
+
if (typeof sprite !== "object" || sprite === null) {
|
|
50
|
+
throw new Error("sprite2d block missing");
|
|
51
|
+
}
|
|
52
|
+
const s = sprite;
|
|
53
|
+
if (typeof s.spritesheetPath !== "string" ||
|
|
54
|
+
s.spritesheetPath === "" ||
|
|
55
|
+
!Array.isArray(s.frames) ||
|
|
56
|
+
s.frames.length !== PET_TRACK_ORDER.length ||
|
|
57
|
+
!s.frames.every((f) => typeof f === "number" && Number.isInteger(f) && f > 0) ||
|
|
58
|
+
typeof s.tracks !== "object" ||
|
|
59
|
+
s.tracks === null) {
|
|
60
|
+
throw new Error("sprite2d atlas contract violated (frames/tracks)");
|
|
61
|
+
}
|
|
62
|
+
const tracks = {};
|
|
63
|
+
for (const [name, value] of Object.entries(s.tracks)) {
|
|
64
|
+
if (!isPetTrackName(name))
|
|
65
|
+
continue;
|
|
66
|
+
if (typeof value !== "object" || value === null)
|
|
67
|
+
continue;
|
|
68
|
+
const durations = value.durations;
|
|
69
|
+
if (!Array.isArray(durations) ||
|
|
70
|
+
!durations.every((d) => typeof d === "number" && d > 0)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
tracks[name] = { durations };
|
|
74
|
+
}
|
|
75
|
+
for (const [index, track] of PET_TRACK_ORDER.entries()) {
|
|
76
|
+
if (tracks[track] === undefined) {
|
|
77
|
+
throw new Error(`track missing: ${track}`);
|
|
78
|
+
}
|
|
79
|
+
if (tracks[track].durations.length !== s.frames[index]) {
|
|
80
|
+
throw new Error(`track duration count mismatch: ${track}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
petManifestVersion: 2,
|
|
85
|
+
id: doc.id,
|
|
86
|
+
displayName: typeof doc.displayName === "string" && doc.displayName !== ""
|
|
87
|
+
? doc.displayName
|
|
88
|
+
: doc.id,
|
|
89
|
+
renderer: "sprite2d",
|
|
90
|
+
...(typeof doc.description === "string" ? { description: doc.description } : {}),
|
|
91
|
+
...(typeof doc.license === "string" ? { license: doc.license } : {}),
|
|
92
|
+
sprite2d: {
|
|
93
|
+
spritesheetPath: s.spritesheetPath,
|
|
94
|
+
cell: {
|
|
95
|
+
width: readPositiveInt(s.cell?.width, 192),
|
|
96
|
+
height: readPositiveInt(s.cell?.height, 208),
|
|
97
|
+
},
|
|
98
|
+
columns: readPositiveInt(s.columns, 8),
|
|
99
|
+
atlasRows: readPositiveInt(s.atlasRows, PET_TRACK_ORDER.length),
|
|
100
|
+
frames: s.frames,
|
|
101
|
+
tracks,
|
|
102
|
+
},
|
|
103
|
+
...(typeof doc.sequences === "object" && doc.sequences !== null
|
|
104
|
+
? { sequences: doc.sequences }
|
|
105
|
+
: {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Load the registry from a pets root: every subdirectory carrying a valid
|
|
110
|
+
* pet.json becomes an entry. Invalid manifests are skipped (best effort).
|
|
111
|
+
*/
|
|
112
|
+
export function loadPetRegistry(petsRoot) {
|
|
113
|
+
const registry = new Map();
|
|
114
|
+
let dirents;
|
|
115
|
+
try {
|
|
116
|
+
dirents = readDir(petsRoot);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return registry;
|
|
120
|
+
}
|
|
121
|
+
for (const dirent of dirents) {
|
|
122
|
+
if (!dirent.isDirectory())
|
|
123
|
+
continue;
|
|
124
|
+
const dir = join(petsRoot, dirent.name);
|
|
125
|
+
try {
|
|
126
|
+
const raw = JSON.parse(readFileSync(join(dir, "pet.json"), "utf8"));
|
|
127
|
+
const manifest = parsePetManifest(raw);
|
|
128
|
+
if (manifest.id !== dirent.name)
|
|
129
|
+
continue;
|
|
130
|
+
registry.set(manifest.id, { manifest, dir });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Skip invalid pet directories silently (best-effort registry).
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return registry;
|
|
137
|
+
}
|
|
138
|
+
// Node fsdirent read kept behind a tiny seam for testability.
|
|
139
|
+
import { readdirSync } from "node:fs";
|
|
140
|
+
function readDir(path) {
|
|
141
|
+
return readdirSync(path, { withFileTypes: true });
|
|
142
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pet remark (reaction copy) library — ported from @linxin666/dsh-pet
|
|
3
|
+
* remarks.ts. Round-robin pools per event kind; deterministic, no clock.
|
|
4
|
+
* @module loop-agent/console/chat/pet/pet-remarks
|
|
5
|
+
*/
|
|
6
|
+
export const PET_REMARK_KINDS = [
|
|
7
|
+
"pet",
|
|
8
|
+
"petCooldown",
|
|
9
|
+
"feed",
|
|
10
|
+
"feedCooldown",
|
|
11
|
+
"noTreats",
|
|
12
|
+
];
|
|
13
|
+
export const PET_REMARK_LINE_MAX = 120;
|
|
14
|
+
export const PET_REMARK_LINES_MAX = 64;
|
|
15
|
+
export const BUILTIN_PET_REMARKS = {
|
|
16
|
+
pet: ["尾巴的火苗跳得更欢了!", "小橘灵舒服地眯起眼睛。", "叶子呆毛轻轻晃了晃。", "它用圆圆的脑袋蹭了蹭你。", "小短手开心地拍了拍。", "火苗噼啪一响,精神满满!", "它原地转了个圈。", "伙伴之间就要互相打气!", "小橘灵发出软软的叫声。", "今天也一起加油吧!"],
|
|
17
|
+
petCooldown: ["刚摸过啦,让它缓一缓。", "火苗还在兴奋地摇晃。", "小橘灵害羞地偏了偏头。", "稍等,它正在整理呆毛。", "十秒后再来摸摸吧。", "它抱住脑袋,表示有点晕。", "先让尾巴的火苗稳定下来。", "小橘灵眨了眨眼。", "不要急,它一直在这里。", "休息一下再继续互动吧。"],
|
|
18
|
+
feed: ["火苗果真好吃!", "小橘灵满足地拍了拍肚子。", "尾巴的火苗一下子旺了!", "它把火苗果吃得干干净净。", "吃饱以后更有力气啦!", "小橘灵开心地蹦了一下。", "甜甜的火苗果最棒了!", "它眼巴巴地看着下一颗果子。", "能量恢复,准备继续干活!", "小橘灵向你露出大大的笑容。"],
|
|
19
|
+
feedCooldown: ["已经吃饱啦。", "先消化一下火苗果吧。", "圆肚子暂时装不下了。", "稍后再来喂食。", "它满足地拍了拍嘴角。", "火苗果的能量正在发挥。", "先活动活动再吃下一颗。", "尾巴的火苗已经够旺了。", "小橘灵摇头表示还不饿。", "留一点肚子给下一顿吧。"],
|
|
20
|
+
noTreats: ["火苗果吃完了,完成任务就能获得更多。", "没有火苗果了,一起去干活吧!", "小橘灵期待着下一份任务奖励。", "先完成一轮任务,再补充火苗果。", "它闻了闻空空的果篮。", "努力干活就会有新的火苗果。", "小橘灵已经准备好出发了。", "下一颗火苗果很快就来。", "先攒任务奖励吧。", "火苗燃起来了,开始下一轮!"],
|
|
21
|
+
};
|
|
22
|
+
export function builtinPetRemark(kind) {
|
|
23
|
+
return BUILTIN_PET_REMARKS[kind][0];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Round-robin reaction picker over the effective pools (custom overrides
|
|
27
|
+
* replace a slot's built-in pool). Counters are per slot so each slot cycles
|
|
28
|
+
* independently; picks stay deterministic for tests.
|
|
29
|
+
*/
|
|
30
|
+
export class PetRemarkPicker {
|
|
31
|
+
counters = new Map();
|
|
32
|
+
pools;
|
|
33
|
+
constructor(overrides) {
|
|
34
|
+
this.pools = {};
|
|
35
|
+
for (const kind of PET_REMARK_KINDS) {
|
|
36
|
+
const custom = overrides?.[kind];
|
|
37
|
+
this.pools[kind] =
|
|
38
|
+
custom !== undefined && custom.length > 0
|
|
39
|
+
? custom
|
|
40
|
+
: BUILTIN_PET_REMARKS[kind];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
next(kind, counter) {
|
|
44
|
+
const pool = this.pools[kind];
|
|
45
|
+
const index = Math.abs(Math.floor(counter)) % pool.length;
|
|
46
|
+
return pool[index] ?? builtinPetRemark(kind);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pet HTTP routes — mounts /api/operator/v1/pet/* onto the console
|
|
3
|
+
* dispatcher. Sprite assets use the Console Vite public /pets/* path.
|
|
4
|
+
* Adapted from @linxin666/dsh-pet routes.ts.
|
|
5
|
+
* @module loop-agent/console/chat/pet/pet-routes
|
|
6
|
+
*/
|
|
7
|
+
export const PET_API_PREFIX = "/api/operator/v1/pet";
|
|
8
|
+
function sendJson(res, status, body) {
|
|
9
|
+
const payload = JSON.stringify(body);
|
|
10
|
+
res.writeHead(status, {
|
|
11
|
+
"content-type": "application/json; charset=utf-8",
|
|
12
|
+
"cache-control": "no-store",
|
|
13
|
+
});
|
|
14
|
+
res.end(payload);
|
|
15
|
+
}
|
|
16
|
+
async function readBody(req) {
|
|
17
|
+
const chunks = [];
|
|
18
|
+
for await (const chunk of req) {
|
|
19
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
20
|
+
if (chunks.reduce((n, c) => n + c.length, 0) > 1_000_000) {
|
|
21
|
+
throw new Error("body too large");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (chunks.length === 0)
|
|
25
|
+
return {};
|
|
26
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
27
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
28
|
+
throw new Error("body must be an object");
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Try to serve one pet request. Returns true when the pathname was handled.
|
|
34
|
+
* The dispatcher calls this before its fallthrough so pet paths never hit
|
|
35
|
+
* the SPA static handler.
|
|
36
|
+
*/
|
|
37
|
+
export async function handlePetRequest(req, res, pathname, service) {
|
|
38
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
39
|
+
if (!pathname.startsWith(PET_API_PREFIX))
|
|
40
|
+
return false;
|
|
41
|
+
if (service === undefined) {
|
|
42
|
+
sendJson(res, 503, { ok: false, error: { code: "PET_DISABLED", message: "pet service unavailable" } });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// --- JSON API ---
|
|
46
|
+
if (method === "GET" && pathname === PET_API_PREFIX + "/state") {
|
|
47
|
+
sendJson(res, 200, service.view());
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (method === "POST" && pathname === PET_API_PREFIX + "/interact") {
|
|
51
|
+
try {
|
|
52
|
+
const body = await readBody(req);
|
|
53
|
+
const kind = body.kind === "feed" ? "feed" : body.kind === "pet" ? "pet" : undefined;
|
|
54
|
+
if (kind === undefined) {
|
|
55
|
+
sendJson(res, 400, { ok: false, error: { code: "BAD_INTERACTION", message: "kind must be pet or feed" } });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
sendJson(res, 200, service.interact(kind));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
sendJson(res, 400, { ok: false, error: { code: "BAD_REQUEST", message: error instanceof Error ? error.message : String(error) } });
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (method === "POST" && pathname === PET_API_PREFIX + "/display") {
|
|
66
|
+
try {
|
|
67
|
+
const body = await readBody(req);
|
|
68
|
+
const display = service.setDisplay({
|
|
69
|
+
...(typeof body.visible === "boolean" ? { visible: body.visible } : {}),
|
|
70
|
+
...(typeof body.size === "number" ? { size: body.size } : {}),
|
|
71
|
+
...(typeof body.right === "number" ? { right: body.right } : {}),
|
|
72
|
+
...(typeof body.bottom === "number" ? { bottom: body.bottom } : {}),
|
|
73
|
+
});
|
|
74
|
+
sendJson(res, 200, { ok: true, display });
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
sendJson(res, 400, { ok: false, error: { code: "BAD_REQUEST", message: error instanceof Error ? error.message : String(error) } });
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (method === "POST" && pathname === PET_API_PREFIX + "/select") {
|
|
82
|
+
try {
|
|
83
|
+
const body = await readBody(req);
|
|
84
|
+
if (typeof body.petId !== "string")
|
|
85
|
+
throw new Error("petId required");
|
|
86
|
+
const ok = service.selectPet(body.petId);
|
|
87
|
+
if (!ok) {
|
|
88
|
+
sendJson(res, 404, { ok: false, error: { code: "PET_NOT_FOUND", message: "unknown petId" } });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
sendJson(res, 200, service.view());
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
sendJson(res, 400, { ok: false, error: { code: "BAD_REQUEST", message: error instanceof Error ? error.message : String(error) } });
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (method === "POST" && pathname === PET_API_PREFIX + "/rename") {
|
|
99
|
+
try {
|
|
100
|
+
const body = await readBody(req);
|
|
101
|
+
if (typeof body.name !== "string")
|
|
102
|
+
throw new Error("name required");
|
|
103
|
+
sendJson(res, 200, { ok: true, name: service.renamePet(body.name) });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
sendJson(res, 400, { ok: false, error: { code: "BAD_REQUEST", message: error instanceof Error ? error.message : String(error) } });
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (method === "POST" && pathname === PET_API_PREFIX + "/focus") {
|
|
111
|
+
try {
|
|
112
|
+
const body = await readBody(req);
|
|
113
|
+
service.focusSession(typeof body.sessionId === "string" && body.sessionId !== ""
|
|
114
|
+
? body.sessionId
|
|
115
|
+
: undefined);
|
|
116
|
+
sendJson(res, 200, service.view());
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
sendJson(res, 400, { ok: false, error: { code: "BAD_REQUEST", message: error instanceof Error ? error.message : String(error) } });
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
sendJson(res, 404, { ok: false, error: { code: "PET_ROUTE_NOT_FOUND", message: "unknown pet route" } });
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pet service — console orchestration half, adapted from @linxin666/dsh-pet
|
|
3
|
+
* service.ts. Subscribes to the chat event ring, projects turn activity
|
|
4
|
+
* onto the pet state machine, settles the treat ledger lazily, and serves
|
|
5
|
+
* the read/interact API the dock UI calls.
|
|
6
|
+
* @module loop-agent/console/chat/pet/pet-service
|
|
7
|
+
*/
|
|
8
|
+
import { PetStateMachine, } from "./pet-state.js";
|
|
9
|
+
import { applyPetInteraction, applyPetTurnReward, petAffinityViewOf, settlePetTreatGrants, consumePetTreat, } from "./pet-affinity.js";
|
|
10
|
+
import { builtinPetRemark, } from "./pet-remarks.js";
|
|
11
|
+
import { DEFAULT_PET_ID, PET_DISPLAY_INSET_MAX, PET_DISPLAY_SIZE_MAX, PET_DISPLAY_SIZE_MIN, loadPetPersist, savePetPersist, } from "./pet-persist.js";
|
|
12
|
+
const PET_BUBBLE_MAX = 120;
|
|
13
|
+
function bubbleText(value) {
|
|
14
|
+
if (typeof value !== "string")
|
|
15
|
+
return undefined;
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if (trimmed === "")
|
|
18
|
+
return undefined;
|
|
19
|
+
return trimmed.slice(0, PET_BUBBLE_MAX);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* PetService — one instance per workspace context. The active session is the
|
|
23
|
+
* one the operator last interacted with (prompt/switch); its events drive
|
|
24
|
+
* the pet. All state is persisted to <dataDir>/pet.json atomically.
|
|
25
|
+
*/
|
|
26
|
+
export class PetService {
|
|
27
|
+
chatEvents;
|
|
28
|
+
registry;
|
|
29
|
+
dataDir;
|
|
30
|
+
now;
|
|
31
|
+
machine = new PetStateMachine();
|
|
32
|
+
persist;
|
|
33
|
+
unsubscribes = new Set();
|
|
34
|
+
activeSessionId;
|
|
35
|
+
lastInteractionBubble;
|
|
36
|
+
rewardedTurnIds = new Set();
|
|
37
|
+
constructor(chatEvents, registry, dataDir, now = Date.now) {
|
|
38
|
+
this.chatEvents = chatEvents;
|
|
39
|
+
this.registry = registry;
|
|
40
|
+
this.dataDir = dataDir;
|
|
41
|
+
this.now = now;
|
|
42
|
+
this.persist = loadPetPersist(dataDir);
|
|
43
|
+
}
|
|
44
|
+
/** Currently selected pet id, clamped to the registry. */
|
|
45
|
+
selectedPetId() {
|
|
46
|
+
return this.registry.has(this.persist.petId)
|
|
47
|
+
? this.persist.petId
|
|
48
|
+
: (this.registry.keys().next().value ?? DEFAULT_PET_ID);
|
|
49
|
+
}
|
|
50
|
+
petName() {
|
|
51
|
+
const id = this.selectedPetId();
|
|
52
|
+
return this.persist.names[id] ?? this.registry.get(id)?.manifest.displayName ?? id;
|
|
53
|
+
}
|
|
54
|
+
display() {
|
|
55
|
+
return { ...this.persist.display };
|
|
56
|
+
}
|
|
57
|
+
/** Point the pet at a session (the one the operator is watching). */
|
|
58
|
+
focusSession(sessionId) {
|
|
59
|
+
if (this.activeSessionId === sessionId)
|
|
60
|
+
return;
|
|
61
|
+
for (const off of this.unsubscribes)
|
|
62
|
+
off();
|
|
63
|
+
this.unsubscribes.clear();
|
|
64
|
+
this.activeSessionId = sessionId;
|
|
65
|
+
this.rewardedTurnIds.clear();
|
|
66
|
+
if (sessionId === undefined) {
|
|
67
|
+
this.machine.onSessionDisposed();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.machine.onSessionActive();
|
|
71
|
+
// Replay recent history so a refocus lands on the real phase.
|
|
72
|
+
const recent = this.chatEvents.snapshot(sessionId).slice(-40);
|
|
73
|
+
for (const event of recent)
|
|
74
|
+
this.project(event, false);
|
|
75
|
+
const off = this.chatEvents.subscribe(sessionId, (event) => {
|
|
76
|
+
this.project(event);
|
|
77
|
+
});
|
|
78
|
+
this.unsubscribes.add(off);
|
|
79
|
+
}
|
|
80
|
+
/** Project one chat ring event onto the machine (pure dispatch). */
|
|
81
|
+
project(event, reward = true) {
|
|
82
|
+
switch (event.kind) {
|
|
83
|
+
case "agent_start":
|
|
84
|
+
case "step_start": {
|
|
85
|
+
this.machine.onActivity({ phase: "thinking" });
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
case "user_message": {
|
|
89
|
+
this.machine.onActivity({ phase: "waiting" });
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "message_update":
|
|
93
|
+
case "message_end": {
|
|
94
|
+
// Assistant text landing: short ack reads as thinking (more
|
|
95
|
+
// work may follow), a substantive reply reads as review.
|
|
96
|
+
const data = event.data;
|
|
97
|
+
const text = typeof data.text === "string" ? data.text : "";
|
|
98
|
+
this.machine.onActivity({
|
|
99
|
+
phase: text.length > 80 ? "review" : "thinking",
|
|
100
|
+
});
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "tool_call": {
|
|
104
|
+
const data = event.data;
|
|
105
|
+
const toolName = typeof data.toolName === "string" && data.toolName !== ""
|
|
106
|
+
? data.toolName
|
|
107
|
+
: "工具";
|
|
108
|
+
this.machine.onActivity({
|
|
109
|
+
phase: "tool",
|
|
110
|
+
line: `${toolName} 进行中`,
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "tool_result": {
|
|
115
|
+
const data = event.data;
|
|
116
|
+
const failed = data.isError === true;
|
|
117
|
+
const toolName = typeof data.toolName === "string" && data.toolName !== ""
|
|
118
|
+
? data.toolName
|
|
119
|
+
: "工具";
|
|
120
|
+
this.machine.onActivity(failed
|
|
121
|
+
? { phase: "failed", line: `${toolName} 出错了` }
|
|
122
|
+
: { phase: "tool", line: `${toolName} 完成` });
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "agent_end":
|
|
126
|
+
case "agent_settled": {
|
|
127
|
+
// A settled agent turn: reward exactly once even when both lifecycle
|
|
128
|
+
// events are present in the durable ring.
|
|
129
|
+
if (reward && !this.rewardedTurnIds.has(event.turnId)) {
|
|
130
|
+
this.rewardedTurnIds.add(event.turnId);
|
|
131
|
+
this.persist.affinity = applyPetTurnReward(this.persist.affinity);
|
|
132
|
+
this.save();
|
|
133
|
+
}
|
|
134
|
+
this.machine.onActivity({ phase: "done" });
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "turn_error":
|
|
138
|
+
case "turn_max_tokens":
|
|
139
|
+
case "model_retry": {
|
|
140
|
+
this.machine.onActivity({ phase: "failed" });
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
default:
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Full read view (settles treats lazily first). */
|
|
148
|
+
view() {
|
|
149
|
+
const settle = settlePetTreatGrants(this.persist.treats, this.persist.affinity.turns, this.now());
|
|
150
|
+
if (settle.ledger !== this.persist.treats) {
|
|
151
|
+
this.persist.treats = settle.ledger;
|
|
152
|
+
this.save();
|
|
153
|
+
}
|
|
154
|
+
const state = this.machine.render();
|
|
155
|
+
const bubble = this.lastInteractionBubble !== undefined &&
|
|
156
|
+
this.now() - this.lastInteractionBubble.at < 4000
|
|
157
|
+
? this.lastInteractionBubble.text
|
|
158
|
+
: undefined;
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
sessionActive: state.sessionActive,
|
|
162
|
+
state: bubble === undefined ? state : { ...state, bubble },
|
|
163
|
+
affinity: petAffinityViewOf(this.persist.affinity, this.now()),
|
|
164
|
+
treats: this.persist.treats.treats,
|
|
165
|
+
petId: this.selectedPetId(),
|
|
166
|
+
petName: this.petName(),
|
|
167
|
+
display: this.display(),
|
|
168
|
+
pets: [...this.registry.entries()].map(([id, entry]) => ({
|
|
169
|
+
id,
|
|
170
|
+
displayName: entry.manifest.displayName,
|
|
171
|
+
})),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** One user interaction (pet/feed). */
|
|
175
|
+
interact(kind) {
|
|
176
|
+
let reaction;
|
|
177
|
+
let delta = 0;
|
|
178
|
+
let accepted = false;
|
|
179
|
+
if (kind === "feed") {
|
|
180
|
+
const consume = consumePetTreat(this.persist.treats);
|
|
181
|
+
if (!consume.ok) {
|
|
182
|
+
reaction = builtinPetRemark("noTreats");
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
this.persist.treats = consume.ledger;
|
|
186
|
+
const outcome = applyPetInteraction(this.persist.affinity, "feed", this.now());
|
|
187
|
+
this.persist.affinity = outcome.affinity;
|
|
188
|
+
delta = outcome.delta;
|
|
189
|
+
accepted = outcome.accepted;
|
|
190
|
+
reaction = outcome.reaction;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const outcome = applyPetInteraction(this.persist.affinity, "pet", this.now());
|
|
195
|
+
this.persist.affinity = outcome.affinity;
|
|
196
|
+
delta = outcome.delta;
|
|
197
|
+
accepted = outcome.accepted;
|
|
198
|
+
reaction = outcome.reaction;
|
|
199
|
+
}
|
|
200
|
+
this.lastInteractionBubble = { text: reaction, at: this.now() };
|
|
201
|
+
this.save();
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
reaction,
|
|
205
|
+
delta,
|
|
206
|
+
accepted,
|
|
207
|
+
affinity: petAffinityViewOf(this.persist.affinity, this.now()),
|
|
208
|
+
treats: this.persist.treats.treats,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** Update display config (clamped) and persist. */
|
|
212
|
+
setDisplay(patch) {
|
|
213
|
+
const current = this.persist.display;
|
|
214
|
+
const next = {
|
|
215
|
+
visible: typeof patch.visible === "boolean"
|
|
216
|
+
? patch.visible
|
|
217
|
+
: current.visible,
|
|
218
|
+
size: Math.round(Math.min(PET_DISPLAY_SIZE_MAX, Math.max(PET_DISPLAY_SIZE_MIN, typeof patch.size === "number" && Number.isFinite(patch.size)
|
|
219
|
+
? patch.size
|
|
220
|
+
: current.size))),
|
|
221
|
+
right: Math.round(Math.min(PET_DISPLAY_INSET_MAX, Math.max(0, typeof patch.right === "number" && Number.isFinite(patch.right)
|
|
222
|
+
? patch.right
|
|
223
|
+
: current.right))),
|
|
224
|
+
bottom: Math.round(Math.min(PET_DISPLAY_INSET_MAX, Math.max(0, typeof patch.bottom === "number" && Number.isFinite(patch.bottom)
|
|
225
|
+
? patch.bottom
|
|
226
|
+
: current.bottom))),
|
|
227
|
+
};
|
|
228
|
+
this.persist.display = next;
|
|
229
|
+
this.save();
|
|
230
|
+
return { ...next };
|
|
231
|
+
}
|
|
232
|
+
/** Select another pet from the registry. */
|
|
233
|
+
selectPet(petId) {
|
|
234
|
+
if (!this.registry.has(petId))
|
|
235
|
+
return false;
|
|
236
|
+
this.persist.petId = petId;
|
|
237
|
+
this.save();
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
/** Rename the selected pet (persisted per-pet). */
|
|
241
|
+
renamePet(name) {
|
|
242
|
+
const trimmed = name.trim().slice(0, 20);
|
|
243
|
+
if (trimmed === "")
|
|
244
|
+
return this.petName();
|
|
245
|
+
this.persist.names[this.selectedPetId()] = trimmed;
|
|
246
|
+
this.save();
|
|
247
|
+
return trimmed;
|
|
248
|
+
}
|
|
249
|
+
save() {
|
|
250
|
+
try {
|
|
251
|
+
savePetPersist(this.persist, this.dataDir);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
// Persist failures never break the pet runtime.
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
dispose() {
|
|
258
|
+
for (const off of this.unsubscribes)
|
|
259
|
+
off();
|
|
260
|
+
this.unsubscribes.clear();
|
|
261
|
+
}
|
|
262
|
+
}
|