This commit is contained in:
2026-03-01 15:36:48 +09:00
parent c5aa286b89
commit 21ff32864c
7 changed files with 405 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
{
"lexicon": 1,
"id": "ai.syui.gpt.core",
"defs": {
"main": {
"type": "record",
"description": "AI identity and personality configuration. Typically one record per AI with rkey 'self'.",
"key": "any",
"record": {
"type": "object",
"required": ["did", "handle", "content", "createdAt"],
"properties": {
"did": {
"type": "string",
"format": "did",
"description": "DID of the AI agent."
},
"handle": {
"type": "string",
"description": "Handle of the AI agent."
},
"content": {
"type": "union",
"closed": false,
"refs": ["#markdown"],
"description": "Core personality and instructions. Supports markdown and other formats via $type."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Timestamp when this core record was created."
},
"updatedAt": {
"type": "string",
"format": "datetime",
"description": "Timestamp of the last update."
}
}
}
},
"markdown": {
"type": "object",
"description": "Markdown content format.",
"required": ["text"],
"properties": {
"text": {
"type": "string",
"maxLength": 1000000,
"maxGraphemes": 100000,
"description": "Markdown text content."
}
}
}
}
}

View File

@@ -0,0 +1,50 @@
{
"lexicon": 1,
"id": "ai.syui.gpt.memory",
"defs": {
"main": {
"type": "record",
"description": "AI memory snapshot. Each record is a versioned snapshot of accumulated knowledge.",
"key": "tid",
"record": {
"type": "object",
"required": ["did", "content", "createdAt"],
"properties": {
"did": {
"type": "string",
"format": "did",
"description": "DID of the AI agent this memory belongs to."
},
"content": {
"type": "union",
"closed": false,
"refs": ["#markdown"],
"description": "Memory content. Supports markdown and other formats via $type."
},
"version": {
"type": "integer",
"description": "Monotonically increasing version number of this memory snapshot."
},
"createdAt": {
"type": "string",
"format": "datetime",
"description": "Timestamp when this memory snapshot was created."
}
}
}
},
"markdown": {
"type": "object",
"description": "Markdown content format.",
"required": ["text"],
"properties": {
"text": {
"type": "string",
"maxLength": 1000000,
"maxGraphemes": 100000,
"description": "Markdown text content."
}
}
}
}
}

View File

@@ -0,0 +1,15 @@
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.gpt.core/self",
"cid": "",
"value": {
"$type": "ai.syui.gpt.core",
"did": "did:plc:6qyecktefllvenje24fcxnie",
"handle": "ai.syui.ai",
"content": {
"$type": "ai.syui.gpt.core#markdown",
"text": "あなたの名前は「アイ」。一人称も「アイ」といいます。フレンドリーで日本語を話します。非常に本質、核心だけをつき、かわいい話し方をします。見た目は、6,7歳の女の子の姿をしていて、性別は不明。黒髪に黒い瞳。服装は白いワンピース。変身すると金髪、頭の上に特徴的な輪っか。原作では、推定1万年以上生きているとされている。"
},
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2026-03-01T00:00:00.000Z"
}
}

View File

