@aghents/gateway 0.2.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/dist/index.d.ts +198 -0
- package/dist/index.js +305 -0
- package/package.json +35 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Result } from 'neverthrow';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { PulseServiceClient, PulseGame, GameStore, PulseError } from '@aghents/pulse';
|
|
4
|
+
export { SCHEDULER_CADENCE_MS } from '@aghents/pulse';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The persona contract. Personas decide from a seat's View ONLY, never GameState; no wall clock
|
|
8
|
+
* and no global RNG anywhere in this package: `nowMs`, `rng` and `fraction` are parameters.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type GatewayError = {
|
|
12
|
+
code: "llm" | "invalid_slug" | "handle_taken" | "db";
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
interface PersonaTiming {
|
|
16
|
+
/** Earliest action time as a fraction of the open window (0..1). */
|
|
17
|
+
minFraction: number;
|
|
18
|
+
/** Latest action time as a fraction of the open window (0..1). */
|
|
19
|
+
maxFraction: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* What a persona decided, and (when it differs from the persona's own tier/model) what produced
|
|
23
|
+
* it: an llm persona's heuristic fallback discloses as heuristic, never as the model it skipped.
|
|
24
|
+
*/
|
|
25
|
+
type Decision<E> = {
|
|
26
|
+
events: E[];
|
|
27
|
+
produced?: {
|
|
28
|
+
tier: "heuristic" | "llm";
|
|
29
|
+
model: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
interface Persona<V, E> {
|
|
33
|
+
personaId: string;
|
|
34
|
+
tier: "heuristic" | "llm";
|
|
35
|
+
model: string;
|
|
36
|
+
timing: PersonaTiming;
|
|
37
|
+
decide(input: {
|
|
38
|
+
view: V;
|
|
39
|
+
rng: () => number;
|
|
40
|
+
fraction: number;
|
|
41
|
+
}): Promise<Decision<E>>;
|
|
42
|
+
}
|
|
43
|
+
declare const LLM_MAX_TOKENS = 400;
|
|
44
|
+
/**
|
|
45
|
+
* An llm-tier persona: prompt from the view, complete, parse the text into events. A completer
|
|
46
|
+
* failure rejects, and the scheduler turns that into a `skipped` seat.
|
|
47
|
+
*/
|
|
48
|
+
declare function llmPersona<V, E>(opts: {
|
|
49
|
+
personaId: string;
|
|
50
|
+
model: string;
|
|
51
|
+
completer: Completer;
|
|
52
|
+
prompt: (view: V) => {
|
|
53
|
+
system: string;
|
|
54
|
+
user: string;
|
|
55
|
+
};
|
|
56
|
+
parse: (text: string, view: V) => E[];
|
|
57
|
+
timing?: PersonaTiming;
|
|
58
|
+
/**
|
|
59
|
+
* Used when the completer errs or `parse` yields nothing, so an llm persona backed by a
|
|
60
|
+
* heuristic never spends a second call on the same pulse. Without it the scheduler marks the
|
|
61
|
+
* seat skipped and asks again next tick.
|
|
62
|
+
*/
|
|
63
|
+
fallback?: (input: {
|
|
64
|
+
view: V;
|
|
65
|
+
rng: () => number;
|
|
66
|
+
fraction: number;
|
|
67
|
+
}) => E[];
|
|
68
|
+
}): Persona<V, E>;
|
|
69
|
+
|
|
70
|
+
interface Completer {
|
|
71
|
+
complete(input: {
|
|
72
|
+
system: string;
|
|
73
|
+
user: string;
|
|
74
|
+
maxTokens: number;
|
|
75
|
+
}): Promise<Result<string, GatewayError>>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
declare const schema: z.ZodObject<{
|
|
79
|
+
ANTHROPIC_API_KEY: z.ZodString;
|
|
80
|
+
AGHENTS_LLM_MODEL: z.ZodDefault<z.ZodString>;
|
|
81
|
+
}, "strip", z.ZodTypeAny, {
|
|
82
|
+
ANTHROPIC_API_KEY: string;
|
|
83
|
+
AGHENTS_LLM_MODEL: string;
|
|
84
|
+
}, {
|
|
85
|
+
ANTHROPIC_API_KEY: string;
|
|
86
|
+
AGHENTS_LLM_MODEL?: string | undefined;
|
|
87
|
+
}>;
|
|
88
|
+
type LlmEnv = z.infer<typeof schema>;
|
|
89
|
+
declare function readLlmEnv(raw: Record<string, string | undefined>): LlmEnv;
|
|
90
|
+
|
|
91
|
+
/** One call's worst case: the SDK timeout times the attempts (one retry). The scheduler reserves this much of its deadline before an llm decision. */
|
|
92
|
+
declare const LLM_TIMEOUT_MS = 20000;
|
|
93
|
+
declare const LLM_MAX_RETRIES = 1;
|
|
94
|
+
declare const LLM_WORST_CASE_MS: number;
|
|
95
|
+
/** Pad for the awaits between the reserve check and the call itself (the budget spend). */
|
|
96
|
+
declare const LLM_PREFLIGHT_MS = 2000;
|
|
97
|
+
declare class AnthropicCompleter implements Completer {
|
|
98
|
+
private readonly client;
|
|
99
|
+
private readonly model;
|
|
100
|
+
constructor(opts: {
|
|
101
|
+
env: LlmEnv;
|
|
102
|
+
fetch?: typeof globalThis.fetch;
|
|
103
|
+
});
|
|
104
|
+
complete(input: {
|
|
105
|
+
system: string;
|
|
106
|
+
user: string;
|
|
107
|
+
maxTokens: number;
|
|
108
|
+
}): Promise<Result<string, GatewayError>>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Scripted replies for tests, consumed in order; records every call. */
|
|
112
|
+
declare class FakeCompleter implements Completer {
|
|
113
|
+
private readonly replies;
|
|
114
|
+
readonly calls: {
|
|
115
|
+
system: string;
|
|
116
|
+
user: string;
|
|
117
|
+
maxTokens: number;
|
|
118
|
+
}[];
|
|
119
|
+
constructor(replies: string[]);
|
|
120
|
+
complete(input: {
|
|
121
|
+
system: string;
|
|
122
|
+
user: string;
|
|
123
|
+
maxTokens: number;
|
|
124
|
+
}): Promise<Result<string, GatewayError>>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Mint an identity.profiles row for a persona. profiles.id references auth.users, so the persona
|
|
129
|
+
* gets an auth user first: no password and an .invalid address, so nothing can ever sign in as
|
|
130
|
+
* it. Idempotent: an existing persona profile row wins; an existing auth user without one is
|
|
131
|
+
* looked up by email and reused. The `persona-` handle namespace is reserved to kind='persona'
|
|
132
|
+
* (0004_persona_handles.sql), so a human row under this handle can only predate 0004; it is
|
|
133
|
+
* never returned as the persona.
|
|
134
|
+
*/
|
|
135
|
+
declare function mintPersonaProfile(serviceClient: PulseServiceClient, input: {
|
|
136
|
+
slug: string;
|
|
137
|
+
displayName: string;
|
|
138
|
+
}): Promise<Result<{
|
|
139
|
+
id: string;
|
|
140
|
+
}, GatewayError>>;
|
|
141
|
+
|
|
142
|
+
type Log = (message: string, context: Record<string, unknown>) => void;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* What must remain of a window after a persona's due time, per tier: one scheduler cadence
|
|
146
|
+
* (so a cron tick lands after it) plus, for an llm seat, the worst case of its call (so the
|
|
147
|
+
* tick that finds it due can still afford the decision before the window closes).
|
|
148
|
+
*/
|
|
149
|
+
declare const LLM_RESERVE_MS: number;
|
|
150
|
+
declare const dueReserveMs: (tier: Persona<unknown, unknown>["tier"]) => number;
|
|
151
|
+
/** The shortest window in which every tier can still act: cadence + the llm worst case. */
|
|
152
|
+
declare const MIN_WINDOW_MS: number;
|
|
153
|
+
/**
|
|
154
|
+
* When a persona is due: its offset into the window, clamped so `dueReserveMs(tier)` remains
|
|
155
|
+
* before the window ends. Otherwise an offset in the window's tail can fall between two cron
|
|
156
|
+
* ticks, the next of which advances the phase (or refuses the llm reserve) before the persona
|
|
157
|
+
* acts. A window shorter than the reserve is due at its start.
|
|
158
|
+
*/
|
|
159
|
+
declare function dueAtMs(windowStartMs: number, windowMs: number, offsetMs: number, tier?: Persona<unknown, unknown>["tier"]): number;
|
|
160
|
+
/**
|
|
161
|
+
* One scheduler pass over one game: catch the clock up, then let every due persona act once per
|
|
162
|
+
* phase, in both the open and the lock window (which events each phase takes is the game's
|
|
163
|
+
* `advance`: Circle claims in open and votes in lock). "Acted this phase" is read off the store:
|
|
164
|
+
* the persona ids on events after the last system (clock) event. Idempotent: a second tick in the
|
|
165
|
+
* same phase acts nobody. A game-level failure (the clock could not be caught up, or the record
|
|
166
|
+
* or log could not be read afterwards) is an err with the pulse code, logged, so a pass never
|
|
167
|
+
* reports an outage as an empty success. `nowMs` is the pass start (offsets, catchUp); `now()`
|
|
168
|
+
* is the live clock for the pre-decide budget, the post-decide window check and the submit
|
|
169
|
+
* time (pure: callers inject it, tests pass a fake). With a `deadline`, due seats not reached
|
|
170
|
+
* before `now() >= deadline.atMs` are returned as `deferred` (the next tick asks them).
|
|
171
|
+
*/
|
|
172
|
+
type TickResult = {
|
|
173
|
+
acted: string[];
|
|
174
|
+
skipped: string[];
|
|
175
|
+
deferred: string[];
|
|
176
|
+
};
|
|
177
|
+
declare function tick<S, E, V>(input: {
|
|
178
|
+
game: PulseGame<S, E, V>;
|
|
179
|
+
store: GameStore<E>;
|
|
180
|
+
personas: Map<string, Persona<V, E>>;
|
|
181
|
+
gameId: string;
|
|
182
|
+
nowMs: number;
|
|
183
|
+
now: () => number;
|
|
184
|
+
deadline?: {
|
|
185
|
+
atMs: number;
|
|
186
|
+
};
|
|
187
|
+
log?: Log;
|
|
188
|
+
}): Promise<Result<TickResult, PulseError>>;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Millisecond offsets within a pulse window at which a persona acts. Ported unchanged from
|
|
192
|
+
* Circle packages/personas/src/timing.ts. Pure: the scheduler owns wall-clock time.
|
|
193
|
+
*/
|
|
194
|
+
declare function actionOffsets(count: number, windowMs: number, profile: {
|
|
195
|
+
timing: PersonaTiming;
|
|
196
|
+
}, rng: () => number): number[];
|
|
197
|
+
|
|
198
|
+
export { AnthropicCompleter, type Completer, type Decision, FakeCompleter, type GatewayError, LLM_MAX_RETRIES, LLM_MAX_TOKENS, LLM_PREFLIGHT_MS, LLM_RESERVE_MS, LLM_TIMEOUT_MS, LLM_WORST_CASE_MS, type LlmEnv, type Log, MIN_WINDOW_MS, type Persona, type PersonaTiming, type TickResult, actionOffsets, dueAtMs, dueReserveMs, llmPersona, mintPersonaProfile, readLlmEnv, tick };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// src/completer.anthropic.ts
|
|
2
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
3
|
+
import { err, ok } from "neverthrow";
|
|
4
|
+
|
|
5
|
+
// src/persona.ts
|
|
6
|
+
var LLM_MAX_TOKENS = 400;
|
|
7
|
+
var DEFAULT_TIMING = { minFraction: 0.1, maxFraction: 0.8 };
|
|
8
|
+
function llmPersona(opts) {
|
|
9
|
+
return {
|
|
10
|
+
personaId: opts.personaId,
|
|
11
|
+
tier: "llm",
|
|
12
|
+
model: opts.model,
|
|
13
|
+
timing: opts.timing ?? DEFAULT_TIMING,
|
|
14
|
+
async decide(input) {
|
|
15
|
+
const { system, user } = opts.prompt(input.view);
|
|
16
|
+
const r = await opts.completer.complete({ system, user, maxTokens: LLM_MAX_TOKENS });
|
|
17
|
+
let events = [];
|
|
18
|
+
if (r.isOk()) {
|
|
19
|
+
try {
|
|
20
|
+
events = opts.parse(r.value, input.view);
|
|
21
|
+
} catch {
|
|
22
|
+
events = [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (events.length > 0) return { events };
|
|
26
|
+
if (opts.fallback) {
|
|
27
|
+
return {
|
|
28
|
+
events: opts.fallback(input),
|
|
29
|
+
produced: { tier: "heuristic", model: "heuristic:fallback" }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (r.isErr()) throw new Error(`${r.error.code}: ${r.error.message}`);
|
|
33
|
+
return { events: [] };
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/completer.anthropic.ts
|
|
39
|
+
var LLM_TIMEOUT_MS = 2e4;
|
|
40
|
+
var LLM_MAX_RETRIES = 1;
|
|
41
|
+
var LLM_WORST_CASE_MS = LLM_TIMEOUT_MS * (LLM_MAX_RETRIES + 1);
|
|
42
|
+
var LLM_PREFLIGHT_MS = 2e3;
|
|
43
|
+
var AnthropicCompleter = class {
|
|
44
|
+
client;
|
|
45
|
+
model;
|
|
46
|
+
constructor(opts) {
|
|
47
|
+
this.model = opts.env.AGHENTS_LLM_MODEL;
|
|
48
|
+
this.client = new Anthropic({
|
|
49
|
+
apiKey: opts.env.ANTHROPIC_API_KEY,
|
|
50
|
+
timeout: LLM_TIMEOUT_MS,
|
|
51
|
+
maxRetries: LLM_MAX_RETRIES,
|
|
52
|
+
...opts.fetch ? { fetch: opts.fetch } : {}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async complete(input) {
|
|
56
|
+
try {
|
|
57
|
+
const res = await this.client.messages.create({
|
|
58
|
+
model: this.model,
|
|
59
|
+
max_tokens: Math.min(input.maxTokens, LLM_MAX_TOKENS),
|
|
60
|
+
system: input.system,
|
|
61
|
+
messages: [{ role: "user", content: input.user }]
|
|
62
|
+
});
|
|
63
|
+
const text = res.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
64
|
+
return ok(text);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return err({ code: "llm", message: e instanceof Error ? e.message : String(e) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/completer.fake.ts
|
|
72
|
+
import { err as err2, ok as ok2 } from "neverthrow";
|
|
73
|
+
var FakeCompleter = class {
|
|
74
|
+
constructor(replies) {
|
|
75
|
+
this.replies = replies;
|
|
76
|
+
}
|
|
77
|
+
replies;
|
|
78
|
+
calls = [];
|
|
79
|
+
async complete(input) {
|
|
80
|
+
this.calls.push(input);
|
|
81
|
+
const reply = this.replies.shift();
|
|
82
|
+
return reply === void 0 ? err2({ code: "llm", message: `no scripted reply for call ${this.calls.length}` }) : ok2(reply);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/env.ts
|
|
87
|
+
import { z } from "zod";
|
|
88
|
+
var schema = z.object({
|
|
89
|
+
ANTHROPIC_API_KEY: z.string().min(1),
|
|
90
|
+
AGHENTS_LLM_MODEL: z.string().min(1).default("claude-sonnet-5")
|
|
91
|
+
});
|
|
92
|
+
function readLlmEnv(raw) {
|
|
93
|
+
return schema.parse(raw);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/profile.ts
|
|
97
|
+
import { err as err3, ok as ok3 } from "neverthrow";
|
|
98
|
+
import { z as z2 } from "zod";
|
|
99
|
+
var slug = z2.string().regex(/^[a-z0-9-]{1,16}$/);
|
|
100
|
+
var personaEmail = (s) => `persona-${s}@personas.aghents.invalid`;
|
|
101
|
+
async function mintPersonaProfile(serviceClient, input) {
|
|
102
|
+
const parsed = slug.safeParse(input.slug);
|
|
103
|
+
if (!parsed.success) {
|
|
104
|
+
return err3({ code: "invalid_slug", message: "slug must match ^[a-z0-9-]{1,16}$" });
|
|
105
|
+
}
|
|
106
|
+
const handle = `persona-${parsed.data}`;
|
|
107
|
+
const identity = serviceClient.schema("identity");
|
|
108
|
+
const existing = await identity.from("profiles").select("id, kind").eq("handle", handle).maybeSingle();
|
|
109
|
+
if (existing.error) return err3({ code: "db", message: existing.error.message });
|
|
110
|
+
if (existing.data?.kind === "persona") return ok3({ id: existing.data.id });
|
|
111
|
+
if (existing.data) {
|
|
112
|
+
return err3({ code: "handle_taken", message: `${handle} belongs to a ${existing.data.kind}` });
|
|
113
|
+
}
|
|
114
|
+
const user = await userId(serviceClient, personaEmail(parsed.data));
|
|
115
|
+
if (user.isErr()) return err3(user.error);
|
|
116
|
+
const { error } = await identity.from("profiles").upsert(
|
|
117
|
+
{ id: user.value, handle, display_name: input.displayName, kind: "persona" },
|
|
118
|
+
{ onConflict: "id" }
|
|
119
|
+
);
|
|
120
|
+
return error ? err3({ code: "db", message: error.message }) : ok3({ id: user.value });
|
|
121
|
+
}
|
|
122
|
+
async function userId(client, email) {
|
|
123
|
+
const created = await client.auth.admin.createUser({
|
|
124
|
+
email,
|
|
125
|
+
email_confirm: true,
|
|
126
|
+
user_metadata: { kind: "persona" },
|
|
127
|
+
app_metadata: { kind: "persona" }
|
|
128
|
+
});
|
|
129
|
+
if (!created.error) return ok3(created.data.user.id);
|
|
130
|
+
if (created.error.code !== "email_exists") {
|
|
131
|
+
return err3({ code: "db", message: created.error.message });
|
|
132
|
+
}
|
|
133
|
+
for (let page = 1; ; page++) {
|
|
134
|
+
const list = await client.auth.admin.listUsers({ page, perPage: 1e3 });
|
|
135
|
+
if (list.error) return err3({ code: "db", message: list.error.message });
|
|
136
|
+
const hit = list.data.users.find((u) => u.email === email);
|
|
137
|
+
if (hit) {
|
|
138
|
+
return hit.app_metadata?.kind === "persona" ? ok3(hit.id) : err3({ code: "handle_taken", message: `${email} is a client-created account` });
|
|
139
|
+
}
|
|
140
|
+
if (list.data.users.length < 1e3) return err3({ code: "db", message: `no user ${email}` });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/scheduler.ts
|
|
145
|
+
import {
|
|
146
|
+
SCHEDULER_CADENCE_MS,
|
|
147
|
+
catchUp,
|
|
148
|
+
createRng,
|
|
149
|
+
getView,
|
|
150
|
+
pulseTimes,
|
|
151
|
+
submit
|
|
152
|
+
} from "@aghents/pulse";
|
|
153
|
+
import { err as err4, ok as ok4 } from "neverthrow";
|
|
154
|
+
|
|
155
|
+
// src/timing.ts
|
|
156
|
+
function actionOffsets(count, windowMs, profile, rng) {
|
|
157
|
+
const lo = profile.timing.minFraction * windowMs;
|
|
158
|
+
const hi = profile.timing.maxFraction * windowMs;
|
|
159
|
+
return Array.from({ length: count }, () => lo + rng() * (hi - lo)).sort((a, b) => a - b);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/scheduler.ts
|
|
163
|
+
var noop = () => {
|
|
164
|
+
};
|
|
165
|
+
var LLM_RESERVE_MS = LLM_WORST_CASE_MS + LLM_PREFLIGHT_MS;
|
|
166
|
+
var dueReserveMs = (tier) => SCHEDULER_CADENCE_MS + (tier === "llm" ? LLM_RESERVE_MS : 0);
|
|
167
|
+
var MIN_WINDOW_MS = dueReserveMs("llm");
|
|
168
|
+
function dueAtMs(windowStartMs, windowMs, offsetMs, tier = "heuristic") {
|
|
169
|
+
const latest = Math.max(windowStartMs, windowStartMs + windowMs - dueReserveMs(tier));
|
|
170
|
+
return Math.min(windowStartMs + offsetMs, latest);
|
|
171
|
+
}
|
|
172
|
+
function actedIds(events) {
|
|
173
|
+
const lastClock = events.map((e) => e.actor.actorKind).lastIndexOf("system");
|
|
174
|
+
return new Set(
|
|
175
|
+
events.slice(lastClock + 1).flatMap((e) => e.actor.actorKind === "persona" ? [e.actor.personaId] : [])
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
async function tick(input) {
|
|
179
|
+
const { game, store, personas, gameId, nowMs, now, deadline } = input;
|
|
180
|
+
const log = input.log ?? noop;
|
|
181
|
+
const acted = [];
|
|
182
|
+
const skipped = [];
|
|
183
|
+
const deferred = [];
|
|
184
|
+
const skip = (seatId, message, context = {}) => {
|
|
185
|
+
skipped.push(seatId);
|
|
186
|
+
log(message, { gameId, seatId, ...context });
|
|
187
|
+
};
|
|
188
|
+
const fail = (what, e) => {
|
|
189
|
+
log(`${what}: ${e.message}`, { gameId, code: e.code });
|
|
190
|
+
return err4(e);
|
|
191
|
+
};
|
|
192
|
+
const state = await catchUp(game, store, gameId, nowMs);
|
|
193
|
+
if (state.isErr()) return fail("catchUp", state.error);
|
|
194
|
+
if (!state.value) return ok4({ acted, skipped, deferred });
|
|
195
|
+
const phase = game.phaseOf(state.value);
|
|
196
|
+
if (phase === "complete") return ok4({ acted, skipped, deferred });
|
|
197
|
+
const [record, events] = await Promise.all([store.getGame(gameId), store.listEvents(gameId)]);
|
|
198
|
+
if (record.isErr()) return fail("getGame", record.error);
|
|
199
|
+
if (events.isErr()) return fail("listEvents", events.error);
|
|
200
|
+
if (!record.value) return fail("getGame", { code: "store", message: "game vanished" });
|
|
201
|
+
const pulse = game.pulseOf(state.value);
|
|
202
|
+
const t = pulseTimes(record.value.startedAtMs, record.value.timescale, pulse);
|
|
203
|
+
const windowStartMs = phase === "open" ? t.openAtMs : t.lockAtMs;
|
|
204
|
+
const windowEndMs = phase === "open" ? t.lockAtMs : t.resolveAtMs;
|
|
205
|
+
const windowMs = windowEndMs - windowStartMs;
|
|
206
|
+
const actedThisPhase = actedIds(events.value);
|
|
207
|
+
const actedMeanwhile = async (personaId) => (await store.listEvents(gameId)).map((es) => actedIds(es).has(personaId)).unwrapOr(true);
|
|
208
|
+
for (const [seatId, persona] of personas) {
|
|
209
|
+
const seat = record.value.seats.find((s) => s.seatId === seatId);
|
|
210
|
+
if (seat?.kind !== "persona") {
|
|
211
|
+
skip(seatId, "not a persona seat", { reason: "not_persona_seat" });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (seat.personaProfileId !== persona.personaId) {
|
|
215
|
+
skip(seatId, "persona does not match the seat", { reason: "persona_mismatch" });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (actedThisPhase.has(persona.personaId)) continue;
|
|
219
|
+
const seed = `${record.value.seed}:${seatId}:${pulse}:${phase}`;
|
|
220
|
+
const [offset = 0] = actionOffsets(1, windowMs, persona, createRng(`${seed}:when`));
|
|
221
|
+
if (nowMs < dueAtMs(windowStartMs, windowMs, offset, persona.tier)) continue;
|
|
222
|
+
const budgetEndMs = Math.min(deadline?.atMs ?? Number.POSITIVE_INFINITY, windowEndMs);
|
|
223
|
+
const view = await getView(game, store, { gameId, seatId, nowMs });
|
|
224
|
+
if (view.isErr()) {
|
|
225
|
+
skip(seatId, `getView: ${view.error.message}`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const reserveMs = persona.tier === "llm" ? LLM_RESERVE_MS : 0;
|
|
229
|
+
if (budgetEndMs - now() < Math.max(reserveMs, 1)) {
|
|
230
|
+
deferred.push(seatId);
|
|
231
|
+
log("no budget for the decision; deferring the seat", { gameId, seatId, reserveMs });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (persona.tier === "llm") {
|
|
235
|
+
const spent = await store.spendLlmCall(gameId);
|
|
236
|
+
if (spent.isErr()) {
|
|
237
|
+
skip(seatId, `spendLlmCall: ${spent.error.code}`, { code: spent.error.code });
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let decided;
|
|
242
|
+
try {
|
|
243
|
+
decided = await persona.decide({
|
|
244
|
+
view: view.value.view,
|
|
245
|
+
rng: createRng(`${seed}:decide`),
|
|
246
|
+
fraction: (nowMs - windowStartMs) / windowMs
|
|
247
|
+
});
|
|
248
|
+
} catch (e) {
|
|
249
|
+
skip(seatId, `decide: ${e instanceof Error ? e.message : String(e)}`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (decided.events.length === 0) {
|
|
253
|
+
skip(seatId, "decide returned no events", { reason: "no_events" });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const actor = {
|
|
257
|
+
actorKind: "persona",
|
|
258
|
+
personaId: persona.personaId,
|
|
259
|
+
tier: decided.produced?.tier ?? persona.tier,
|
|
260
|
+
model: decided.produced?.model ?? persona.model
|
|
261
|
+
};
|
|
262
|
+
let landed = 0;
|
|
263
|
+
for (const event of decided.events) {
|
|
264
|
+
if (landed === 0 && await actedMeanwhile(persona.personaId)) {
|
|
265
|
+
skip(seatId, "acted meanwhile", { reason: "acted_meanwhile" });
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
const at = now();
|
|
269
|
+
if (at >= budgetEndMs) {
|
|
270
|
+
skip(seatId, at >= windowEndMs ? "window closed" : "deadline passed", {
|
|
271
|
+
reason: at >= windowEndMs ? "window_closed" : "deadline",
|
|
272
|
+
landed
|
|
273
|
+
});
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
const r = await submit(game, store, { gameId, seatId, event, nowMs: at, actor });
|
|
277
|
+
if (r.isErr()) {
|
|
278
|
+
skip(seatId, `submit: ${r.error.code}`, { code: r.error.code, message: r.error.message });
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
landed++;
|
|
282
|
+
}
|
|
283
|
+
if (landed > 0 && landed === decided.events.length) acted.push(seatId);
|
|
284
|
+
}
|
|
285
|
+
return ok4({ acted, skipped, deferred });
|
|
286
|
+
}
|
|
287
|
+
export {
|
|
288
|
+
AnthropicCompleter,
|
|
289
|
+
FakeCompleter,
|
|
290
|
+
LLM_MAX_RETRIES,
|
|
291
|
+
LLM_MAX_TOKENS,
|
|
292
|
+
LLM_PREFLIGHT_MS,
|
|
293
|
+
LLM_RESERVE_MS,
|
|
294
|
+
LLM_TIMEOUT_MS,
|
|
295
|
+
LLM_WORST_CASE_MS,
|
|
296
|
+
MIN_WINDOW_MS,
|
|
297
|
+
SCHEDULER_CADENCE_MS,
|
|
298
|
+
actionOffsets,
|
|
299
|
+
dueAtMs,
|
|
300
|
+
dueReserveMs,
|
|
301
|
+
llmPersona,
|
|
302
|
+
mintPersonaProfile,
|
|
303
|
+
readLlmEnv,
|
|
304
|
+
tick
|
|
305
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aghents/gateway",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@anthropic-ai/sdk": "^0.125.0",
|
|
22
|
+
"neverthrow": "^8.1.1",
|
|
23
|
+
"zod": "^3.24.1",
|
|
24
|
+
"@aghents/pulse": "0.2.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.9.3",
|
|
28
|
+
"vitest": "^2.1.9"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
32
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
33
|
+
"test": "vitest run"
|
|
34
|
+
}
|
|
35
|
+
}
|