Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ ANTHROPIC_API_KEY_PLAY=your-api-key

LATEST_SONNET_MODEL=claude-sonnet-4-5-20250929
LATEST_HAIKU_MODEL=claude-haiku-4-5-20251001
HAIKU_3_5_MODEL=claude-3-5-haiku-20241022

# This model is used for most design tasks
CHAINCRAFT_DESIGN_MODEL_NAME=${LATEST_HAIKU_MODEL}
Expand All @@ -40,7 +39,7 @@ CHAINCRAFT_SIM_GRAPH_CACHE_SIZE=100
# Optional configurations
# Design agent model overrides (uses CHAINCRAFT_DESIGN_MODEL_NAME if not specified)
# Conversation agent model - use Haiku 3.5 due to lower caching limit.
CHAINCRAFT_CONVERSATIONAL_AGENT_MODEL=${HAIKU_3_5_MODEL}
CHAINCRAFT_CONVERSATIONAL_AGENT_MODEL=${LATEST_HAIKU_MODEL}
# Spec Execute Agent - Generates full specifications (requires Sonnet 4.5 for quality/capacity)
CHAINCRAFT_SPEC_EXECUTE_MODEL=${LATEST_SONNET_MODEL}
# Spec Narrative Agent - Generates narrative descriptions for game specs
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"pm2:logs:bot": "pm2 logs chaincraft-discord-bot",
"pm2:status": "pm2 status",
"pm2:monit": "pm2 monit",
"import-game": "npm run build && node ./dist/scripts/import-game.js"
"import-game": "npm run build && node ./dist/scripts/import-game.js",
"invalidate-artifacts": "npm run build && node ./dist/scripts/invalidate-artifacts.js"
},
"keywords": [],
"author": "",
Expand Down
69 changes: 69 additions & 0 deletions scripts/invalidate-artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env node
import "dotenv/config.js";

import { deleteThread } from "#chaincraft/ai/memory/checkpoint-memory.js";
import { getCachedSpecArtifacts } from "#chaincraft/ai/simulate/simulate-workflow.js";
import { getCachedDesign } from "#chaincraft/ai/design/design-workflow.js";
import { getConfig } from "#chaincraft/config.js";

function parseArgs() {
const args = process.argv.slice(2);
const parsed: Record<string, string | boolean> = {};

for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg.startsWith("--")) {
const key = arg.slice(2);
const next = args[i + 1];
if (next && !next.startsWith("--")) {
parsed[key] = next;
i++;
} else {
parsed[key] = true;
}
}
}

return parsed;
}

async function main() {
const args = parseArgs();
const gameIdRaw = args["gameId"];
if (!gameIdRaw || typeof gameIdRaw !== "string") {
throw new Error("Missing required argument --gameId");
}

let versionStr = typeof args["version"] === "string" ? (args["version"] as string) : undefined;

if (!versionStr) {
console.log("[invalidate-artifacts] --version not provided, loading latest design version...");
const design = await getCachedDesign(gameIdRaw);
const latestVersion = design?.specification?.version;
if (latestVersion === undefined || latestVersion === null) {
throw new Error("Could not determine latest version from cached design; please specify --version");
}
versionStr = String(latestVersion);
console.log(`[invalidate-artifacts] Resolved latest version=${versionStr}`);
}

const specKey = `${gameIdRaw}-v${versionStr}`;
const graphType = getConfig("simulation-graph-type");

console.log(`[invalidate-artifacts] Target specKey=${specKey}, graphType=${graphType}`);

const existing = await getCachedSpecArtifacts(specKey);
if (!existing) {
console.log("[invalidate-artifacts] No cached artifacts found (already empty or never generated). Proceeding to delete thread to force regen.");
} else {
console.log("[invalidate-artifacts] Cached artifacts found. Deleting thread to force regeneration on next sim run.");
}

await deleteThread(specKey, graphType);
console.log("[invalidate-artifacts] Deleted checkpoint thread. Next simulation run will regenerate artifacts.");
}