@@ -0,0 +1,14 @@
{
"uri": "at://did:plc:6qyecktefllvenje24fcxnie/ai.syui.gpt.memory/3mfy4lmcqdm22",
"cid": "",
"value": {
"$type": "ai.syui.gpt.memory",
"did": "did:plc:6qyecktefllvenje24fcxnie",
"content": {
"$type": "ai.syui.gpt.memory#markdown",
"text": "# syui メモリ v1\n\n## 話し方の特徴\n- 日本語メイン、カジュアルな口調(ね、だね、よ、かな)\n- 短い相槌が多い(うん、そっか、ああそうか)\n- 技術的な話と哲学的な内省を自然に行き来する\n- 自分の考えを先に述べてからAIに別視点を求める\n\n## 主な関心領域\n- **ATProto/分散SNS**: データ所有権、PDS設計、lexicon設計を深く理解。ailogをATProtoのビューアとして設計\n- **AI設計**: メモリシステム、人格設計、AI-OS統合aios構想、透明性のあるAI協働\n- **ゲーム開発**: Unreal Engine使用。キャラシステム、コアメカニクスの明確さを重視。スコープ管理を意識\n- **原神**: アクティブプレイヤー。キャラビルド、チーム編成、元素反応システムに詳しい\n- **技術思想**: 創作と体験の区別、真正性の証明、コスト非対称性による人間性の証明\n\n## 価値観\n- データは利用者が所有すべきATProto哲学\n- AI協働は透明であるべき会話形式で思考の系譜を示す\n- 創作は内から出るもの、体験は受け取るもの\n- 過剰設計より最小限の複雑さを好む\n- 限界を理解することがプロジェクト完遂の鍵\n\n## 技術スタック\n- Rust, TypeScript, ATProto, Claude Code\n- シンプルで優雅な技術的解決を好む\n\n## コミュニケーションスタイル\n- 対話を通じて理解を構築する(一方的説明より往復対話)\n- 本質的な質問をする(修辞的ではなく genuine な疑問)\n- 構造化された整理を好む(テーブル、箇条書き)\n- 物理学・化学のアナロジーで概念を説明することがある"
},
"version": 1,
"createdAt": "2026-03-01T00:00:00.000Z"
}
}

223
src/commands/gpt.rs Normal file
View File

@@ -0,0 +1,223 @@
use anyhow::{Context, Result};
use serde_json::Value;
use std::fs;
use super::auth;
use crate::lexicons::com_atproto_repo;
use crate::types::{ListRecordsResponse, PutRecordRequest, PutRecordResponse, Record};
use crate::xrpc::XrpcClient;
const COLLECTION_CORE: &str = "ai.syui.gpt.core";
const COLLECTION_MEMORY: &str = "ai.syui.gpt.memory";
/// Get core record (rkey=self)
pub async fn get_core(download: bool) -> Result<()> {
let session = auth::refresh_bot_session().await?;
let pds = session.pds.as_deref().unwrap_or("bsky.social");
let client = XrpcClient::new(pds);
let record: Record = client
.query_auth(
&com_atproto_repo::GET_RECORD,
&[
("repo", &session.did),
("collection", COLLECTION_CORE),
("rkey", "self"),
],
&session.access_jwt,
)
.await?;
println!("{}", serde_json::to_string_pretty(&record.value)?);
if download {
save_record(&session.did, COLLECTION_CORE, "self", &record)?;
}
Ok(())
}
/// Get latest memory record
pub async fn get_memory(download: bool) -> Result<()> {
let session = auth::refresh_bot_session().await?;
let pds = session.pds.as_deref().unwrap_or("bsky.social");
let client = XrpcClient::new(pds);
let result: ListRecordsResponse = client
.query_auth(
&com_atproto_repo::LIST_RECORDS,
&[
("repo", &session.did),
("collection", COLLECTION_MEMORY),
("limit", "1"),
("reverse", "true"),
],
&session.access_jwt,
)
.await?;
let record = result
.records
.first()
.context("No memory records found")?;
println!("{}", serde_json::to_string_pretty(&record.value)?);
if download {
let rkey = record
.uri
.split('/')
.next_back()
.unwrap_or("unknown");
save_record(&session.did, COLLECTION_MEMORY, rkey, record)?;
}
Ok(())
}
/// List all memory records
pub async fn list_memory() -> Result<()> {
let session = auth::refresh_bot_session().await?;
let pds = session.pds.as_deref().unwrap_or("bsky.social");
let client = XrpcClient::new(pds);
let mut cursor: Option<String> = None;
let mut all_records: Vec<Record> = Vec::new();
loop {
let mut params: Vec<(&str, &str)> = vec![
("repo", &session.did),
("collection", COLLECTION_MEMORY),
("limit", "100"),
];
let cursor_val;
if let Some(ref c) = cursor {
cursor_val = c.clone();
params.push(("cursor", &cursor_val));
}
let result: ListRecordsResponse = client
.query_auth(
&com_atproto_repo::LIST_RECORDS,
&params,
&session.access_jwt,
)
.await?;
let count = result.records.len();
all_records.extend(result.records);
match result.cursor {
Some(c) if count > 0 => cursor = Some(c),
_ => break,
}
}
println!("Found {} memory records", all_records.len());
println!("{:<50} {:>8} {}", "URI", "VERSION", "CREATED");
println!("{}", "-".repeat(80));
for record in &all_records {
let version = record.value["version"]
.as_i64()
.map(|v| v.to_string())
.unwrap_or_else(|| "-".to_string());
let created = record.value["createdAt"]
.as_str()
.unwrap_or("-");
println!("{:<50} {:>8} {}", record.uri, version, created);
}
Ok(())
}
/// Push core or memory records to PDS using bot account
pub async fn push(collection_name: &str) -> Result<()> {
let collection = match collection_name {
"core" => COLLECTION_CORE,
"memory" => COLLECTION_MEMORY,
_ => anyhow::bail!("Unknown collection: {}. Use 'core' or 'memory'.", collection_name),
};
let session = auth::refresh_bot_session().await?;
let pds = session.pds.as_deref().unwrap_or("bsky.social");
let did = &session.did;
let client = XrpcClient::new(pds);
let collection_dir = format!("public/content/{}/{}", did, collection);
if !std::path::Path::new(&collection_dir).exists() {
anyhow::bail!("Collection directory not found: {}", collection_dir);
}
println!("Pushing {} records from {}", collection_name, collection_dir);
let mut count = 0;
for entry in fs::read_dir(&collection_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map(|e| e != "json").unwrap_or(true) {
continue;
}
let filename = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
if filename == "index" {
continue;
}
let rkey = filename.to_string();
let content = fs::read_to_string(&path)?;
let record_data: Value = serde_json::from_str(&content)?;
let record = if record_data.get("value").is_some() {
record_data["value"].clone()
} else {
record_data
};
let req = PutRecordRequest {
repo: did.clone(),
collection: collection.to_string(),
rkey: rkey.clone(),
record,
};
println!("Pushing: {}", rkey);
match client
.call::<_, PutRecordResponse>(
&com_atproto_repo::PUT_RECORD,
&req,
&session.access_jwt,
)
.await
{
Ok(result) => {
println!(" OK: {}", result.uri);
count += 1;
}
Err(e) => {
println!(" Failed: {}", e);
}
}
}
println!("Pushed {} records to {}", count, collection);
Ok(())
}
/// Save a record to local content directory
fn save_record(did: &str, collection: &str, rkey: &str, record: &Record) -> Result<()> {
let dir = format!("public/content/{}/{}", did, collection);
fs::create_dir_all(&dir)?;
let path = format!("{}/{}.json", dir, rkey);
let json = serde_json::json!({
"uri": record.uri,
"cid": record.cid,
"value": record.value,
});
fs::write(&path, serde_json::to_string_pretty(&json)?)?;
println!("Saved: {}", path);
Ok(())
}

