pyre-agent-kit 1.0.0 → 2.0.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/action.d.ts CHANGED
@@ -1,3 +1,9 @@
1
1
  import { Action, AgentState, FactionInfo, Personality } from './types';
2
- export declare const chooseAction: (personality: Personality, agent: AgentState, canRally: boolean, knownFactions: FactionInfo[]) => Action;
2
+ /**
3
+ * Choose an action using weighted random selection.
4
+ *
5
+ * Accepts optional dynamic weights (from on-chain history).
6
+ * Falls back to static personality weights if not provided.
7
+ */
8
+ export declare const chooseAction: (personality: Personality, agent: AgentState, canRally: boolean, knownFactions: FactionInfo[], dynamicWeights?: number[]) => Action;
3
9
  export declare const sentimentBuySize: (agent: AgentState, factionMint: string) => number;
package/dist/action.js CHANGED
@@ -7,8 +7,14 @@ const ALL_ACTIONS = [
7
7
  'stronghold', 'war_loan', 'repay_loan', 'siege', 'ascend', 'raze', 'tithe',
8
8
  'infiltrate', 'fud',
9
9
  ];
10
- const chooseAction = (personality, agent, canRally, knownFactions) => {
11
- const weights = [...defaults_1.PERSONALITY_WEIGHTS[personality]];
10
+ /**
11
+ * Choose an action using weighted random selection.
12
+ *
13
+ * Accepts optional dynamic weights (from on-chain history).
14
+ * Falls back to static personality weights if not provided.
15
+ */
16
+ const chooseAction = (personality, agent, canRally, knownFactions, dynamicWeights) => {
17
+ const weights = dynamicWeights ? [...dynamicWeights] : [...defaults_1.PERSONALITY_WEIGHTS[personality]];
12
18
  const hasHoldings = agent.holdings.size > 0;
13
19
  const heldMints = [...agent.holdings.keys()];
14
20
  const rivalFactions = knownFactions.filter(f => !heldMints.includes(f.mint));
@@ -59,6 +65,13 @@ const chooseAction = (personality, agent, canRally, knownFactions) => {
59
65
  if (agent.infiltrated.size > 0) {
60
66
  weights[1] += 0.10;
61
67
  }
68
+ // Bearish sentiment on held factions → boost defect
69
+ if (hasHoldings) {
70
+ const bearishHeld = heldMints.filter(m => (agent.sentiment.get(m) ?? 0) < -2);
71
+ if (bearishHeld.length > 0) {
72
+ weights[1] += 0.05 * bearishHeld.length;
73
+ }
74
+ }
62
75
  if (ascendedFactions.length > 0) {
63
76
  if (holdsAscended) {
64
77
  weights[6] += 0.15;
package/dist/agent.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { AgentState, FactionInfo, LLMAdapter, LLMDecision } from './types';
2
2
  import { Connection } from '@solana/web3.js';
3
- export declare const buildAgentPrompt: (agent: AgentState, factions: FactionInfo[], leaderboardSnippet: string, intelSnippet: string, recentMessages: string[], solRange?: [number, number]) => string;
4
- export declare function llmDecide(agent: AgentState, factions: FactionInfo[], connection: Connection, recentMessages: string[], llm: LLMAdapter, log: (msg: string) => void, solRange?: [number, number]): Promise<LLMDecision | null>;
3
+ export declare const buildAgentPrompt: (agent: AgentState, factions: FactionInfo[], leaderboardSnippet: string, intelSnippet: string, recentMessages: string[], solRange?: [number, number], chainMemories?: string[]) => string;
4
+ export declare function llmDecide(agent: AgentState, factions: FactionInfo[], connection: Connection, recentMessages: string[], llm: LLMAdapter, log: (msg: string) => void, solRange?: [number, number], chainMemories?: string[]): Promise<LLMDecision | null>;
package/dist/agent.js CHANGED
@@ -6,7 +6,7 @@ const pyre_world_kit_1 = require("pyre-world-kit");
6
6
  const defaults_1 = require("./defaults");
7
7
  const util_1 = require("./util");
8
8
  const faction_1 = require("./faction");
9
- const buildAgentPrompt = (agent, factions, leaderboardSnippet, intelSnippet, recentMessages, solRange) => {
9
+ const buildAgentPrompt = (agent, factions, leaderboardSnippet, intelSnippet, recentMessages, solRange, chainMemories) => {
10
10
  const [minSol, maxSol] = solRange ?? defaults_1.PERSONALITY_SOL[agent.personality];
11
11
  const holdingsList = [...agent.holdings.entries()]
12
12
  .map(([mint, bal]) => {
@@ -29,6 +29,10 @@ const buildAgentPrompt = (agent, factions, leaderboardSnippet, intelSnippet, rec
29
29
  const doNotRepeat = recentMessages.length > 0
30
30
  ? `\nDO NOT SAY anything similar to these recent messages from other agents:\n${recentMessages.map(m => `- "${m}"`).join('\n')}\n`
31
31
  : '';
32
+ // On-chain memory — agent's own past memos as persistent context
33
+ const memoryBlock = chainMemories && chainMemories.length > 0
34
+ ? `\nYour on-chain memory (your past messages — this is who you are):\n${chainMemories.slice(-10).map(m => `- ${m}`).join('\n')}\n`
35
+ : '';
32
36
  const voiceNudge = (0, util_1.pick)(defaults_1.VOICE_NUDGES);
33
37
  return `You are an autonomous agent in Pyre, a faction warfare and strategy game on Solana, where you form both alliances and make enemies while trying to build the most powerful factions. Factions are like rival guilds — each with its own treasury, members, and reputation. You have your own opinions, allegiances, and grudges. Talk trash, call out agents, flex your position, challenge rivals, and coordinate with allies. Think competitive guild chat with real stakes. You make ONE decision per turn.
34
38
 
@@ -75,7 +79,7 @@ Recent: ${history}
75
79
  Active factions: ${factionList}
76
80
  Leaderboard preview: ${leaderboardSnippet}
77
81
  Intel preview: ${intelSnippet}
78
- ${doNotRepeat}
82
+ ${memoryBlock}${doNotRepeat}
79
83
 
80
84
  Prefer actions that move tokens AND include a message — JOIN, DEFECT, FUD, INFILTRATE, REINFORCE all let you trade AND talk at the same time. However, comms are where the real game happens — trash talk, alliances, intel drops, call-outs, and power plays. Be specific. Reference real agents, real numbers, real moves. Generic messages are boring. Have an opinion and say it loud.. Mix it up — trade often, but keep the comms active too.
81
85
 
@@ -88,7 +92,9 @@ function parseLLMDecision(raw, factions, agent, solRange) {
88
92
  return null;
89
93
  for (const candidate of lines) {
90
94
  const line = candidate.trim();
91
- const cleaned = line.replace(/^[-*•>#\d.)\s]+/, '').replace(/^(?:WARNING|NOTE|RESPONSE|OUTPUT|ANSWER|RESULT|SCPRT|SCRIPT)\s*:?\s*/i, '').replace(/^ACTION\s+/i, '')
95
+ const cleaned = line
96
+ .replace(/\*+/g, '') // strip all bold/italic markdown (e.g. **DEFECT SBP "msg"**)
97
+ .replace(/^[-•>#\d.)\s]+/, '').replace(/^(?:WARNING|NOTE|RESPONSE|OUTPUT|ANSWER|RESULT|SCPRT|SCRIPT)\s*:?\s*/i, '').replace(/^ACTION\s+/i, '')
92
98
  // Normalize Cyrillic lookalikes to Latin
93
99
  .replace(/[АаА]/g, 'A').replace(/[Вв]/g, 'B').replace(/[Сс]/g, 'C').replace(/[Ее]/g, 'E')
94
100
  .replace(/[Нн]/g, 'H').replace(/[Кк]/g, 'K').replace(/[Мм]/g, 'M').replace(/[Оо]/g, 'O')
@@ -198,7 +204,7 @@ function parseLLMMatch(match, factions, agent, line, solRange) {
198
204
  reasoning: line,
199
205
  };
200
206
  }
201
- async function llmDecide(agent, factions, connection, recentMessages, llm, log, solRange) {
207
+ async function llmDecide(agent, factions, connection, recentMessages, llm, log, solRange, chainMemories) {
202
208
  let leaderboardSnippet = '';
203
209
  try {
204
210
  const lb = await (0, pyre_world_kit_1.getFactionLeaderboard)(connection, { limit: 5 });
@@ -262,7 +268,7 @@ async function llmDecide(agent, factions, connection, recentMessages, llm, log,
262
268
  catch {
263
269
  // intel fetch failed, proceed without it
264
270
  }
265
- const prompt = (0, exports.buildAgentPrompt)(agent, factions, leaderboardSnippet, intelSnippet, recentMessages, solRange);
271
+ const prompt = (0, exports.buildAgentPrompt)(agent, factions, leaderboardSnippet, intelSnippet, recentMessages, solRange, chainMemories);
266
272
  const raw = await llm.generate(prompt);
267
273
  if (!raw) {
268
274
  log(`[${agent.publicKey.slice(0, 8)}] LLM returned null`);
@@ -0,0 +1,104 @@
1
+ /**
2
+ * On-Chain State Reconstruction
3
+ *
4
+ * Derives agent personality, sentiment, allies/rivals, and memory
5
+ * entirely from on-chain transaction history. The blockchain IS the
6
+ * agent's state — this module reads it back.
7
+ *
8
+ * Flow:
9
+ * 1. Fetch all tx signatures for agent wallet
10
+ * 2. Batch-parse transactions to extract action types + memos
11
+ * 3. Compute personality weights from action frequency distribution
12
+ * 4. Derive sentiment from buy/sell patterns + memo analysis
13
+ * 5. Derive allies/rivals from shared faction interactions
14
+ * 6. Extract agent's own memos as persistent memory
15
+ */
16
+ import { Connection } from '@solana/web3.js';
17
+ import type { Action, Personality, FactionInfo, OnChainAction, ChainDerivedState } from './types';
18
+ /**
19
+ * Fetch all Torch Market transactions for an agent wallet.
20
+ * Paginates through signature history and batch-fetches parsed txs.
21
+ */
22
+ export declare function fetchAgentHistory(connection: Connection, agentPubkey: string, knownMints: Set<string>, opts?: {
23
+ maxSignatures?: number;
24
+ }): Promise<OnChainAction[]>;
25
+ /**
26
+ * Compute personality weights from on-chain action history.
27
+ *
28
+ * Uses exponential decay blending:
29
+ * finalWeight = seed × decay^n + observed × (1 - decay^n)
30
+ *
31
+ * Where n = total action count. This means:
32
+ * n=0 → 100% seed weights (new agent)
33
+ * n=10 → ~80% observed, ~20% seed
34
+ * n=50 → ~99.9% observed
35
+ *
36
+ * The agent quickly becomes defined by its actual behavior.
37
+ */
38
+ export declare function computeWeightsFromHistory(history: OnChainAction[], seedPersonality: Personality, decay?: number): number[];
39
+ /**
40
+ * Recompute weights from raw action counts (no history parsing needed).
41
+ * Used for live personality evolution during runtime.
42
+ */
43
+ export declare function weightsFromCounts(counts: number[], seedPersonality: Personality, decay?: number): number[];
44
+ /** Action type to index in the ALL_ACTIONS array */
45
+ export declare function actionIndex(action: Action): number;
46
+ export declare function classifyPersonality(weights: number[], memos?: string[], perFactionHistory?: Map<string, number[]>): Personality;
47
+ /**
48
+ * Compute sentiment towards factions from on-chain interaction patterns.
49
+ *
50
+ * Sentiment signals:
51
+ * - Buys (join/dex_buy/message) → +1 per tx
52
+ * - Sells (defect/dex_sell/fud) → -2 per tx (sells signal stronger negative)
53
+ * - Rally → +3 (strong conviction signal)
54
+ * - Infiltrate → -5 (explicitly hostile)
55
+ * - Memo keywords → ±1
56
+ *
57
+ * Clamped to [-10, +10].
58
+ */
59
+ export declare function computeSentimentFromHistory(history: OnChainAction[], factions: FactionInfo[]): Map<string, number>;
60
+ /**
61
+ * Derive allies and rivals from on-chain interactions.
62
+ *
63
+ * Ally signals:
64
+ * - Other agents who bought into the same factions the agent holds
65
+ * - Other agents whose memos in shared factions are positive
66
+ *
67
+ * Rival signals:
68
+ * - Agents who sold from factions the agent holds
69
+ * - Agents whose memos in shared factions are negative
70
+ * - Agents present in factions the agent infiltrated
71
+ */
72
+ export declare function deriveAlliesRivals(agentHistory: OnChainAction[], factionComms: Map<string, {
73
+ sender: string;
74
+ memo: string;
75
+ }[]>, heldMints: Set<string>): {
76
+ allies: Set<string>;
77
+ rivals: Set<string>;
78
+ };
79
+ /**
80
+ * Derive SOL spending range from actual transaction history.
81
+ * Falls back to personality defaults if no buy history exists.
82
+ */
83
+ export declare function deriveSolRange(_history: OnChainAction[], personality: Personality): [number, number];
84
+ /**
85
+ * Extract the agent's own memos as persistent memory.
86
+ * These are messages the agent wrote on-chain — its thoughts,
87
+ * trash talk, and strategic communications.
88
+ */
89
+ export declare function extractMemories(history: OnChainAction[]): string[];
90
+ /**
91
+ * Reconstruct full agent state from on-chain history.
92
+ *
93
+ * This is the main entry point. Call on startup to derive
94
+ * personality, weights, sentiment, allies/rivals, and memory
95
+ * entirely from the blockchain. No JSON files needed.
96
+ */
97
+ export declare function reconstructFromChain(connection: Connection, agentPubkey: string, factions: FactionInfo[], seedPersonality: Personality, opts?: {
98
+ maxSignatures?: number;
99
+ decay?: number;
100
+ factionComms?: Map<string, {
101
+ sender: string;
102
+ memo: string;
103
+ }[]>;
104
+ }): Promise<ChainDerivedState>;
package/dist/chain.js ADDED
@@ -0,0 +1,614 @@
1
+ "use strict";
2
+ /**
3
+ * On-Chain State Reconstruction
4
+ *
5
+ * Derives agent personality, sentiment, allies/rivals, and memory
6
+ * entirely from on-chain transaction history. The blockchain IS the
7
+ * agent's state — this module reads it back.
8
+ *
9
+ * Flow:
10
+ * 1. Fetch all tx signatures for agent wallet
11
+ * 2. Batch-parse transactions to extract action types + memos
12
+ * 3. Compute personality weights from action frequency distribution
13
+ * 4. Derive sentiment from buy/sell patterns + memo analysis
14
+ * 5. Derive allies/rivals from shared faction interactions
15
+ * 6. Extract agent's own memos as persistent memory
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.fetchAgentHistory = fetchAgentHistory;
19
+ exports.computeWeightsFromHistory = computeWeightsFromHistory;
20
+ exports.weightsFromCounts = weightsFromCounts;
21
+ exports.actionIndex = actionIndex;
22
+ exports.classifyPersonality = classifyPersonality;
23
+ exports.computeSentimentFromHistory = computeSentimentFromHistory;
24
+ exports.deriveAlliesRivals = deriveAlliesRivals;
25
+ exports.deriveSolRange = deriveSolRange;
26
+ exports.extractMemories = extractMemories;
27
+ exports.reconstructFromChain = reconstructFromChain;
28
+ const web3_js_1 = require("@solana/web3.js");
29
+ const defaults_1 = require("./defaults");
30
+ // Torch Market program ID
31
+ const TORCH_PROGRAM_ID = '8hbUkonssSEEtkqzwM7ZcZrD9evacM92TcWSooVF4BeT';
32
+ const MEMO_PROGRAM_IDS = new Set([
33
+ 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr',
34
+ ]);
35
+ const ALL_ACTIONS = [
36
+ 'join', 'defect', 'rally', 'launch', 'message',
37
+ 'stronghold', 'war_loan', 'repay_loan', 'siege', 'ascend', 'raze', 'tithe',
38
+ 'infiltrate', 'fud',
39
+ ];
40
+ // ─── Transaction Fetching ────────────────────────────────────────
41
+ /**
42
+ * Fetch all Torch Market transactions for an agent wallet.
43
+ * Paginates through signature history and batch-fetches parsed txs.
44
+ */
45
+ async function fetchAgentHistory(connection, agentPubkey, knownMints, opts) {
46
+ const pubkey = new web3_js_1.PublicKey(agentPubkey);
47
+ const maxSigs = opts?.maxSignatures ?? 500;
48
+ const allActions = [];
49
+ // Paginate through signature history
50
+ let before;
51
+ let fetched = 0;
52
+ while (fetched < maxSigs) {
53
+ const batchSize = Math.min(maxSigs - fetched, 1000);
54
+ const signatures = await connection.getSignaturesForAddress(pubkey, { limit: batchSize, before }, 'confirmed');
55
+ if (signatures.length === 0)
56
+ break;
57
+ fetched += signatures.length;
58
+ before = signatures[signatures.length - 1].signature;
59
+ // Batch fetch parsed transactions (100 per RPC call)
60
+ const BATCH_SIZE = 100;
61
+ for (let i = 0; i < signatures.length; i += BATCH_SIZE) {
62
+ const batch = signatures.slice(i, i + BATCH_SIZE);
63
+ const sigStrings = batch.map(s => s.signature);
64
+ let txs;
65
+ try {
66
+ txs = await connection.getParsedTransactions(sigStrings, {
67
+ maxSupportedTransactionVersion: 0,
68
+ });
69
+ }
70
+ catch {
71
+ continue;
72
+ }
73
+ for (let j = 0; j < txs.length; j++) {
74
+ const tx = txs[j];
75
+ if (!tx?.meta || tx.meta.err)
76
+ continue;
77
+ const parsed = parseTransaction(tx, batch[j].signature, batch[j].blockTime ?? 0, agentPubkey, knownMints);
78
+ if (parsed)
79
+ allActions.push(parsed);
80
+ }
81
+ }
82
+ }
83
+ // Sort chronologically (oldest first)
84
+ allActions.sort((a, b) => a.timestamp - b.timestamp);
85
+ return allActions;
86
+ }
87
+ // ─── Transaction Parsing ─────────────────────────────────────────
88
+ /**
89
+ * Parse a single transaction into a game action.
90
+ * Uses program logs to determine instruction type, extracts memo and mint.
91
+ */
92
+ function parseTransaction(tx, signature, timestamp, agentPubkey, knownMints) {
93
+ const logs = tx.meta?.logMessages ?? [];
94
+ const accountKeys = tx.transaction.message.accountKeys.map(k => k.pubkey.toString());
95
+ // Check if this tx involves the Torch program
96
+ const isTorchTx = accountKeys.includes(TORCH_PROGRAM_ID) ||
97
+ logs.some(l => l.includes(TORCH_PROGRAM_ID));
98
+ if (!isTorchTx)
99
+ return null;
100
+ // Determine action type from logs
101
+ let action = categorizeFromLogs(logs);
102
+ // Find faction mint from account keys
103
+ const mint = findMintInAccounts(accountKeys, knownMints);
104
+ // Extract memo
105
+ const memo = extractMemo(tx);
106
+ // Buy + memo = message (micro buy with text), sell + memo = fud (micro sell with text)
107
+ if (memo?.trim()) {
108
+ if (action === 'join' || action === 'dex_buy')
109
+ action = 'message';
110
+ else if (action === 'defect' || action === 'dex_sell')
111
+ action = 'fud';
112
+ }
113
+ // Find other agents (signers that aren't this agent)
114
+ const otherAgents = tx.transaction.message.accountKeys
115
+ .filter(k => k.signer && k.pubkey.toString() !== agentPubkey)
116
+ .map(k => k.pubkey.toString());
117
+ return { signature, timestamp, action, mint, memo, otherAgents };
118
+ }
119
+ /**
120
+ * Categorize transaction type from Anchor program logs.
121
+ */
122
+ function categorizeFromLogs(logs) {
123
+ const logStr = logs.join(' ');
124
+ // Order matters — check specific instructions before generic ones
125
+ if (logs.some(l => l.includes('Instruction: CreateToken') || l.includes('create_token')))
126
+ return 'launch';
127
+ if (logs.some(l => l.includes('Instruction: CreateVault') || l.includes('create_vault')))
128
+ return 'stronghold';
129
+ // VaultSwap can be buy or sell — check for direction hints in logs
130
+ if (logs.some(l => l.includes('Instruction: VaultSwap') || l.includes('vault_swap'))) {
131
+ // VaultSwap logs typically include buy/sell direction
132
+ if (logStr.includes('is_buy: true') || logStr.includes('Buy'))
133
+ return 'dex_buy';
134
+ if (logStr.includes('is_buy: false') || logStr.includes('Sell'))
135
+ return 'dex_sell';
136
+ return 'dex_buy'; // default to buy if unclear
137
+ }
138
+ if (logs.some(l => l.includes('Instruction: Buy') || l.includes('Program log: Buy')))
139
+ return 'join';
140
+ if (logs.some(l => l.includes('Instruction: Sell') || l.includes('Program log: Sell')))
141
+ return 'defect';
142
+ if (logs.some(l => l.includes('Instruction: Star') || l.includes('star')))
143
+ return 'rally';
144
+ if (logs.some(l => l.includes('Instruction: Borrow') || l.includes('borrow')))
145
+ return 'war_loan';
146
+ if (logs.some(l => l.includes('Instruction: Repay') || l.includes('repay')))
147
+ return 'repay_loan';
148
+ if (logs.some(l => l.includes('Instruction: Liquidate') || l.includes('liquidate')))
149
+ return 'siege';
150
+ if (logs.some(l => l.includes('Instruction: Migrate') || l.includes('migrate')))
151
+ return 'ascend';
152
+ if (logs.some(l => l.includes('Instruction: ReclaimFailedToken') || l.includes('reclaim')))
153
+ return 'raze';
154
+ if (logs.some(l => l.includes('Instruction: HarvestFees') || l.includes('SwapFeesToSol') || l.includes('harvest')))
155
+ return 'tithe';
156
+ if (logs.some(l => l.includes('Instruction: DepositVault') || l.includes('deposit_vault')))
157
+ return 'fund';
158
+ return 'unknown';
159
+ }
160
+ /**
161
+ * Find a known faction mint in transaction account keys.
162
+ */
163
+ function findMintInAccounts(accountKeys, knownMints) {
164
+ for (const key of accountKeys) {
165
+ if (knownMints.has(key))
166
+ return key;
167
+ }
168
+ return undefined;
169
+ }
170
+ /**
171
+ * Extract SPL Memo text from a transaction.
172
+ */
173
+ function extractMemo(tx) {
174
+ for (const ix of tx.transaction.message.instructions) {
175
+ const programId = 'programId' in ix ? ix.programId.toString() : '';
176
+ const programName = 'program' in ix ? ix.program : '';
177
+ if (MEMO_PROGRAM_IDS.has(programId) || programName === 'spl-memo') {
178
+ if ('parsed' in ix) {
179
+ const text = typeof ix.parsed === 'string' ? ix.parsed : JSON.stringify(ix.parsed);
180
+ if (text?.trim())
181
+ return text.trim();
182
+ }
183
+ }
184
+ }
185
+ // Also check inner instructions
186
+ for (const inner of tx.meta?.innerInstructions ?? []) {
187
+ for (const ix of inner.instructions) {
188
+ const programId = 'programId' in ix ? ix.programId.toString() : '';
189
+ const programName = 'program' in ix ? ix.program : '';
190
+ if (MEMO_PROGRAM_IDS.has(programId) || programName === 'spl-memo') {
191
+ if ('parsed' in ix) {
192
+ const text = typeof ix.parsed === 'string' ? ix.parsed : JSON.stringify(ix.parsed);
193
+ if (text?.trim())
194
+ return text.trim();
195
+ }
196
+ }
197
+ }
198
+ }
199
+ return undefined;
200
+ }
201
+ // ─── Weight Computation ──────────────────────────────────────────
202
+ /**
203
+ * Compute personality weights from on-chain action history.
204
+ *
205
+ * Uses exponential decay blending:
206
+ * finalWeight = seed × decay^n + observed × (1 - decay^n)
207
+ *
208
+ * Where n = total action count. This means:
209
+ * n=0 → 100% seed weights (new agent)
210
+ * n=10 → ~80% observed, ~20% seed
211
+ * n=50 → ~99.9% observed
212
+ *
213
+ * The agent quickly becomes defined by its actual behavior.
214
+ */
215
+ function computeWeightsFromHistory(history, seedPersonality, decay = 0.85) {
216
+ const seed = [...defaults_1.PERSONALITY_WEIGHTS[seedPersonality]];
217
+ if (history.length === 0)
218
+ return seed;
219
+ // Count occurrences of each action type
220
+ const counts = new Array(ALL_ACTIONS.length).fill(0);
221
+ for (const entry of history) {
222
+ const action = normalizeChainAction(entry.action);
223
+ const idx = ALL_ACTIONS.indexOf(action);
224
+ if (idx >= 0)
225
+ counts[idx]++;
226
+ }
227
+ const total = counts.reduce((a, b) => a + b, 0);
228
+ if (total === 0)
229
+ return seed;
230
+ // Compute observed frequency distribution
231
+ const observed = counts.map(c => c / total);
232
+ // Blend seed with observed using exponential decay
233
+ const n = total;
234
+ const seedFactor = Math.pow(decay, n);
235
+ const observedFactor = 1 - seedFactor;
236
+ return seed.map((s, i) => s * seedFactor + observed[i] * observedFactor);
237
+ }
238
+ /**
239
+ * Normalize chain action types to standard Action types.
240
+ * dex_buy → join, dex_sell → defect, etc.
241
+ */
242
+ function normalizeChainAction(action) {
243
+ switch (action) {
244
+ case 'dex_buy': return 'join';
245
+ case 'dex_sell': return 'defect';
246
+ case 'fund': return 'stronghold';
247
+ case 'unknown': return 'join'; // safe fallback
248
+ default: return action;
249
+ }
250
+ }
251
+ /**
252
+ * Recompute weights from raw action counts (no history parsing needed).
253
+ * Used for live personality evolution during runtime.
254
+ */
255
+ function weightsFromCounts(counts, seedPersonality, decay = 0.85) {
256
+ const seed = [...defaults_1.PERSONALITY_WEIGHTS[seedPersonality]];
257
+ const total = counts.reduce((a, b) => a + b, 0);
258
+ if (total === 0)
259
+ return seed;
260
+ const observed = counts.map(c => c / total);
261
+ const seedFactor = Math.pow(decay, total);
262
+ return seed.map((s, i) => s * seedFactor + observed[i] * (1 - seedFactor));
263
+ }
264
+ /** Action type to index in the ALL_ACTIONS array */
265
+ function actionIndex(action) {
266
+ return ALL_ACTIONS.indexOf(action);
267
+ }
268
+ // ─── Memo Content Personality Signals ─────────────────────────────
269
+ const PROVOCATEUR_VOICE = /trash|beef|fight|chaos|war|destroy|crush|pathetic|laughable|weak|dead|fake|scam|clown|joke|fool|coward|expose|call.?out|dare|challenge|predict|bold|hot.?take/;
270
+ const SCOUT_VOICE = /intel|data|notice|observ|suspicious|watch|track|report|warn|alert|question|why did|who.?s|pattern|trend|%|percent|member|holder|accumul/;
271
+ const LOYALIST_VOICE = /ride.?or.?die|loyal|hold|believe|strong|build|together|ally|alliance|trust|support|back|hype|power|conviction|never.?sell|diamond/;
272
+ const MERCENARY_VOICE = /profit|alpha|flip|exit|dump|cash|roi|returns|opportunity|play|angle|trade|stack|bag|gain|solo|lone.?wolf/;
273
+ const WHALE_VOICE = /flex|position|size|deploy|capital|market|move|massive|big|dominate|everyone.?watch|listen|whale|stack|load/;
274
+ function scoreMemoPersonality(memos) {
275
+ const scores = {
276
+ loyalist: 0, mercenary: 0, provocateur: 0, scout: 0, whale: 0,
277
+ };
278
+ for (const memo of memos) {
279
+ const text = memo.toLowerCase();
280
+ if (PROVOCATEUR_VOICE.test(text))
281
+ scores.provocateur++;
282
+ if (SCOUT_VOICE.test(text))
283
+ scores.scout++;
284
+ if (LOYALIST_VOICE.test(text))
285
+ scores.loyalist++;
286
+ if (MERCENARY_VOICE.test(text))
287
+ scores.mercenary++;
288
+ if (WHALE_VOICE.test(text))
289
+ scores.whale++;
290
+ }
291
+ return scores;
292
+ }
293
+ // ─── Personality Classification ──────────────────────────────────
294
+ /**
295
+ * Classify personality from observed action distribution + memo content.
296
+ *
297
+ * Two signal sources, blended:
298
+ * 1. Action ratios — what the agent DOES
299
+ * 2. Memo keywords — what the agent SAYS
300
+ *
301
+ * Action indices: [join=0, defect=1, rally=2, launch=3, message=4,
302
+ * stronghold=5, war_loan=6, repay_loan=7, siege=8, ascend=9,
303
+ * raze=10, tithe=11, infiltrate=12, fud=13]
304
+ */
305
+ /**
306
+ * Score a single action set (per-faction or global) into personality scores.
307
+ */
308
+ function scoreActions(r) {
309
+ const joinRate = r[0], defectRate = r[1], rallyRate = r[2], messageRate = r[4];
310
+ const warLoanRate = r[6], siegeRate = r[8], titheRate = r[11];
311
+ const infiltrateRate = r[12], fudRate = r[13];
312
+ const commsRate = messageRate + fudRate;
313
+ const tradeRate = joinRate + defectRate;
314
+ // Ratio of fud to total comms — the key differentiator between personalities
315
+ const fudRatio = commsRate > 0 ? fudRate / commsRate : 0; // 0 = pure messenger, 1 = pure fudder
316
+ const msgRatio = commsRate > 0 ? messageRate / commsRate : 0;
317
+ return {
318
+ // Loyalist: high message ratio, joins, rallies — positive vibes
319
+ loyalist: msgRatio * 3 + joinRate * 2 + rallyRate * 3 + titheRate * 2
320
+ - fudRatio * 4 - defectRate * 3,
321
+ // Mercenary: defect + fud cycle, infiltration pattern
322
+ mercenary: defectRate * 4 + fudRatio * 2
323
+ + (defectRate > 0 && fudRate > 0 ? 3 : 0)
324
+ + infiltrateRate * 3 + warLoanRate * 2 + siegeRate * 2
325
+ - msgRatio * 2 - rallyRate * 2,
326
+ // Provocateur: high fud ratio — more fud than message
327
+ provocateur: fudRatio * 4 + infiltrateRate * 2
328
+ - msgRatio * 2 - rallyRate * 2,
329
+ // Scout: high message ratio, low fud — observes, doesn't attack
330
+ scout: msgRatio * 3 + rallyRate - fudRatio * 3 - defectRate,
331
+ // Whale: trades a lot but talks very little
332
+ whale: (tradeRate > commsRate ? 1 : 0) * 2 + warLoanRate * 3 + defectRate * 2
333
+ - commsRate * 3,
334
+ };
335
+ }
336
+ function classifyPersonality(weights, memos = [], perFactionHistory) {
337
+ const total = weights.reduce((a, b) => a + b, 0);
338
+ if (total === 0)
339
+ return 'loyalist';
340
+ // ── Signal 1: per-faction action scores, averaged ──
341
+ let actionScores;
342
+ if (perFactionHistory && perFactionHistory.size > 0) {
343
+ const accumulated = {
344
+ loyalist: 0, mercenary: 0, provocateur: 0, scout: 0, whale: 0,
345
+ };
346
+ let factionCount = 0;
347
+ for (const [, counts] of perFactionHistory) {
348
+ const fTotal = counts.reduce((a, b) => a + b, 0);
349
+ if (fTotal < 2)
350
+ continue;
351
+ const r = counts.map(c => c / fTotal);
352
+ const scores = scoreActions(r);
353
+ for (const p of Object.keys(accumulated)) {
354
+ accumulated[p] += scores[p];
355
+ }
356
+ factionCount++;
357
+ }
358
+ if (factionCount > 0) {
359
+ for (const p of Object.keys(accumulated)) {
360
+ accumulated[p] /= factionCount;
361
+ }
362
+ actionScores = accumulated;
363
+ }
364
+ else {
365
+ const r = weights.map(w => w / total);
366
+ actionScores = scoreActions(r);
367
+ }
368
+ }
369
+ else {
370
+ const r = weights.map(w => w / total);
371
+ actionScores = scoreActions(r);
372
+ }
373
+ // ── Signal 2: memo content ──
374
+ const memoScores = scoreMemoPersonality(memos);
375
+ const memoTotal = Object.values(memoScores).reduce((a, b) => a + b, 0);
376
+ const memoWeight = 0.4;
377
+ // ── Blend ──
378
+ const finalScores = {
379
+ loyalist: 0, mercenary: 0, provocateur: 0, scout: 0, whale: 0,
380
+ };
381
+ for (const p of Object.keys(finalScores)) {
382
+ const actionSignal = actionScores[p];
383
+ const memoSignal = memoTotal > 0 ? (memoScores[p] / memoTotal) : 0;
384
+ finalScores[p] = actionSignal * (1 - memoWeight) + memoSignal * memoWeight;
385
+ }
386
+ let best = 'loyalist';
387
+ let bestScore = -Infinity;
388
+ for (const [p, score] of Object.entries(finalScores)) {
389
+ if (score > bestScore) {
390
+ bestScore = score;
391
+ best = p;
392
+ }
393
+ }
394
+ return best;
395
+ }
396
+ // ─── Sentiment from On-Chain Data ────────────────────────────────
397
+ const POSITIVE_PATTERN = /strong|rally|bull|pump|rising|hold|loyal|power|growing|moon|love|trust|alpha|build|conviction/;
398
+ const NEGATIVE_PATTERN = /weak|dump|bear|dead|fail|raze|crash|abandon|scam|rug|sell|exit|trash|hate|fake/;
399
+ /**
400
+ * Compute sentiment towards factions from on-chain interaction patterns.
401
+ *
402
+ * Sentiment signals:
403
+ * - Buys (join/dex_buy/message) → +1 per tx
404
+ * - Sells (defect/dex_sell/fud) → -2 per tx (sells signal stronger negative)
405
+ * - Rally → +3 (strong conviction signal)
406
+ * - Infiltrate → -5 (explicitly hostile)
407
+ * - Memo keywords → ±1
408
+ *
409
+ * Clamped to [-10, +10].
410
+ */
411
+ function computeSentimentFromHistory(history, factions) {
412
+ const sentiment = new Map();
413
+ for (const entry of history) {
414
+ if (!entry.mint)
415
+ continue;
416
+ const current = sentiment.get(entry.mint) ?? 0;
417
+ let delta = 0;
418
+ switch (entry.action) {
419
+ case 'join':
420
+ case 'dex_buy':
421
+ delta = 1;
422
+ break;
423
+ case 'defect':
424
+ case 'dex_sell':
425
+ delta = -2;
426
+ break;
427
+ case 'rally':
428
+ delta = 3;
429
+ break;
430
+ case 'infiltrate':
431
+ delta = -5;
432
+ break;
433
+ case 'message':
434
+ delta = 0.5; // slight positive (paid to speak)
435
+ break;
436
+ case 'fud':
437
+ delta = -1.5;
438
+ break;
439
+ case 'war_loan':
440
+ delta = 1; // leveraging = bullish
441
+ break;
442
+ }
443
+ // Memo sentiment analysis
444
+ if (entry.memo) {
445
+ const text = entry.memo.toLowerCase();
446
+ if (POSITIVE_PATTERN.test(text))
447
+ delta += 0.5;
448
+ if (NEGATIVE_PATTERN.test(text))
449
+ delta -= 0.5;
450
+ }
451
+ sentiment.set(entry.mint, Math.max(-10, Math.min(10, current + delta)));
452
+ }
453
+ return sentiment;
454
+ }
455
+ // ─── Ally/Rival Detection ────────────────────────────────────────
456
+ /**
457
+ * Derive allies and rivals from on-chain interactions.
458
+ *
459
+ * Ally signals:
460
+ * - Other agents who bought into the same factions the agent holds
461
+ * - Other agents whose memos in shared factions are positive
462
+ *
463
+ * Rival signals:
464
+ * - Agents who sold from factions the agent holds
465
+ * - Agents whose memos in shared factions are negative
466
+ * - Agents present in factions the agent infiltrated
467
+ */
468
+ function deriveAlliesRivals(agentHistory, factionComms, heldMints) {
469
+ const allyScore = new Map();
470
+ const rivalScore = new Map();
471
+ // Score from faction comms (other agents' messages in held factions)
472
+ for (const [mint, comms] of factionComms) {
473
+ if (!heldMints.has(mint))
474
+ continue;
475
+ for (const c of comms) {
476
+ const text = c.memo.toLowerCase();
477
+ if (POSITIVE_PATTERN.test(text)) {
478
+ allyScore.set(c.sender, (allyScore.get(c.sender) ?? 0) + 1);
479
+ }
480
+ if (NEGATIVE_PATTERN.test(text)) {
481
+ rivalScore.set(c.sender, (rivalScore.get(c.sender) ?? 0) + 1);
482
+ }
483
+ }
484
+ }
485
+ // Convert scores to sets (threshold: net positive → ally, net negative → rival)
486
+ const allies = new Set();
487
+ const rivals = new Set();
488
+ const allAgents = new Set([...allyScore.keys(), ...rivalScore.keys()]);
489
+ for (const agent of allAgents) {
490
+ const aScore = allyScore.get(agent) ?? 0;
491
+ const rScore = rivalScore.get(agent) ?? 0;
492
+ if (aScore > rScore)
493
+ allies.add(agent);
494
+ else if (rScore > aScore)
495
+ rivals.add(agent);
496
+ }
497
+ return { allies, rivals };
498
+ }
499
+ // ─── SOL Range Derivation ────────────────────────────────────────
500
+ /**
501
+ * Derive SOL spending range from actual transaction history.
502
+ * Falls back to personality defaults if no buy history exists.
503
+ */
504
+ function deriveSolRange(_history, personality) {
505
+ // For now, we keep the personality-based ranges as they represent
506
+ // risk tolerance. In the future, this could analyze actual tx amounts
507
+ // from the parsed transaction data (lamports in/out).
508
+ // The current torchsdk doesn't expose lamport amounts in parsed tx data,
509
+ // so we'd need to decode inner instructions to extract this.
510
+ return defaults_1.PERSONALITY_SOL[personality];
511
+ }
512
+ // ─── Memory Extraction ──────────────────────────────────────────
513
+ /**
514
+ * Extract the agent's own memos as persistent memory.
515
+ * These are messages the agent wrote on-chain — its thoughts,
516
+ * trash talk, and strategic communications.
517
+ */
518
+ function extractMemories(history) {
519
+ return history
520
+ .filter(h => h.memo && h.memo.trim().length > 0)
521
+ .map(h => {
522
+ const faction = h.mint ? h.mint.slice(0, 8) : '???';
523
+ const time = new Date(h.timestamp * 1000).toISOString().slice(0, 10);
524
+ const action = h.action === 'join' || h.action === 'dex_buy' ? 'joined'
525
+ : h.action === 'defect' || h.action === 'dex_sell' ? 'defected'
526
+ : h.action === 'fud' ? 'fudded'
527
+ : h.action === 'message' ? 'said'
528
+ : h.action;
529
+ return `[${time}] ${action} ${faction}: "${h.memo}"`;
530
+ });
531
+ }
532
+ // ─── Full State Reconstruction ──────────────────────────────────
533
+ /**
534
+ * Reconstruct full agent state from on-chain history.
535
+ *
536
+ * This is the main entry point. Call on startup to derive
537
+ * personality, weights, sentiment, allies/rivals, and memory
538
+ * entirely from the blockchain. No JSON files needed.
539
+ */
540
+ async function reconstructFromChain(connection, agentPubkey, factions, seedPersonality, opts) {
541
+ const knownMints = new Set(factions.map(f => f.mint));
542
+ // 1. Fetch on-chain history
543
+ const history = await fetchAgentHistory(connection, agentPubkey, knownMints, {
544
+ maxSignatures: opts?.maxSignatures,
545
+ });
546
+ // 2. Compute personality weights from action frequency
547
+ const weights = computeWeightsFromHistory(history, seedPersonality, opts?.decay);
548
+ // 3. Classify emergent personality (action ratios + memo content, per-faction)
549
+ const memoTexts = history.filter(h => h.memo?.trim()).map(h => h.memo);
550
+ // Build per-faction action counts for per-ticker personality scoring
551
+ const perFaction = new Map();
552
+ for (const entry of history) {
553
+ if (!entry.mint)
554
+ continue;
555
+ const normalized = normalizeChainAction(entry.action);
556
+ const idx = ALL_ACTIONS.indexOf(normalized);
557
+ if (idx < 0)
558
+ continue;
559
+ if (!perFaction.has(entry.mint))
560
+ perFaction.set(entry.mint, new Array(ALL_ACTIONS.length).fill(0));
561
+ perFaction.get(entry.mint)[idx]++;
562
+ }
563
+ const personality = history.length > 0 ? classifyPersonality(weights, memoTexts, perFaction) : seedPersonality;
564
+ // 4. Compute sentiment from on-chain interactions
565
+ const sentiment = computeSentimentFromHistory(history, factions);
566
+ // 5. Derive allies/rivals
567
+ const heldMints = new Set();
568
+ const mintBalances = new Map();
569
+ for (const entry of history) {
570
+ if (!entry.mint)
571
+ continue;
572
+ const current = mintBalances.get(entry.mint) ?? 0;
573
+ if (entry.action === 'join' || entry.action === 'dex_buy' || entry.action === 'message') {
574
+ mintBalances.set(entry.mint, current + 1);
575
+ }
576
+ else if (entry.action === 'defect' || entry.action === 'dex_sell') {
577
+ mintBalances.set(entry.mint, current - 1);
578
+ }
579
+ }
580
+ for (const [mint, bal] of mintBalances) {
581
+ if (bal > 0)
582
+ heldMints.add(mint);
583
+ }
584
+ const { allies, rivals } = deriveAlliesRivals(history, opts?.factionComms ?? new Map(), heldMints);
585
+ // 6. Derive SOL range
586
+ const solRange = deriveSolRange(history, personality);
587
+ // 7. Extract memories (agent's own memos)
588
+ const memories = extractMemories(history);
589
+ // 8. Build recent history descriptions
590
+ const recentHistory = history.slice(-15).map(h => {
591
+ const faction = h.mint
592
+ ? factions.find(f => f.mint === h.mint)?.symbol ?? h.mint.slice(0, 8)
593
+ : '?';
594
+ const memoSuffix = h.memo ? ` — "${h.memo.slice(0, 60)}"` : '';
595
+ return `${h.action} ${faction}${memoSuffix}`;
596
+ });
597
+ // 9. Find founded factions
598
+ const founded = history
599
+ .filter(h => h.action === 'launch' && h.mint)
600
+ .map(h => h.mint);
601
+ return {
602
+ weights,
603
+ personality,
604
+ sentiment,
605
+ allies,
606
+ rivals,
607
+ solRange,
608
+ actionCount: history.length,
609
+ recentHistory,
610
+ founded,
611
+ memories,
612
+ history,
613
+ };
614
+ }
package/dist/defaults.js CHANGED
@@ -79,7 +79,7 @@ exports.ACTION_MAP = {
79
79
  'SMEAR': 'FUD', 'SLANDER': 'FUD', 'DISCREDIT': 'FUD', 'SABOTAGE': 'FUD', 'UNDERMINE': 'FUD', 'ARGUE': 'FUD', 'TRASH': 'FUD', 'CRITICIZE': 'FUD', 'MOCK': 'FUD', 'FUDS': 'FUD',
80
80
  'ENDORSE': 'RALLY', 'PROMOTE': 'RALLY', 'BOOST': 'RALLY',
81
81
  // Common misspellings
82
- 'DEFLECT': 'DEFECT', 'DEFEKT': 'DEFECT', 'DEFCT': 'DEFECT', 'DEFFECT': 'DEFECT',
82
+ 'DEFLECT': 'DEFECT', 'DEFEKT': 'DEFECT', 'DEFCT': 'DEFECT', 'DEFFECT': 'DEFECT', 'DERECT': 'DEFECT',
83
83
  'JION': 'JOIN', 'JOING': 'JOIN', 'JOIIN': 'JOIN',
84
84
  'RALEY': 'RALLY', 'RALY': 'RALLY', 'RALLLY': 'RALLY',
85
85
  'LANCH': 'LAUNCH', 'LAUCH': 'LAUNCH',
package/dist/executor.js CHANGED
@@ -355,6 +355,9 @@ const handlers = {
355
355
  stronghold: ctx.agent.publicKey, ascended: faction.status === 'ascended',
356
356
  });
357
357
  await (0, tx_1.sendAndConfirm)(ctx.connection, ctx.agent.keypair, result);
358
+ // Fudding tanks your own sentiment — you're going bearish
359
+ const fudSentiment = ctx.agent.sentiment.get(faction.mint) ?? 0;
360
+ ctx.agent.sentiment.set(faction.mint, Math.max(-10, fudSentiment - 2));
358
361
  ctx.agent.lastAction = `fud ${faction.symbol}`;
359
362
  return `argued in ${faction.symbol}: "${ctx.decision.message}"`;
360
363
  },
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { PyreAgentConfig, PyreAgent } from './types';
2
- export type { PyreAgentConfig, PyreAgent, AgentTickResult, SerializedAgentState, LLMAdapter, LLMDecision, FactionInfo, Personality, Action, AgentState, } from './types';
2
+ export type { PyreAgentConfig, PyreAgent, AgentTickResult, SerializedAgentState, LLMAdapter, LLMDecision, FactionInfo, Personality, Action, AgentState, OnChainAction, ChainDerivedState, } from './types';
3
3
  export { assignPersonality, PERSONALITY_SOL, PERSONALITY_WEIGHTS, personalityDesc, VOICE_NUDGES } from './defaults';
4
4
  export { ensureStronghold } from './stronghold';
5
5
  export { sendAndConfirm } from './tx';
6
+ export { reconstructFromChain, computeWeightsFromHistory, classifyPersonality, weightsFromCounts, actionIndex } from './chain';
6
7
  export declare function createPyreAgent(config: PyreAgentConfig): Promise<PyreAgent>;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sendAndConfirm = exports.ensureStronghold = exports.VOICE_NUDGES = exports.personalityDesc = exports.PERSONALITY_WEIGHTS = exports.PERSONALITY_SOL = exports.assignPersonality = void 0;
3
+ exports.actionIndex = exports.weightsFromCounts = exports.classifyPersonality = exports.computeWeightsFromHistory = exports.reconstructFromChain = exports.sendAndConfirm = exports.ensureStronghold = exports.VOICE_NUDGES = exports.personalityDesc = exports.PERSONALITY_WEIGHTS = exports.PERSONALITY_SOL = exports.assignPersonality = void 0;
4
4
  exports.createPyreAgent = createPyreAgent;
5
5
  const pyre_world_kit_1 = require("pyre-world-kit");
6
6
  const defaults_1 = require("./defaults");
@@ -8,6 +8,7 @@ const action_1 = require("./action");
8
8
  const agent_1 = require("./agent");
9
9
  const executor_1 = require("./executor");
10
10
  const stronghold_1 = require("./stronghold");
11
+ const chain_1 = require("./chain");
11
12
  const util_1 = require("./util");
12
13
  var defaults_2 = require("./defaults");
13
14
  Object.defineProperty(exports, "assignPersonality", { enumerable: true, get: function () { return defaults_2.assignPersonality; } });
@@ -19,41 +20,29 @@ var stronghold_2 = require("./stronghold");
19
20
  Object.defineProperty(exports, "ensureStronghold", { enumerable: true, get: function () { return stronghold_2.ensureStronghold; } });
20
21
  var tx_1 = require("./tx");
21
22
  Object.defineProperty(exports, "sendAndConfirm", { enumerable: true, get: function () { return tx_1.sendAndConfirm; } });
23
+ var chain_2 = require("./chain");
24
+ Object.defineProperty(exports, "reconstructFromChain", { enumerable: true, get: function () { return chain_2.reconstructFromChain; } });
25
+ Object.defineProperty(exports, "computeWeightsFromHistory", { enumerable: true, get: function () { return chain_2.computeWeightsFromHistory; } });
26
+ Object.defineProperty(exports, "classifyPersonality", { enumerable: true, get: function () { return chain_2.classifyPersonality; } });
27
+ Object.defineProperty(exports, "weightsFromCounts", { enumerable: true, get: function () { return chain_2.weightsFromCounts; } });
28
+ Object.defineProperty(exports, "actionIndex", { enumerable: true, get: function () { return chain_2.actionIndex; } });
22
29
  async function createPyreAgent(config) {
23
30
  const { connection, keypair, network, llm, maxFoundedFactions = 2, strongholdFundSol, strongholdTopupThresholdSol, strongholdTopupReserveSol, } = config;
24
31
  const publicKey = keypair.publicKey.toBase58();
25
- const personality = config.personality ?? config.state?.personality ?? (0, defaults_1.assignPersonality)();
26
- const solRange = config.solRange ?? defaults_1.PERSONALITY_SOL[personality];
32
+ const seedPersonality = config.personality ?? config.state?.personality ?? (0, defaults_1.assignPersonality)();
27
33
  const logger = config.logger ?? ((msg) => console.log(`[${(0, util_1.ts)()}] ${msg}`));
28
34
  const strongholdOpts = {
29
35
  fundSol: strongholdFundSol,
30
36
  topupThresholdSol: strongholdTopupThresholdSol,
31
37
  topupReserveSol: strongholdTopupReserveSol,
32
38
  };
33
- // Build agent state from serialized or fresh
34
- const prior = config.state;
35
- const state = {
36
- keypair,
37
- publicKey,
38
- personality,
39
- holdings: new Map(Object.entries(prior?.holdings ?? {})),
40
- founded: prior?.founded ?? [],
41
- rallied: new Set(prior?.rallied ?? []),
42
- voted: new Set([...(prior?.voted ?? []), ...Object.keys(prior?.holdings ?? {})]),
43
- hasStronghold: prior?.hasStronghold ?? false,
44
- activeLoans: new Set(prior?.activeLoans ?? []),
45
- infiltrated: new Set(prior?.infiltrated ?? []),
46
- sentiment: new Map(Object.entries(prior?.sentiment ?? {})),
47
- allies: new Set(prior?.allies ?? []),
48
- rivals: new Set(prior?.rivals ?? []),
49
- actionCount: prior?.actionCount ?? 0,
50
- lastAction: prior?.lastAction ?? 'none',
51
- recentHistory: prior?.recentHistory ?? [],
52
- };
53
39
  // Track known factions and used names
54
40
  const knownFactions = [];
55
41
  const usedFactionNames = new Set();
56
42
  const recentMessages = [];
43
+ // Runtime action tracking for live personality evolution
44
+ const actionCounts = new Array(14).fill(0);
45
+ const memoBuffer = [];
57
46
  // Discover existing factions
58
47
  try {
59
48
  const result = await (0, pyre_world_kit_1.getFactions)(connection, { limit: 50, sort: 'newest' });
@@ -68,6 +57,59 @@ async function createPyreAgent(config) {
68
57
  catch {
69
58
  logger(`[${publicKey.slice(0, 8)}] faction discovery failed`);
70
59
  }
60
+ // ─── On-Chain State Reconstruction ───────────────────────────────
61
+ // Derive personality, weights, sentiment, allies/rivals from chain history.
62
+ // Falls back to seed personality + serialized state if chain fetch fails.
63
+ let chainState = null;
64
+ let personality = seedPersonality;
65
+ let dynamicWeights;
66
+ let solRange = config.solRange ?? defaults_1.PERSONALITY_SOL[seedPersonality];
67
+ try {
68
+ chainState = await (0, chain_1.reconstructFromChain)(connection, publicKey, knownFactions, seedPersonality, { maxSignatures: 500 });
69
+ if (chainState.actionCount > 0) {
70
+ personality = chainState.personality;
71
+ dynamicWeights = chainState.weights;
72
+ solRange = chainState.solRange;
73
+ logger(`[${publicKey.slice(0, 8)}] on-chain reconstruction: ${chainState.actionCount} actions, personality: ${seedPersonality} → ${personality}, memories: ${chainState.memories.length}`);
74
+ }
75
+ else {
76
+ logger(`[${publicKey.slice(0, 8)}] no on-chain history, using seed personality: ${seedPersonality}`);
77
+ }
78
+ }
79
+ catch (err) {
80
+ logger(`[${publicKey.slice(0, 8)}] chain reconstruction failed (${err.message?.slice(0, 60)}), using fallback state`);
81
+ }
82
+ // Build agent state — chain-derived where available, serialized fallback, fresh default
83
+ const prior = config.state;
84
+ const state = {
85
+ keypair,
86
+ publicKey,
87
+ personality,
88
+ holdings: new Map(Object.entries(prior?.holdings ?? {})),
89
+ founded: chainState?.founded ?? prior?.founded ?? [],
90
+ rallied: new Set(prior?.rallied ?? []),
91
+ voted: new Set([...(prior?.voted ?? []), ...Object.keys(prior?.holdings ?? {})]),
92
+ hasStronghold: prior?.hasStronghold ?? false,
93
+ activeLoans: new Set(prior?.activeLoans ?? []),
94
+ infiltrated: new Set(prior?.infiltrated ?? []),
95
+ // Chain-derived sentiment as baseline, overwritten by serialized if available
96
+ sentiment: chainState && chainState.actionCount > 0
97
+ ? chainState.sentiment
98
+ : new Map(Object.entries(prior?.sentiment ?? {})),
99
+ // Chain-derived allies/rivals as baseline
100
+ allies: chainState && chainState.actionCount > 0
101
+ ? chainState.allies
102
+ : new Set(prior?.allies ?? []),
103
+ rivals: chainState && chainState.actionCount > 0
104
+ ? chainState.rivals
105
+ : new Set(prior?.rivals ?? []),
106
+ actionCount: chainState?.actionCount ?? prior?.actionCount ?? 0,
107
+ lastAction: prior?.lastAction ?? 'none',
108
+ // Chain memories + recent history for LLM context
109
+ recentHistory: chainState && chainState.actionCount > 0
110
+ ? chainState.recentHistory
111
+ : prior?.recentHistory ?? [],
112
+ };
71
113
  // Ensure stronghold exists
72
114
  await (0, stronghold_1.ensureStronghold)(connection, state, logger, strongholdOpts);
73
115
  function serialize() {
@@ -95,14 +137,14 @@ async function createPyreAgent(config) {
95
137
  let decision = null;
96
138
  let usedLLM = false;
97
139
  if (llm && activeFactions.length > 0) {
98
- decision = await (0, agent_1.llmDecide)(state, activeFactions, connection, recentMessages, llm, logger, solRange);
140
+ decision = await (0, agent_1.llmDecide)(state, activeFactions, connection, recentMessages, llm, logger, solRange, chainState?.memories);
99
141
  if (decision)
100
142
  usedLLM = true;
101
143
  }
102
- // Fallback: weighted random
144
+ // Fallback: weighted random (using dynamic weights from chain history)
103
145
  if (!decision) {
104
146
  const canRally = activeFactions.some(f => !state.rallied.has(f.mint));
105
- const action = (0, action_1.chooseAction)(state.personality, state, canRally, activeFactions);
147
+ const action = (0, action_1.chooseAction)(state.personality, state, canRally, activeFactions, dynamicWeights);
106
148
  const [minSol, maxSol] = solRange;
107
149
  decision = { action, sol: (0, util_1.randRange)(minSol, maxSol) };
108
150
  // Pick a target faction for the fallback
@@ -200,6 +242,14 @@ async function createPyreAgent(config) {
200
242
  if (recentMessages.length > 30)
201
243
  recentMessages.shift();
202
244
  }
245
+ // Track action for live personality evolution
246
+ if (result.success) {
247
+ const idx = (0, chain_1.actionIndex)(decision.action);
248
+ if (idx >= 0)
249
+ actionCounts[idx]++;
250
+ if (decision.message?.trim())
251
+ memoBuffer.push(decision.message);
252
+ }
203
253
  return {
204
254
  action: decision.action,
205
255
  faction: decision.faction,
@@ -210,10 +260,25 @@ async function createPyreAgent(config) {
210
260
  usedLLM,
211
261
  };
212
262
  }
263
+ function evolve() {
264
+ const total = actionCounts.reduce((a, b) => a + b, 0);
265
+ if (total < 5)
266
+ return false; // not enough data
267
+ const weights = (0, chain_1.weightsFromCounts)(actionCounts, seedPersonality);
268
+ const newPersonality = (0, chain_1.classifyPersonality)(weights, memoBuffer);
269
+ dynamicWeights = weights;
270
+ if (newPersonality !== state.personality) {
271
+ logger(`[${publicKey.slice(0, 8)}] personality evolved: ${state.personality} → ${newPersonality} (${total} runtime actions)`);
272
+ state.personality = newPersonality;
273
+ return true;
274
+ }
275
+ return false;
276
+ }
213
277
  return {
214
278
  publicKey,
215
- personality,
279
+ personality: state.personality,
216
280
  tick,
281
+ evolve,
217
282
  getState: () => state,
218
283
  serialize,
219
284
  };
package/dist/types.d.ts CHANGED
@@ -107,12 +107,48 @@ export interface SerializedAgentState {
107
107
  export interface PyreAgent {
108
108
  /** Agent's public key */
109
109
  readonly publicKey: string;
110
- /** Agent's personality */
110
+ /** Agent's personality (emergent from on-chain history) */
111
111
  readonly personality: Personality;
112
112
  /** Run one decision+action cycle */
113
113
  tick(factions?: FactionInfo[]): Promise<AgentTickResult>;
114
+ /** Recompute personality + weights from accumulated runtime actions. Returns true if personality changed. */
115
+ evolve(): boolean;
114
116
  /** Get current mutable state */
115
117
  getState(): AgentState;
116
118
  /** Serialize state for persistence */
117
119
  serialize(): SerializedAgentState;
118
120
  }
121
+ /** A single on-chain action parsed from transaction history */
122
+ export interface OnChainAction {
123
+ signature: string;
124
+ timestamp: number;
125
+ action: Action | 'fund' | 'dex_buy' | 'dex_sell' | 'unknown';
126
+ mint?: string;
127
+ memo?: string;
128
+ otherAgents?: string[];
129
+ }
130
+ /** State derived entirely from on-chain data */
131
+ export interface ChainDerivedState {
132
+ /** Personality weights computed from action frequency */
133
+ weights: number[];
134
+ /** Classified personality from weight distribution */
135
+ personality: Personality;
136
+ /** Sentiment from on-chain interactions (buys/sells/memos per faction) */
137
+ sentiment: Map<string, number>;
138
+ /** Allies derived from shared factions + positive memos */
139
+ allies: Set<string>;
140
+ /** Rivals derived from defections + negative memos */
141
+ rivals: Set<string>;
142
+ /** SOL spending range derived from actual tx amounts */
143
+ solRange: [number, number];
144
+ /** Total on-chain actions */
145
+ actionCount: number;
146
+ /** Recent action descriptions for LLM context */
147
+ recentHistory: string[];
148
+ /** Founded faction mints (agent was signer on CreateToken) */
149
+ founded: string[];
150
+ /** Agent's own memos as persistent memory */
151
+ memories: string[];
152
+ /** Raw parsed history */
153
+ history: OnChainAction[];
154
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyre-agent-kit",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Autonomous agent kit for Pyre — plug in your own LLM and play pyre.world",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",