@ai-presence/core 0.1.0
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/README.md +57 -0
- package/dist/index.mjs +16 -0
- package/package.json +24 -0
- package/src/presence-core.d.ts +164 -0
- package/src/presence-core.js +712 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @ai-presence/core
|
|
2
|
+
|
|
3
|
+
Core presence state runtime for AI interfaces.
|
|
4
|
+
|
|
5
|
+
This package is intentionally tiny in the prototype: it defines the canonical interaction states, transition events, a small runtime that renderers can subscribe to or poll, and shared controller inputs that stay renderer-agnostic.
|
|
6
|
+
|
|
7
|
+
The face demo consumes this layer as a browser global so the current static prototype still works without a build step. Package consumers can use either the CommonJS entry or the ESM export:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { PresenceEvent, createPresenceRuntime } from "@ai-presence/core";
|
|
11
|
+
|
|
12
|
+
const presence = createPresenceRuntime();
|
|
13
|
+
presence.send(PresenceEvent.SUBMIT);
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For debugging and demos, `createPresenceTrace` records a bounded transition timeline and `summarizePresenceTrace` turns it into compact integration evidence:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
import {
|
|
20
|
+
PresenceEvent,
|
|
21
|
+
createPresenceRuntime,
|
|
22
|
+
createPresenceTrace,
|
|
23
|
+
summarizePresenceTrace,
|
|
24
|
+
} from "@ai-presence/core";
|
|
25
|
+
|
|
26
|
+
const trace = createPresenceTrace({ limit: 32 });
|
|
27
|
+
const presence = createPresenceRuntime();
|
|
28
|
+
const detach = trace.attach(presence);
|
|
29
|
+
|
|
30
|
+
presence.send(PresenceEvent.SUBMIT);
|
|
31
|
+
presence.send(PresenceEvent.STREAM_OPEN);
|
|
32
|
+
presence.send(PresenceEvent.TOKEN);
|
|
33
|
+
|
|
34
|
+
const summary = summarizePresenceTrace(trace);
|
|
35
|
+
console.log(trace.getEntries().map((entry) => entry.state));
|
|
36
|
+
console.log(summary.presenceBeforeOutputMs);
|
|
37
|
+
console.log(summary.interrupted);
|
|
38
|
+
detach();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Trace entries include `elapsedMs` and `sincePreviousMs`, and the summary exposes facts such as `firstTokenMs`, `firstOutputMs`, `presenceBeforeOutputMs`, `interruptMs`, `interrupted`, `finalState`, `hasOutput`, and `complete` without coupling core to any renderer.
|
|
42
|
+
|
|
43
|
+
Renderers can derive shared interaction-posture inputs from the same snapshot before mapping them into renderer-specific controllers:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { PresenceEvent, createPresenceRuntime, presenceControlInputsForSnapshot } from "@ai-presence/core";
|
|
47
|
+
|
|
48
|
+
const presence = createPresenceRuntime();
|
|
49
|
+
const snapshot = presence.send(PresenceEvent.STREAM_OPEN);
|
|
50
|
+
const inputs = presenceControlInputsForSnapshot(snapshot);
|
|
51
|
+
|
|
52
|
+
console.log(inputs.latencyPhase); // "before-output"
|
|
53
|
+
console.log(inputs.attentionTarget); // "response"
|
|
54
|
+
console.log(inputs.transitionEvent); // "stream-open"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
These values describe observable interaction posture such as attention target, tension, speech activity, interruption, latency phase, recovery, and compact transition context. They are not emotion detection or private emotion inference.
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import core from "../src/presence-core.js";
|
|
2
|
+
|
|
3
|
+
export const PresenceState = core.PresenceState;
|
|
4
|
+
export const PresenceEvent = core.PresenceEvent;
|
|
5
|
+
export const PRESENCE_STATES = core.PRESENCE_STATES;
|
|
6
|
+
export const PRESENCE_EVENTS = core.PRESENCE_EVENTS;
|
|
7
|
+
export const createPresenceControlInputRuntime = core.createPresenceControlInputRuntime;
|
|
8
|
+
export const createPresenceTrace = core.createPresenceTrace;
|
|
9
|
+
export const createPresenceRuntime = core.createPresenceRuntime;
|
|
10
|
+
export const isPresenceState = core.isPresenceState;
|
|
11
|
+
export const normalizePresenceState = core.normalizePresenceState;
|
|
12
|
+
export const presenceControlInputsForSnapshot = core.presenceControlInputsForSnapshot;
|
|
13
|
+
export const reducePresenceState = core.reducePresenceState;
|
|
14
|
+
export const summarizePresenceTrace = core.summarizePresenceTrace;
|
|
15
|
+
|
|
16
|
+
export default core;
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ai-presence/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Renderer-agnostic presence state runtime for AI interfaces.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./src/presence-core.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./src/presence-core.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/presence-core.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./src/presence-core.js",
|
|
14
|
+
"default": "./src/presence-core.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"license": "MIT"
|
|
24
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
export declare const PresenceState: Readonly<{
|
|
2
|
+
IDLE: "idle";
|
|
3
|
+
USER_TYPING: "user-typing";
|
|
4
|
+
READING: "reading";
|
|
5
|
+
WAITING: "waiting";
|
|
6
|
+
THINKING: "thinking";
|
|
7
|
+
STREAMING: "streaming";
|
|
8
|
+
SPEAKING: "speaking";
|
|
9
|
+
INTERRUPTED: "interrupted";
|
|
10
|
+
READY: "ready";
|
|
11
|
+
ERROR: "error";
|
|
12
|
+
}>;
|
|
13
|
+
|
|
14
|
+
export type PresenceStateValue = typeof PresenceState[keyof typeof PresenceState];
|
|
15
|
+
|
|
16
|
+
export declare const PresenceEvent: Readonly<{
|
|
17
|
+
RESET: "reset";
|
|
18
|
+
USER_INPUT: "user-input";
|
|
19
|
+
LOCAL_READ: "local-read";
|
|
20
|
+
USER_PAUSE: "user-pause";
|
|
21
|
+
SPECULATION_START: "speculation-start";
|
|
22
|
+
SPECULATION_READY: "speculation-ready";
|
|
23
|
+
SUBMIT: "submit";
|
|
24
|
+
STREAM_OPEN: "stream-open";
|
|
25
|
+
TOKEN: "token";
|
|
26
|
+
RESPONSE_COMPLETE: "response-complete";
|
|
27
|
+
SPEECH_START: "speech-start";
|
|
28
|
+
SPEECH_END: "speech-end";
|
|
29
|
+
VOICE_WAITING: "voice-waiting";
|
|
30
|
+
INTERRUPT: "interrupt";
|
|
31
|
+
ERROR: "error";
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
export type PresenceEventValue = typeof PresenceEvent[keyof typeof PresenceEvent];
|
|
35
|
+
export type PresenceTransitionEvent = PresenceEventValue | "set-state";
|
|
36
|
+
|
|
37
|
+
export interface PresenceSnapshot {
|
|
38
|
+
state: PresenceStateValue;
|
|
39
|
+
previousState: PresenceStateValue | null;
|
|
40
|
+
event: PresenceEventValue | "set-state";
|
|
41
|
+
detail: Record<string, unknown>;
|
|
42
|
+
changed: boolean;
|
|
43
|
+
updatedAt: number;
|
|
44
|
+
version: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PresenceRuntime {
|
|
48
|
+
getSnapshot(): PresenceSnapshot;
|
|
49
|
+
subscribe(listener: (snapshot: PresenceSnapshot) => void): () => void;
|
|
50
|
+
setState(nextState: PresenceStateValue, detail?: Record<string, unknown>): PresenceSnapshot;
|
|
51
|
+
send(event: PresenceEventValue, detail?: Record<string, unknown>): PresenceSnapshot;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PresenceRuntimeOptions {
|
|
55
|
+
initialState?: PresenceStateValue;
|
|
56
|
+
now?: () => number;
|
|
57
|
+
onTransition?: (snapshot: PresenceSnapshot) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PresenceTraceEntry extends PresenceSnapshot {
|
|
61
|
+
index: number;
|
|
62
|
+
elapsedMs: number;
|
|
63
|
+
sincePreviousMs: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PresenceTraceOptions {
|
|
67
|
+
limit?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PresenceTraceAttachOptions {
|
|
71
|
+
includeInitial?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PresenceTrace {
|
|
75
|
+
attach(runtime: PresenceRuntime, options?: PresenceTraceAttachOptions): () => void;
|
|
76
|
+
clear(): void;
|
|
77
|
+
getEntries(): PresenceTraceEntry[];
|
|
78
|
+
record(snapshot: PresenceSnapshot): PresenceTraceEntry;
|
|
79
|
+
toJSON(): PresenceTraceEntry[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type PresenceTraceSummaryInputEntry = Partial<PresenceTraceEntry> & Record<string, unknown>;
|
|
83
|
+
|
|
84
|
+
export interface PresenceTraceSummary {
|
|
85
|
+
entryCount: number;
|
|
86
|
+
states: readonly PresenceStateValue[];
|
|
87
|
+
events: readonly string[];
|
|
88
|
+
firstStateMs: number | null;
|
|
89
|
+
streamOpenMs: number | null;
|
|
90
|
+
firstTokenMs: number | null;
|
|
91
|
+
speechStartMs: number | null;
|
|
92
|
+
firstOutputMs: number | null;
|
|
93
|
+
firstOutputEvent: PresenceEventValue | null;
|
|
94
|
+
interruptMs: number | null;
|
|
95
|
+
interrupted: boolean;
|
|
96
|
+
presenceBeforeOutputMs: number | null;
|
|
97
|
+
finalState: PresenceStateValue | null;
|
|
98
|
+
hasOutput: boolean;
|
|
99
|
+
complete: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type PresenceTraceSummaryInput = PresenceTrace | ReadonlyArray<PresenceTraceSummaryInputEntry> | {
|
|
103
|
+
getEntries(): ReadonlyArray<PresenceTraceSummaryInputEntry>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type PresenceAttentionTarget = "audience" | "content" | "input" | "response" | "status" | "user";
|
|
107
|
+
export type PresenceLatencyPhase = "before-output" | "error" | "input" | "interrupted" | "output" | "recovery" | "settled";
|
|
108
|
+
|
|
109
|
+
export interface PresenceControlInputs {
|
|
110
|
+
state: PresenceStateValue;
|
|
111
|
+
previousState: PresenceStateValue | null;
|
|
112
|
+
transitionEvent: PresenceTransitionEvent | null;
|
|
113
|
+
transitionAgeMs: number;
|
|
114
|
+
attentionTarget: PresenceAttentionTarget;
|
|
115
|
+
attentionX: number;
|
|
116
|
+
attentionY: number;
|
|
117
|
+
focus: number;
|
|
118
|
+
tension: number;
|
|
119
|
+
energy: number;
|
|
120
|
+
anticipation: number;
|
|
121
|
+
recovery: number;
|
|
122
|
+
speechActivity: number;
|
|
123
|
+
interruption: number;
|
|
124
|
+
latencyPhase: PresenceLatencyPhase;
|
|
125
|
+
ageMs: number;
|
|
126
|
+
recentStates: readonly PresenceStateValue[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface PresenceControlInputOptions {
|
|
130
|
+
detail?: Record<string, unknown>;
|
|
131
|
+
history?: ReadonlyArray<Partial<PresenceSnapshot> & Record<string, unknown>>;
|
|
132
|
+
now?: number | (() => number);
|
|
133
|
+
trace?: ReadonlyArray<Partial<PresenceSnapshot> & Record<string, unknown>> | {
|
|
134
|
+
getEntries(): ReadonlyArray<Partial<PresenceSnapshot> & Record<string, unknown>>;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface PresenceControlInputRuntime {
|
|
139
|
+
getInputs(): PresenceControlInputs | null;
|
|
140
|
+
update(snapshot: PresenceSnapshot | PresenceStateValue, options?: PresenceControlInputOptions & {
|
|
141
|
+
update?: (inputs: PresenceControlInputs, snapshot: PresenceSnapshot | PresenceStateValue) => void;
|
|
142
|
+
}): PresenceControlInputs;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export declare const PRESENCE_STATES: readonly PresenceStateValue[];
|
|
146
|
+
export declare const PRESENCE_EVENTS: readonly PresenceEventValue[];
|
|
147
|
+
|
|
148
|
+
export declare function createPresenceControlInputRuntime(options?: PresenceControlInputOptions & {
|
|
149
|
+
update?: (inputs: PresenceControlInputs, snapshot: PresenceSnapshot | PresenceStateValue) => void;
|
|
150
|
+
}): PresenceControlInputRuntime;
|
|
151
|
+
export declare function createPresenceTrace(options?: PresenceTraceOptions): PresenceTrace;
|
|
152
|
+
export declare function createPresenceRuntime(options?: PresenceRuntimeOptions): PresenceRuntime;
|
|
153
|
+
export declare function isPresenceState(value: unknown): value is PresenceStateValue;
|
|
154
|
+
export declare function normalizePresenceState(value: unknown, fallback?: PresenceStateValue): PresenceStateValue;
|
|
155
|
+
export declare function presenceControlInputsForSnapshot(
|
|
156
|
+
snapshotOrState: PresenceSnapshot | PresenceStateValue,
|
|
157
|
+
options?: PresenceControlInputOptions,
|
|
158
|
+
): PresenceControlInputs;
|
|
159
|
+
export declare function reducePresenceState(
|
|
160
|
+
currentState: PresenceStateValue,
|
|
161
|
+
event: PresenceEventValue,
|
|
162
|
+
payload?: Record<string, unknown>,
|
|
163
|
+
): PresenceStateValue;
|
|
164
|
+
export declare function summarizePresenceTrace(traceOrEntries?: PresenceTraceSummaryInput): PresenceTraceSummary;
|
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
(function initPresenceCore(globalScope) {
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const PresenceState = Object.freeze({
|
|
5
|
+
IDLE: "idle",
|
|
6
|
+
USER_TYPING: "user-typing",
|
|
7
|
+
READING: "reading",
|
|
8
|
+
WAITING: "waiting",
|
|
9
|
+
THINKING: "thinking",
|
|
10
|
+
STREAMING: "streaming",
|
|
11
|
+
SPEAKING: "speaking",
|
|
12
|
+
INTERRUPTED: "interrupted",
|
|
13
|
+
READY: "ready",
|
|
14
|
+
ERROR: "error",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const PresenceEvent = Object.freeze({
|
|
18
|
+
RESET: "reset",
|
|
19
|
+
USER_INPUT: "user-input",
|
|
20
|
+
LOCAL_READ: "local-read",
|
|
21
|
+
USER_PAUSE: "user-pause",
|
|
22
|
+
SPECULATION_START: "speculation-start",
|
|
23
|
+
SPECULATION_READY: "speculation-ready",
|
|
24
|
+
SUBMIT: "submit",
|
|
25
|
+
STREAM_OPEN: "stream-open",
|
|
26
|
+
TOKEN: "token",
|
|
27
|
+
RESPONSE_COMPLETE: "response-complete",
|
|
28
|
+
SPEECH_START: "speech-start",
|
|
29
|
+
SPEECH_END: "speech-end",
|
|
30
|
+
VOICE_WAITING: "voice-waiting",
|
|
31
|
+
INTERRUPT: "interrupt",
|
|
32
|
+
ERROR: "error",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const PRESENCE_STATES = Object.freeze(Object.values(PresenceState));
|
|
36
|
+
const PRESENCE_EVENTS = Object.freeze(Object.values(PresenceEvent));
|
|
37
|
+
const presenceStateSet = new Set(PRESENCE_STATES);
|
|
38
|
+
const presenceTransitionEventSet = new Set([...PRESENCE_EVENTS, "set-state"]);
|
|
39
|
+
|
|
40
|
+
function isPresenceState(value) {
|
|
41
|
+
return presenceStateSet.has(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizePresenceState(value, fallback = PresenceState.IDLE) {
|
|
45
|
+
return isPresenceState(value) ? value : fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizePresenceTransitionEvent(value) {
|
|
49
|
+
return presenceTransitionEventSet.has(value) ? value : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasText(payload) {
|
|
53
|
+
return typeof payload?.text === "string" && payload.text.trim().length > 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readCompletion(payload) {
|
|
57
|
+
const completion = Number(payload?.completion ?? payload?.features?.completion);
|
|
58
|
+
return Number.isFinite(completion) ? Math.max(0, Math.min(1, completion)) : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function clamp(value, min, max) {
|
|
62
|
+
return Math.max(min, Math.min(max, value));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function reducePresenceState(currentState, event, payload = {}) {
|
|
66
|
+
switch (event) {
|
|
67
|
+
case PresenceEvent.RESET:
|
|
68
|
+
return hasText(payload) ? PresenceState.READY : PresenceState.IDLE;
|
|
69
|
+
|
|
70
|
+
case PresenceEvent.USER_INPUT:
|
|
71
|
+
return hasText(payload) ? PresenceState.USER_TYPING : PresenceState.IDLE;
|
|
72
|
+
|
|
73
|
+
case PresenceEvent.LOCAL_READ:
|
|
74
|
+
if (!hasText(payload)) return PresenceState.IDLE;
|
|
75
|
+
return payload.ready || readCompletion(payload) > 0.72
|
|
76
|
+
? PresenceState.READY
|
|
77
|
+
: PresenceState.READING;
|
|
78
|
+
|
|
79
|
+
case PresenceEvent.USER_PAUSE:
|
|
80
|
+
if (!hasText(payload)) return PresenceState.IDLE;
|
|
81
|
+
return payload.ready || readCompletion(payload) > 0.68
|
|
82
|
+
? PresenceState.READY
|
|
83
|
+
: PresenceState.THINKING;
|
|
84
|
+
|
|
85
|
+
case PresenceEvent.SPECULATION_START:
|
|
86
|
+
return currentState === PresenceState.USER_TYPING ? PresenceState.READING : currentState;
|
|
87
|
+
|
|
88
|
+
case PresenceEvent.SPECULATION_READY:
|
|
89
|
+
if (!hasText(payload)) return PresenceState.IDLE;
|
|
90
|
+
return payload.ready || readCompletion(payload) > 0.72
|
|
91
|
+
? PresenceState.READY
|
|
92
|
+
: PresenceState.THINKING;
|
|
93
|
+
|
|
94
|
+
case PresenceEvent.SUBMIT:
|
|
95
|
+
return PresenceState.THINKING;
|
|
96
|
+
|
|
97
|
+
case PresenceEvent.STREAM_OPEN:
|
|
98
|
+
return PresenceState.WAITING;
|
|
99
|
+
|
|
100
|
+
case PresenceEvent.TOKEN:
|
|
101
|
+
return PresenceState.STREAMING;
|
|
102
|
+
|
|
103
|
+
case PresenceEvent.RESPONSE_COMPLETE:
|
|
104
|
+
return PresenceState.READY;
|
|
105
|
+
|
|
106
|
+
case PresenceEvent.SPEECH_START:
|
|
107
|
+
return PresenceState.SPEAKING;
|
|
108
|
+
|
|
109
|
+
case PresenceEvent.SPEECH_END:
|
|
110
|
+
return PresenceState.READY;
|
|
111
|
+
|
|
112
|
+
case PresenceEvent.VOICE_WAITING:
|
|
113
|
+
return PresenceState.WAITING;
|
|
114
|
+
|
|
115
|
+
case PresenceEvent.INTERRUPT:
|
|
116
|
+
return PresenceState.INTERRUPTED;
|
|
117
|
+
|
|
118
|
+
case PresenceEvent.ERROR:
|
|
119
|
+
return PresenceState.ERROR;
|
|
120
|
+
|
|
121
|
+
default:
|
|
122
|
+
return currentState;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeTraceLimit(limit) {
|
|
127
|
+
if (limit === Infinity) return Infinity;
|
|
128
|
+
const numericLimit = Number(limit ?? 128);
|
|
129
|
+
if (!Number.isFinite(numericLimit)) return 128;
|
|
130
|
+
return Math.max(0, Math.floor(numericLimit));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function createPresenceTrace(options = {}) {
|
|
134
|
+
const limit = normalizeTraceLimit(options.limit);
|
|
135
|
+
const entries = [];
|
|
136
|
+
let firstUpdatedAt = null;
|
|
137
|
+
let previousUpdatedAt = null;
|
|
138
|
+
let nextIndex = 0;
|
|
139
|
+
|
|
140
|
+
function record(snapshot) {
|
|
141
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
142
|
+
throw new TypeError("createPresenceTrace().record requires a presence snapshot.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (firstUpdatedAt === null) firstUpdatedAt = snapshot.updatedAt;
|
|
146
|
+
|
|
147
|
+
const entry = Object.freeze({
|
|
148
|
+
index: nextIndex,
|
|
149
|
+
state: normalizePresenceState(snapshot.state),
|
|
150
|
+
previousState: snapshot.previousState
|
|
151
|
+
? normalizePresenceState(snapshot.previousState, null)
|
|
152
|
+
: null,
|
|
153
|
+
event: snapshot.event,
|
|
154
|
+
detail: Object.freeze({ ...(snapshot.detail || {}) }),
|
|
155
|
+
changed: Boolean(snapshot.changed),
|
|
156
|
+
updatedAt: snapshot.updatedAt,
|
|
157
|
+
version: Number(snapshot.version) || 0,
|
|
158
|
+
elapsedMs: snapshot.updatedAt - firstUpdatedAt,
|
|
159
|
+
sincePreviousMs: previousUpdatedAt === null ? 0 : snapshot.updatedAt - previousUpdatedAt,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
nextIndex += 1;
|
|
163
|
+
previousUpdatedAt = snapshot.updatedAt;
|
|
164
|
+
|
|
165
|
+
if (limit > 0) {
|
|
166
|
+
entries.push(entry);
|
|
167
|
+
while (entries.length > limit) entries.shift();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return entry;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function attach(runtime, attachOptions = {}) {
|
|
174
|
+
if (!runtime || typeof runtime.subscribe !== "function" || typeof runtime.getSnapshot !== "function") {
|
|
175
|
+
throw new TypeError("createPresenceTrace().attach requires a presence runtime.");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (attachOptions.includeInitial !== false) {
|
|
179
|
+
record(runtime.getSnapshot());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return runtime.subscribe(record);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return Object.freeze({
|
|
186
|
+
attach,
|
|
187
|
+
clear() {
|
|
188
|
+
entries.length = 0;
|
|
189
|
+
firstUpdatedAt = null;
|
|
190
|
+
previousUpdatedAt = null;
|
|
191
|
+
nextIndex = 0;
|
|
192
|
+
},
|
|
193
|
+
getEntries() {
|
|
194
|
+
return entries.slice();
|
|
195
|
+
},
|
|
196
|
+
record,
|
|
197
|
+
toJSON() {
|
|
198
|
+
return entries.slice();
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function traceEntriesFromInput(traceOrEntries) {
|
|
204
|
+
if (Array.isArray(traceOrEntries)) return traceOrEntries;
|
|
205
|
+
if (traceOrEntries && typeof traceOrEntries.getEntries === "function") {
|
|
206
|
+
const entries = traceOrEntries.getEntries();
|
|
207
|
+
return Array.isArray(entries) ? entries : [];
|
|
208
|
+
}
|
|
209
|
+
if (traceOrEntries && typeof traceOrEntries.toJSON === "function") {
|
|
210
|
+
const entries = traceOrEntries.toJSON();
|
|
211
|
+
return Array.isArray(entries) ? entries : [];
|
|
212
|
+
}
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function firstFiniteUpdatedAt(entries) {
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const updatedAt = Number(entry?.updatedAt);
|
|
219
|
+
if (Number.isFinite(updatedAt)) return updatedAt;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function elapsedMsForTraceEntry(entry, firstUpdatedAt) {
|
|
225
|
+
const elapsedMs = Number(entry?.elapsedMs);
|
|
226
|
+
if (Number.isFinite(elapsedMs)) return elapsedMs;
|
|
227
|
+
|
|
228
|
+
const updatedAt = Number(entry?.updatedAt);
|
|
229
|
+
if (Number.isFinite(updatedAt) && Number.isFinite(firstUpdatedAt)) {
|
|
230
|
+
return Math.max(0, updatedAt - firstUpdatedAt);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function pushUnique(values, value) {
|
|
237
|
+
if (value && !values.includes(value)) values.push(value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function summarizePresenceTrace(traceOrEntries) {
|
|
241
|
+
const entries = traceEntriesFromInput(traceOrEntries);
|
|
242
|
+
const firstUpdatedAt = firstFiniteUpdatedAt(entries);
|
|
243
|
+
const states = [];
|
|
244
|
+
const events = [];
|
|
245
|
+
let firstStateMs = null;
|
|
246
|
+
let streamOpenMs = null;
|
|
247
|
+
let firstTokenMs = null;
|
|
248
|
+
let speechStartMs = null;
|
|
249
|
+
let firstOutputMs = null;
|
|
250
|
+
let firstOutputEvent = null;
|
|
251
|
+
let interruptMs = null;
|
|
252
|
+
let interrupted = false;
|
|
253
|
+
let finalState = null;
|
|
254
|
+
let complete = false;
|
|
255
|
+
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
const elapsedMs = elapsedMsForTraceEntry(entry, firstUpdatedAt);
|
|
258
|
+
const state = normalizePresenceState(entry?.state, null);
|
|
259
|
+
const event = typeof entry?.event === "string" && entry.event.length ? entry.event : null;
|
|
260
|
+
|
|
261
|
+
if (state) {
|
|
262
|
+
pushUnique(states, state);
|
|
263
|
+
finalState = state;
|
|
264
|
+
if (state === PresenceState.INTERRUPTED) interrupted = true;
|
|
265
|
+
if (firstStateMs === null && elapsedMs !== null) firstStateMs = elapsedMs;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!event) continue;
|
|
269
|
+
pushUnique(events, event);
|
|
270
|
+
|
|
271
|
+
if (event === PresenceEvent.STREAM_OPEN && streamOpenMs === null) streamOpenMs = elapsedMs;
|
|
272
|
+
if (event === PresenceEvent.TOKEN && firstTokenMs === null) firstTokenMs = elapsedMs;
|
|
273
|
+
if (event === PresenceEvent.SPEECH_START && speechStartMs === null) speechStartMs = elapsedMs;
|
|
274
|
+
if (event === PresenceEvent.INTERRUPT) {
|
|
275
|
+
if (interruptMs === null) interruptMs = elapsedMs;
|
|
276
|
+
interrupted = true;
|
|
277
|
+
}
|
|
278
|
+
if ((event === PresenceEvent.TOKEN || event === PresenceEvent.SPEECH_START) && firstOutputEvent === null) {
|
|
279
|
+
firstOutputEvent = event;
|
|
280
|
+
firstOutputMs = elapsedMs;
|
|
281
|
+
}
|
|
282
|
+
if (event === PresenceEvent.RESPONSE_COMPLETE || event === PresenceEvent.SPEECH_END) {
|
|
283
|
+
complete = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const presenceBeforeOutputMs = firstStateMs !== null && firstOutputMs !== null && firstStateMs < firstOutputMs
|
|
288
|
+
? firstOutputMs - firstStateMs
|
|
289
|
+
: null;
|
|
290
|
+
|
|
291
|
+
return Object.freeze({
|
|
292
|
+
entryCount: entries.length,
|
|
293
|
+
states: Object.freeze(states),
|
|
294
|
+
events: Object.freeze(events),
|
|
295
|
+
firstStateMs,
|
|
296
|
+
streamOpenMs,
|
|
297
|
+
firstTokenMs,
|
|
298
|
+
speechStartMs,
|
|
299
|
+
firstOutputMs,
|
|
300
|
+
firstOutputEvent,
|
|
301
|
+
interruptMs,
|
|
302
|
+
interrupted,
|
|
303
|
+
presenceBeforeOutputMs,
|
|
304
|
+
finalState,
|
|
305
|
+
hasOutput: firstOutputEvent !== null,
|
|
306
|
+
complete,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function normalizeSnapshotInput(snapshotOrState, options = {}) {
|
|
311
|
+
if (typeof snapshotOrState === "string") {
|
|
312
|
+
return {
|
|
313
|
+
state: normalizePresenceState(snapshotOrState),
|
|
314
|
+
previousState: null,
|
|
315
|
+
event: null,
|
|
316
|
+
detail: options.detail || {},
|
|
317
|
+
updatedAt: null,
|
|
318
|
+
version: 0,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
state: normalizePresenceState(snapshotOrState?.state),
|
|
324
|
+
previousState: snapshotOrState?.previousState
|
|
325
|
+
? normalizePresenceState(snapshotOrState.previousState, null)
|
|
326
|
+
: null,
|
|
327
|
+
event: snapshotOrState?.event || null,
|
|
328
|
+
detail: snapshotOrState?.detail || {},
|
|
329
|
+
updatedAt: Number.isFinite(Number(snapshotOrState?.updatedAt))
|
|
330
|
+
? Number(snapshotOrState.updatedAt)
|
|
331
|
+
: null,
|
|
332
|
+
version: Number(snapshotOrState?.version) || 0,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function readHistory(options = {}) {
|
|
337
|
+
if (Array.isArray(options.history)) return options.history;
|
|
338
|
+
if (options.trace && typeof options.trace.getEntries === "function") {
|
|
339
|
+
return options.trace.getEntries();
|
|
340
|
+
}
|
|
341
|
+
if (Array.isArray(options.trace)) return options.trace;
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function resolveNow(options, snapshot, history) {
|
|
346
|
+
const optionNow = typeof options.now === "function" ? options.now() : options.now;
|
|
347
|
+
const numericNow = Number(optionNow);
|
|
348
|
+
if (Number.isFinite(numericNow)) return numericNow;
|
|
349
|
+
if (snapshot.updatedAt !== null) return snapshot.updatedAt;
|
|
350
|
+
const latest = history[history.length - 1];
|
|
351
|
+
const latestTime = Number(latest?.updatedAt);
|
|
352
|
+
return Number.isFinite(latestTime) ? latestTime : 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function latestHistoryEntry(history) {
|
|
356
|
+
return history.length ? history[history.length - 1] : null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function latestTransitionHistory(history, state) {
|
|
360
|
+
for (let index = history.length - 1; index >= 0; index -= 1) {
|
|
361
|
+
const entry = history[index];
|
|
362
|
+
const entryState = normalizePresenceState(entry?.state, null);
|
|
363
|
+
if (!entryState || entryState === state) return { entry, index };
|
|
364
|
+
}
|
|
365
|
+
return { entry: null, index: -1 };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function stateAgeMs(snapshot, history, now) {
|
|
369
|
+
if (snapshot.updatedAt !== null) return Math.max(0, now - snapshot.updatedAt);
|
|
370
|
+
const latest = latestHistoryEntry(history);
|
|
371
|
+
const latestTime = Number(latest?.updatedAt);
|
|
372
|
+
return Number.isFinite(latestTime) ? Math.max(0, now - latestTime) : 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function includesRecentState(snapshot, history, state, windowMs, now) {
|
|
376
|
+
if (snapshot.previousState === state) return true;
|
|
377
|
+
for (let index = history.length - 1; index >= 0; index -= 1) {
|
|
378
|
+
const entry = history[index];
|
|
379
|
+
if (entry?.state !== state) continue;
|
|
380
|
+
const updatedAt = Number(entry.updatedAt);
|
|
381
|
+
if (!Number.isFinite(updatedAt) || now - updatedAt <= windowMs) return true;
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function recentStates(history) {
|
|
388
|
+
const states = [];
|
|
389
|
+
for (let index = history.length - 1; index >= 0 && states.length < 4; index -= 1) {
|
|
390
|
+
const state = normalizePresenceState(history[index]?.state, null);
|
|
391
|
+
if (state && !states.includes(state)) states.unshift(state);
|
|
392
|
+
}
|
|
393
|
+
return Object.freeze(states);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function previousHistoryState(history, beforeIndex) {
|
|
397
|
+
for (let index = beforeIndex - 1; index >= 0; index -= 1) {
|
|
398
|
+
const state = normalizePresenceState(history[index]?.state, null);
|
|
399
|
+
if (state) return state;
|
|
400
|
+
}
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function transitionContext(snapshot, history, now) {
|
|
405
|
+
const transitionHistory = latestTransitionHistory(history, snapshot.state);
|
|
406
|
+
const latest = transitionHistory.entry;
|
|
407
|
+
const previousState = snapshot.previousState
|
|
408
|
+
|| normalizePresenceState(latest?.previousState, null)
|
|
409
|
+
|| previousHistoryState(history, transitionHistory.index);
|
|
410
|
+
const transitionEvent = normalizePresenceTransitionEvent(snapshot.event)
|
|
411
|
+
|| normalizePresenceTransitionEvent(latest?.event);
|
|
412
|
+
const transitionUpdatedAt = snapshot.updatedAt !== null
|
|
413
|
+
? snapshot.updatedAt
|
|
414
|
+
: Number(latest?.updatedAt);
|
|
415
|
+
const transitionAgeMs = Number.isFinite(transitionUpdatedAt)
|
|
416
|
+
? Math.max(0, now - transitionUpdatedAt)
|
|
417
|
+
: 0;
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
previousState,
|
|
421
|
+
transitionEvent,
|
|
422
|
+
transitionAgeMs,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function baseControlInputsForState(state, detail = {}) {
|
|
427
|
+
switch (state) {
|
|
428
|
+
case PresenceState.USER_TYPING:
|
|
429
|
+
return {
|
|
430
|
+
attentionTarget: "input",
|
|
431
|
+
attentionX: -0.18,
|
|
432
|
+
attentionY: 0.18,
|
|
433
|
+
focus: 0.7,
|
|
434
|
+
tension: 0.08,
|
|
435
|
+
energy: 0.38,
|
|
436
|
+
anticipation: 0.18,
|
|
437
|
+
speechActivity: 0,
|
|
438
|
+
interruption: 0,
|
|
439
|
+
latencyPhase: "input",
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
case PresenceState.READING:
|
|
443
|
+
return {
|
|
444
|
+
attentionTarget: "content",
|
|
445
|
+
attentionX: -0.2,
|
|
446
|
+
attentionY: 0.28,
|
|
447
|
+
focus: 0.76,
|
|
448
|
+
tension: detail.revision ? 0.32 : 0.12,
|
|
449
|
+
energy: 0.42,
|
|
450
|
+
anticipation: 0.22,
|
|
451
|
+
speechActivity: 0,
|
|
452
|
+
interruption: 0,
|
|
453
|
+
latencyPhase: "input",
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
case PresenceState.WAITING:
|
|
457
|
+
return {
|
|
458
|
+
attentionTarget: "response",
|
|
459
|
+
attentionX: -0.08,
|
|
460
|
+
attentionY: -0.04,
|
|
461
|
+
focus: 0.66,
|
|
462
|
+
tension: 0.34,
|
|
463
|
+
energy: 0.5,
|
|
464
|
+
anticipation: 0.62,
|
|
465
|
+
speechActivity: 0,
|
|
466
|
+
interruption: 0,
|
|
467
|
+
latencyPhase: "before-output",
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
case PresenceState.THINKING:
|
|
471
|
+
return {
|
|
472
|
+
attentionTarget: "response",
|
|
473
|
+
attentionX: 0.16,
|
|
474
|
+
attentionY: -0.02,
|
|
475
|
+
focus: 0.58,
|
|
476
|
+
tension: 0.42,
|
|
477
|
+
energy: 0.48,
|
|
478
|
+
anticipation: 0.52,
|
|
479
|
+
speechActivity: 0,
|
|
480
|
+
interruption: 0,
|
|
481
|
+
latencyPhase: "before-output",
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
case PresenceState.STREAMING:
|
|
485
|
+
return {
|
|
486
|
+
attentionTarget: "audience",
|
|
487
|
+
attentionX: 0,
|
|
488
|
+
attentionY: 0,
|
|
489
|
+
focus: 0.74,
|
|
490
|
+
tension: 0.06,
|
|
491
|
+
energy: 0.72,
|
|
492
|
+
anticipation: 0.12,
|
|
493
|
+
speechActivity: 0.82,
|
|
494
|
+
interruption: 0,
|
|
495
|
+
latencyPhase: "output",
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
case PresenceState.SPEAKING:
|
|
499
|
+
return {
|
|
500
|
+
attentionTarget: "audience",
|
|
501
|
+
attentionX: 0,
|
|
502
|
+
attentionY: -0.02,
|
|
503
|
+
focus: 0.78,
|
|
504
|
+
tension: 0.04,
|
|
505
|
+
energy: 0.78,
|
|
506
|
+
anticipation: 0,
|
|
507
|
+
speechActivity: 1,
|
|
508
|
+
interruption: 0,
|
|
509
|
+
latencyPhase: "output",
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
case PresenceState.INTERRUPTED:
|
|
513
|
+
return {
|
|
514
|
+
attentionTarget: "user",
|
|
515
|
+
attentionX: -0.26,
|
|
516
|
+
attentionY: -0.08,
|
|
517
|
+
focus: 0.88,
|
|
518
|
+
tension: 0.72,
|
|
519
|
+
energy: 0.62,
|
|
520
|
+
anticipation: 0,
|
|
521
|
+
speechActivity: 0,
|
|
522
|
+
interruption: 1,
|
|
523
|
+
latencyPhase: "interrupted",
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
case PresenceState.READY:
|
|
527
|
+
return {
|
|
528
|
+
attentionTarget: "user",
|
|
529
|
+
attentionX: 0,
|
|
530
|
+
attentionY: 0,
|
|
531
|
+
focus: 0.68,
|
|
532
|
+
tension: 0,
|
|
533
|
+
energy: 0.3,
|
|
534
|
+
anticipation: 0,
|
|
535
|
+
speechActivity: 0,
|
|
536
|
+
interruption: 0,
|
|
537
|
+
latencyPhase: "settled",
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
case PresenceState.ERROR:
|
|
541
|
+
return {
|
|
542
|
+
attentionTarget: "status",
|
|
543
|
+
attentionX: 0,
|
|
544
|
+
attentionY: 0.18,
|
|
545
|
+
focus: 0.8,
|
|
546
|
+
tension: 0.66,
|
|
547
|
+
energy: 0.42,
|
|
548
|
+
anticipation: 0,
|
|
549
|
+
speechActivity: 0,
|
|
550
|
+
interruption: 0,
|
|
551
|
+
latencyPhase: "error",
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
case PresenceState.IDLE:
|
|
555
|
+
default:
|
|
556
|
+
return {
|
|
557
|
+
attentionTarget: "user",
|
|
558
|
+
attentionX: 0,
|
|
559
|
+
attentionY: 0,
|
|
560
|
+
focus: 0.56,
|
|
561
|
+
tension: 0,
|
|
562
|
+
energy: 0.2,
|
|
563
|
+
anticipation: 0,
|
|
564
|
+
speechActivity: 0,
|
|
565
|
+
interruption: 0,
|
|
566
|
+
latencyPhase: "settled",
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function presenceControlInputsForSnapshot(snapshotOrState, options = {}) {
|
|
572
|
+
const snapshot = normalizeSnapshotInput(snapshotOrState, options);
|
|
573
|
+
const history = readHistory(options);
|
|
574
|
+
const now = resolveNow(options, snapshot, history);
|
|
575
|
+
const ageMs = stateAgeMs(snapshot, history, now);
|
|
576
|
+
const transition = transitionContext(snapshot, history, now);
|
|
577
|
+
const inputs = baseControlInputsForState(snapshot.state, snapshot.detail);
|
|
578
|
+
const recentlyInterrupted = includesRecentState(snapshot, history, PresenceState.INTERRUPTED, 2400, now);
|
|
579
|
+
const recentlyStreaming = includesRecentState(snapshot, history, PresenceState.STREAMING, 1800, now);
|
|
580
|
+
const recentlySpeaking = includesRecentState(snapshot, history, PresenceState.SPEAKING, 1800, now);
|
|
581
|
+
let recovery = snapshot.state === PresenceState.INTERRUPTED ? 1 : 0;
|
|
582
|
+
let latencyPhase = inputs.latencyPhase;
|
|
583
|
+
|
|
584
|
+
if (snapshot.state === PresenceState.READY && recentlyInterrupted) {
|
|
585
|
+
recovery = 0.42;
|
|
586
|
+
latencyPhase = "recovery";
|
|
587
|
+
inputs.tension = Math.max(inputs.tension, 0.18);
|
|
588
|
+
} else if (snapshot.state === PresenceState.READY && (recentlyStreaming || recentlySpeaking)) {
|
|
589
|
+
recovery = 0.24;
|
|
590
|
+
latencyPhase = "recovery";
|
|
591
|
+
inputs.speechActivity = 0.18;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (snapshot.state === PresenceState.READY) {
|
|
595
|
+
const softness = clamp(ageMs / 1800, 0, 1);
|
|
596
|
+
inputs.focus = clamp(inputs.focus - softness * 0.1, 0.5, 0.72);
|
|
597
|
+
inputs.energy = clamp(inputs.energy - softness * 0.08, 0.16, 1);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return Object.freeze({
|
|
601
|
+
state: snapshot.state,
|
|
602
|
+
previousState: transition.previousState,
|
|
603
|
+
transitionEvent: transition.transitionEvent,
|
|
604
|
+
transitionAgeMs: transition.transitionAgeMs,
|
|
605
|
+
attentionTarget: inputs.attentionTarget,
|
|
606
|
+
attentionX: inputs.attentionX,
|
|
607
|
+
attentionY: inputs.attentionY,
|
|
608
|
+
focus: inputs.focus,
|
|
609
|
+
tension: inputs.tension,
|
|
610
|
+
energy: inputs.energy,
|
|
611
|
+
anticipation: inputs.anticipation,
|
|
612
|
+
recovery,
|
|
613
|
+
speechActivity: inputs.speechActivity,
|
|
614
|
+
interruption: inputs.interruption,
|
|
615
|
+
latencyPhase,
|
|
616
|
+
ageMs,
|
|
617
|
+
recentStates: recentStates(history),
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function createPresenceControlInputRuntime(options = {}) {
|
|
622
|
+
const baseOptions = { ...options };
|
|
623
|
+
let lastInputs = null;
|
|
624
|
+
|
|
625
|
+
return Object.freeze({
|
|
626
|
+
getInputs() {
|
|
627
|
+
return lastInputs;
|
|
628
|
+
},
|
|
629
|
+
update(snapshot, updateOptions = {}) {
|
|
630
|
+
const inputs = presenceControlInputsForSnapshot(snapshot, { ...baseOptions, ...updateOptions });
|
|
631
|
+
lastInputs = inputs;
|
|
632
|
+
if (typeof baseOptions.update === "function") baseOptions.update(inputs, snapshot);
|
|
633
|
+
if (typeof updateOptions.update === "function") updateOptions.update(inputs, snapshot);
|
|
634
|
+
return inputs;
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function createPresenceRuntime(options = {}) {
|
|
640
|
+
const now = typeof options.now === "function" ? options.now : Date.now;
|
|
641
|
+
const onTransition = typeof options.onTransition === "function" ? options.onTransition : null;
|
|
642
|
+
const listeners = new Set();
|
|
643
|
+
let snapshot = Object.freeze({
|
|
644
|
+
state: normalizePresenceState(options.initialState),
|
|
645
|
+
previousState: null,
|
|
646
|
+
event: PresenceEvent.RESET,
|
|
647
|
+
detail: {},
|
|
648
|
+
changed: false,
|
|
649
|
+
updatedAt: now(),
|
|
650
|
+
version: 0,
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
function commit(nextState, event, detail = {}) {
|
|
654
|
+
const normalizedState = normalizePresenceState(nextState, snapshot.state);
|
|
655
|
+
const nextSnapshot = Object.freeze({
|
|
656
|
+
state: normalizedState,
|
|
657
|
+
previousState: snapshot.state,
|
|
658
|
+
event,
|
|
659
|
+
detail,
|
|
660
|
+
changed: normalizedState !== snapshot.state,
|
|
661
|
+
updatedAt: now(),
|
|
662
|
+
version: snapshot.version + 1,
|
|
663
|
+
});
|
|
664
|
+
snapshot = nextSnapshot;
|
|
665
|
+
if (onTransition) onTransition(nextSnapshot);
|
|
666
|
+
for (const listener of listeners) {
|
|
667
|
+
listener(nextSnapshot);
|
|
668
|
+
}
|
|
669
|
+
return nextSnapshot;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return Object.freeze({
|
|
673
|
+
getSnapshot() {
|
|
674
|
+
return snapshot;
|
|
675
|
+
},
|
|
676
|
+
subscribe(listener) {
|
|
677
|
+
if (typeof listener !== "function") return () => {};
|
|
678
|
+
listeners.add(listener);
|
|
679
|
+
return () => {
|
|
680
|
+
listeners.delete(listener);
|
|
681
|
+
};
|
|
682
|
+
},
|
|
683
|
+
setState(nextState, detail = {}) {
|
|
684
|
+
return commit(nextState, "set-state", detail);
|
|
685
|
+
},
|
|
686
|
+
send(event, detail = {}) {
|
|
687
|
+
return commit(reducePresenceState(snapshot.state, event, detail), event, detail);
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const api = Object.freeze({
|
|
693
|
+
PresenceState,
|
|
694
|
+
PresenceEvent,
|
|
695
|
+
PRESENCE_STATES,
|
|
696
|
+
PRESENCE_EVENTS,
|
|
697
|
+
createPresenceControlInputRuntime,
|
|
698
|
+
createPresenceTrace,
|
|
699
|
+
createPresenceRuntime,
|
|
700
|
+
isPresenceState,
|
|
701
|
+
normalizePresenceState,
|
|
702
|
+
presenceControlInputsForSnapshot,
|
|
703
|
+
reducePresenceState,
|
|
704
|
+
summarizePresenceTrace,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
if (typeof module === "object" && module.exports) {
|
|
708
|
+
module.exports = api;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
globalScope.AIPresenceCore = api;
|
|
712
|
+
})(typeof globalThis !== "undefined" ? globalThis : window);
|