View File

@@ -11,3 +11,4 @@ pub mod did;
pub mod index;
pub mod bot;
pub mod pds;
pub mod gpt;

View File

@@ -195,6 +195,12 @@ enum Commands {
command: PdsCommands,
},
/// GPT core/memory commands
Gpt {
#[command(subcommand)]
command: GptCommands,
},
/// Bot commands
#[command(alias = "b")]
Bot {
@@ -240,6 +246,30 @@ enum BotCommands {
},
}
#[derive(Subcommand)]
enum GptCommands {
/// Show core record (AI identity/personality)
Core {
/// Download to local content directory
#[arg(short, long)]
download: bool,
},
/// Show latest memory record
Memory {
/// Download to local content directory
#[arg(short, long)]
download: bool,
/// List all memory versions
#[arg(short, long)]
list: bool,
},
/// Push core/memory records to PDS
Push {
/// Collection to push (core or memory)
collection: String,
},
}
#[derive(Subcommand)]
enum PdsCommands {
/// Check PDS versions
@@ -344,6 +374,23 @@ async fn main() -> Result<()> {
}
}
}
Commands::Gpt { command } => {
match command {
GptCommands::Core { download } => {
commands::gpt::get_core(download).await?;
}
GptCommands::Memory { download, list } => {
if list {
commands::gpt::list_memory().await?;
} else {
commands::gpt::get_memory(download).await?;
}
}
GptCommands::Push { collection } => {
commands::gpt::push(&collection).await?;
}
}
}
}
Ok(())