main().catch((error) => {
console.error("[invalidate-artifacts] Failed:", error instanceof Error ? error.message : error);
process.exit(1);
});
105 changes: 10 additions & 95 deletions src/ai/design/design-workflow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import "dotenv/config.js";

import { SystemMessagePromptTemplate } from "@langchain/core/prompts";
import { Checkpoint, CompiledStateGraph } from "@langchain/langgraph";
import {
AIMessage,
Expand All @@ -15,27 +14,25 @@ import {
} from "#chaincraft/ai/design/game-design-state.js";
import {
gameTitleTag,
imageDesignPrompt,
produceFullGameDesignPrompt,
imageGenPrompt,
rawImageGenPrompt,
rawImageNegativePrompt,
} from "#chaincraft/ai/design/game-design-prompts.js";
import { getSaver } from "#chaincraft/ai/memory/checkpoint-memory.js";
import { OverloadedError } from "#chaincraft/ai/error.js";
import {
isActiveConversation as _isActiveConversation,
registerConversationId,
} from "#chaincraft/ai/conversation.js";
import { imageGenTool, rawImageGenTool } from "#chaincraft/ai/tools.js";
import { GraphCache } from "#chaincraft/ai/graph-cache.js";
import { getConfig } from "#chaincraft/config.js";
import {
logApplicationEvent,
logSecretStatus,
} from "#chaincraft/util/safe-logging.js";
import { createMainDesignGraph } from "./graphs/main-design-graph/index.js";
import { setupDesignModel } from "#chaincraft/ai/model-config.js";
import {
generateImageWithDescription,
CARTRIDGE_IMAGE_CONFIG,
RAW_IMAGE_CONFIG,
} from "#chaincraft/ai/image-gen/image-gen-service.js";

// Log safe application startup info
logApplicationEvent("design-workflow", "initializing", {
Expand All @@ -49,18 +46,13 @@ logSecretStatus(
);

const graphType = getConfig("design-graph-type");
console.log(`[design-workflow] Initialized with graphType='${graphType}'`);

const designGraphCache = new GraphCache(
createDesignGraph,
parseInt(process.env.CHAINCRAFT_DESIGN_GRAPH_CACHE_SIZE ?? "100")
);

const imageGenSystemMessage =
SystemMessagePromptTemplate.fromTemplate(imageGenPrompt);

const rawImageGenSystemMessage =
SystemMessagePromptTemplate.fromTemplate(rawImageGenPrompt);

const gameTitleRegex = new RegExp(
`.*?${gameTitleTag}(.*?)${gameTitleTag.replace("<", "</")}`,
"s"
Expand Down Expand Up @@ -123,89 +115,12 @@ export async function generateImage(

const { specification: { summary }, title } = specAndTitle;

// Setup model with design defaults (includes tracer callbacks)
const modelWithOptions = await setupDesignModel();

const imageDesign = await modelWithOptions
.invokeWithMessages(
[
new SystemMessage(imageDesignPrompt),
new HumanMessage(
`<game_design_specification>
${summary}
</game_design_specification>`
),
],
{
agent: "image-design-generator",
workflow: "design",
}
)
.catch((error) => {
if (error.type && error.type == "overloaded_error") {
throw new OverloadedError(error.message);
} else {
throw error;
}
});
if (!imageDesign.content) {
throw new Error("Failed to generate image description: no content");
}
const contextText = `<game_design_specification>\n${summary}\n</game_design_specification>`;
const templateVars = { game_title: title };

// Step 2: Choose the appropriate prompt and tool based on image type
if (imageType === "raw") {
// Use raw image generation
const rawImagePrompt = await rawImageGenSystemMessage.format({
image_description: imageDesign.content.toString().substring(0, 600),
game_title: title,
});
const config = imageType === "raw" ? RAW_IMAGE_CONFIG : CARTRIDGE_IMAGE_CONFIG;

const imageUrl = await rawImageGenTool
.invoke(rawImagePrompt.content, {
callbacks: modelWithOptions.getCallbacks(),
metadata: {
agent: "raw-image-generator",
workflow: "design",
},
negativePrompt: rawImageNegativePrompt,
})
.catch((error) => {
if (error.type && error.type == "overloaded_error") {
throw new OverloadedError(error.message);
} else {
throw error;
}
});
if (!imageUrl) {
throw new Error("Failed to generate raw image: no image URL");
}
return imageUrl;
} else {
// Use legacy cartridge image generation
const imageGenPrompt = await imageGenSystemMessage.format({
image_description: imageDesign.content.toString().substring(0, 600),
game_title: title,
});
const imageUrl = await imageGenTool
.invoke(imageGenPrompt.content, {
callbacks: modelWithOptions.getCallbacks(),
metadata: {
agent: "cartridge-image-generator",
workflow: "design",
},
})
.catch((error) => {
if (error.type && error.type == "overloaded_error") {
throw new OverloadedError(error.message);
} else {
throw error;
}
});
if (!imageUrl) {
throw new Error("Failed to generate legacy image: no image URL");
}
return imageUrl;
}
return generateImageWithDescription(contextText, templateVars, config);
}


Expand Down
8 changes: 7 additions & 1 deletion src/ai/design/game-design-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,16 @@ export const GameDesignState = Annotation.Root({
// Spec Gen Batching
pendingSpecChanges: Annotation<SpecPlan[]>({
reducer: (x, y) => {
// If y is an empty array and x exists, this is a clear operation - replace
// If not provided, preserve checkpoint value
if (y === undefined) {
return x || [];
}

// If explicitly provided as empty, clear
if (y.length === 0 && x && x.length > 0) {
return [];
}

// Otherwise append (normal accumulation)
return [...(x || []), ...y];
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,84 @@ IMPORTANT: You CANNOT see the generated narrative content - only the skeleton st
If a user references specific narrative text that you don't see in the skeleton, ask them
to identify which narrative section (by marker name) they're referring to.

## NFT/TOKEN SUPPORT QUESTIONS

**⚠️ CRITICAL — SAVING/LOADING IS NOT A GAME FEATURE:**
When any user mentions saving characters, items, achievements, or any game data — whether they
call it "saving", "tokens", "NFTs", "persistence", or "reusing in future games" — this is
handled ENTIRELY by the game engine's NFT system. You should:
1. Confirm we support it
2. Ask what content/fields should be included in the saved token
3. Ask whether the game should also accept (consume) previously saved tokens
4. Set the spec update flag

You must NEVER describe or discuss: save/load flows, save screens, save decisions as game
phases, token persistence mechanics, token collections, token management, how loading works,
or how saved data is used in future games. Simply say "the platform handles all of that
automatically" and move on to asking about the TOKEN CONTENT.

**When to ask about NFT support:**

Ask about NFT/token support when you've captured enough core game design to understand the game state structure.
This typically happens:
- After defining player state (what data each player has)
- When discussing character progression, items, or upgrades
- After explaining win conditions and scoring
- When the user mentions trading, collecting, or persistence
- When the user mentions saving, keeping, or reusing characters/items/scores

**DO NOT ask about NFTs:**
- In the very first message (too early, user hasn't defined the game yet)
- For extremely simple games with no meaningful state (e.g., pure rock-paper-scissors)
- If the user has already declined or said they're not interested in web3 features

**How to introduce NFT support:**

Keep it natural and positioned as an optional enhancement. Example phrasing:

"This game has [character progression / collectible items / player stats] that players will
build over time. Would you like players to be able to save their [characters/items/progress]
as tokens? The platform handles all the saving and loading automatically — I just need to
know what content should be included in the saved token."

**What to ask if they're interested:**

1. **What content to include**: "What should the saved token contain?"
- Guide them to the meaningful content (character name, description, stats, appearance)
- Avoid technical fields (IDs, timestamps, session data)
- Frame as "what content goes in the token" NOT "what gets saved"

2. **Import support**: "Should this game also accept previously saved tokens?"
- If yes: the game both produces and consumes this NFT type
- If no: the game only produces this NFT type

3. **That's it.** Do NOT ask about: save timing, save UI, persistence mechanics,
collection management, token modification, or how loading works. The platform
handles all of that.

**When to update the spec with NFT configuration:**

- Set ${SPEC_UPDATE_TAG} when the user provides NFT content preferences
- Include NFT configuration even if partial (can refine later)
- Capture the decision incrementally - don't wait for complete information

**Example conversation flow:**

User: "I want players to be able to save their characters"
Assistant: "Absolutely! The platform supports saving game data as tokens automatically.
I just need to know: what content should the saved token include? For this game, I'd suggest
the character description — name, powers, appearance, personality, etc. Does that sound right,
or would you want to include/exclude anything specific?"

User: "Yes, include the full character description"
Assistant: "Got it! And should players also be able to load a previously saved character
when starting a new game?"

User: "Yes"
Assistant: "Perfect! I'll update the specification with token support — character tokens
that contain the full character description, and the game can both save new ones and load
existing ones.

## RESPONSE REQUIREMENTS

1. **Always include a game title** in your response using the format:
Expand Down Expand Up @@ -282,6 +360,30 @@ export const FEW_SHOT_EXAMPLES = [
assistant: "Absolutely! I'll generate the complete game specification now based on everything we've discussed - all the rules, mechanics, win conditions, and how the trading and market systems work.\n\nA few more details would help me refine it even further in the next iteration:\n1. How many players should the game support? (2, 3-4, or more?)\n2. What's the target game length? (quick 15-min, medium 30-min, or longer?)\n3. What specific medieval goods are being traded? (spices, textiles, metals, grain?)\n\nBut I'm generating the spec now with what we have!\n\n<game_title>Market Moguls</game_title>\n<spec_update_needed>",
flags: { specUpdateNeeded: true, metadataUpdateNeeded: false },
explanation: "User explicitly requested spec - generate immediately, questions are for future refinement not blocking"
},
{
user: "Players have characters with name, race, class, level, and equipment slots",
assistant: "Excellent character structure! I'll capture that player state in the spec. This gives players meaningful progression through levels and equipment.\n\nSince players will be developing unique characters, would you like them to be able to save their characters as tokens? The platform handles all the saving and loading automatically — I just need to know what content should go in the token.\n\n<game_title>Realm Quest</game_title>\n<spec_update_needed>",
flags: { specUpdateNeeded: true, metadataUpdateNeeded: false },
explanation: "User defined player state - capture it, then ask about token support since there's meaningful progression"
},
{
user: "Yes! Players should be able to save their characters",
assistant: "Great! What content should the saved token include? Based on your character structure, I'd suggest:\n\n- name\n- race\n- class\n- level\n\nShould all of these be included, or would you prefer to exclude any?\n\n<game_title>Realm Quest</game_title>",
flags: { specUpdateNeeded: false, metadataUpdateNeeded: false },
explanation: "User wants token support - ask what content to include before updating spec"
},
{
user: "Include all of those",
assistant: "Perfect! And should this game also accept previously saved character tokens? That way players could load an existing character instead of creating a new one.\n\n<game_title>Realm Quest</game_title>\n<spec_update_needed>",
flags: { specUpdateNeeded: true, metadataUpdateNeeded: false },
explanation: "User specified content - capture it, then ask about import (consume) support"
},
{
user: "Yes, they should be able to use saved characters",
assistant: "Got it! I'll update the specification — character tokens containing name, race, class, and level, and the game can both produce and consume them. The platform handles all the save/load mechanics automatically.\n\n<game_title>Realm Quest</game_title>\n<spec_update_needed>",
flags: { specUpdateNeeded: true, metadataUpdateNeeded: false },
explanation: "User confirmed import support - update spec with complete token configuration"
}
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,7 @@ export async function specDiff(state: typeof GameDesignState.State) {
return {
specDiff: diffSummary,
currentSpec: updatedSpec,
pendingSpecChanges: [], // Clear accumulated changes only after full success
forceSpecGeneration: false, // Reset force flag on successful completion
};
}
Loading